This reverts commit 1af71089dd
.
This commit is contained in:
parent
58a4003a59
commit
63678603e0
11 changed files with 23 additions and 585 deletions
|
@ -1,137 +0,0 @@
|
|||
/*
|
||||
Copyright 2023 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, { ForwardedRef, forwardRef } from "react";
|
||||
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { FormattingFunctions, MappedSuggestion } from "@matrix-org/matrix-wysiwyg";
|
||||
|
||||
import { useRoomContext } from "../../../../../contexts/RoomContext";
|
||||
import Autocomplete from "../../Autocomplete";
|
||||
import { ICompletion } from "../../../../../autocomplete/Autocompleter";
|
||||
import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext";
|
||||
|
||||
interface WysiwygAutocompleteProps {
|
||||
/**
|
||||
* The suggestion output from the rust model is used to build the query that is
|
||||
* passed to the `<Autocomplete />` component
|
||||
*/
|
||||
suggestion: MappedSuggestion | null;
|
||||
|
||||
/**
|
||||
* This handler will be called with the href and display text for a mention on clicking
|
||||
* a mention in the autocomplete list or pressing enter on a selected item
|
||||
*/
|
||||
handleMention: FormattingFunctions["mention"];
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the query for the `<Autocomplete />` component from the rust suggestion. This
|
||||
* will change as we implement handling / commands.
|
||||
*
|
||||
* @param suggestion - represents if the rust model is tracking a potential mention
|
||||
* @returns an empty string if we can not generate a query, otherwise a query beginning
|
||||
* with @ for a user query, # for a room or space query
|
||||
*/
|
||||
function buildQuery(suggestion: MappedSuggestion | null): string {
|
||||
if (!suggestion || !suggestion.keyChar || suggestion.type === "command") {
|
||||
// if we have an empty key character, we do not build a query
|
||||
// TODO implement the command functionality
|
||||
return "";
|
||||
}
|
||||
|
||||
return `${suggestion.keyChar}${suggestion.text}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a room type mention, determine the text that should be displayed in the mention
|
||||
* TODO expand this function to more generally handle outputting the display text from a
|
||||
* given completion
|
||||
*
|
||||
* @param completion - the item selected from the autocomplete, currently treated as a room completion
|
||||
* @param client - the MatrixClient is required for us to look up the correct room mention text
|
||||
* @returns the text to display in the mention
|
||||
*/
|
||||
function getRoomMentionText(completion: ICompletion, client: MatrixClient): string {
|
||||
const roomId = completion.completionId;
|
||||
const alias = completion.completion;
|
||||
|
||||
let roomForAutocomplete: Room | null | undefined;
|
||||
|
||||
// Not quite sure if the logic here makes sense - specifically calling .getRoom with an alias
|
||||
// that doesn't start with #, but keeping the logic the same as in PartCreator.roomPill for now
|
||||
if (roomId) {
|
||||
roomForAutocomplete = client.getRoom(roomId);
|
||||
} else if (!alias.startsWith("#")) {
|
||||
roomForAutocomplete = client.getRoom(alias);
|
||||
} else {
|
||||
roomForAutocomplete = client.getRooms().find((r) => {
|
||||
return r.getCanonicalAlias() === alias || r.getAltAliases().includes(alias);
|
||||
});
|
||||
}
|
||||
|
||||
// if we haven't managed to find the room, use the alias as a fallback
|
||||
return roomForAutocomplete?.name || alias;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the current suggestion from the rust model and a handler function, this component
|
||||
* will display the legacy `<Autocomplete />` component (as used in the BasicMessageComposer)
|
||||
* and call the handler function with the required arguments when a mention is selected
|
||||
*
|
||||
* @param props.ref - the ref will be attached to the rendered `<Autocomplete />` component
|
||||
*/
|
||||
const WysiwygAutocomplete = forwardRef(
|
||||
({ suggestion, handleMention }: WysiwygAutocompleteProps, ref: ForwardedRef<Autocomplete>): JSX.Element | null => {
|
||||
const { room } = useRoomContext();
|
||||
const client = useMatrixClientContext();
|
||||
|
||||
function handleConfirm(completion: ICompletion): void {
|
||||
if (!completion.href) return;
|
||||
|
||||
switch (completion.type) {
|
||||
case "user":
|
||||
handleMention(completion.href, completion.completion);
|
||||
break;
|
||||
case "room": {
|
||||
handleMention(completion.href, getRoomMentionText(completion, client));
|
||||
break;
|
||||
}
|
||||
// TODO implement the command functionality
|
||||
// case "command":
|
||||
// console.log("/command functionality not yet in place");
|
||||
// break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return room ? (
|
||||
<div className="mx_SendWysiwygComposer_AutoCompleteWrapper" data-testid="autocomplete-wrapper">
|
||||
<Autocomplete
|
||||
ref={ref}
|
||||
query={buildQuery(suggestion)}
|
||||
onConfirm={handleConfirm}
|
||||
selection={{ start: 0, end: 0 }}
|
||||
room={room}
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
},
|
||||
);
|
||||
|
||||
WysiwygAutocomplete.displayName = "WysiwygAutocomplete";
|
||||
|
||||
export { WysiwygAutocomplete };
|
|
@ -14,21 +14,15 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { memo, MutableRefObject, ReactNode, useEffect, useRef } from "react";
|
||||
import React, { memo, MutableRefObject, ReactNode, useEffect } from "react";
|
||||
import { useWysiwyg, FormattingFunctions } from "@matrix-org/matrix-wysiwyg";
|
||||
import classNames from "classnames";
|
||||
|
||||
import Autocomplete from "../../Autocomplete";
|
||||
import { WysiwygAutocomplete } from "./WysiwygAutocomplete";
|
||||
import { FormattingButtons } from "./FormattingButtons";
|
||||
import { Editor } from "./Editor";
|
||||
import { useInputEventProcessor } from "../hooks/useInputEventProcessor";
|
||||
import { useSetCursorPosition } from "../hooks/useSetCursorPosition";
|
||||
import { useIsFocused } from "../hooks/useIsFocused";
|
||||
import { useRoomContext } from "../../../../../contexts/RoomContext";
|
||||
import defaultDispatcher from "../../../../../dispatcher/dispatcher";
|
||||
import { Action } from "../../../../../dispatcher/actions";
|
||||
import { parsePermalink } from "../../../../../utils/permalinks/Permalinks";
|
||||
|
||||
interface WysiwygComposerProps {
|
||||
disabled?: boolean;
|
||||
|
@ -53,20 +47,9 @@ export const WysiwygComposer = memo(function WysiwygComposer({
|
|||
rightComponent,
|
||||
children,
|
||||
}: WysiwygComposerProps) {
|
||||
const { room } = useRoomContext();
|
||||
const autocompleteRef = useRef<Autocomplete | null>(null);
|
||||
const inputEventProcessor = useInputEventProcessor(onSend, initialContent);
|
||||
|
||||
const inputEventProcessor = useInputEventProcessor(onSend, autocompleteRef, initialContent);
|
||||
const { ref, isWysiwygReady, content, actionStates, wysiwyg, suggestion } = useWysiwyg({
|
||||
initialContent,
|
||||
inputEventProcessor,
|
||||
});
|
||||
const { isFocused, onFocus } = useIsFocused();
|
||||
|
||||
const isReady = isWysiwygReady && !disabled;
|
||||
const computedPlaceholder = (!content && placeholder) || undefined;
|
||||
|
||||
useSetCursorPosition(!isReady, ref);
|
||||
const { ref, isWysiwygReady, content, actionStates, wysiwyg } = useWysiwyg({ initialContent, inputEventProcessor });
|
||||
|
||||
useEffect(() => {
|
||||
if (!disabled && content !== null) {
|
||||
|
@ -74,32 +57,11 @@ export const WysiwygComposer = memo(function WysiwygComposer({
|
|||
}
|
||||
}, [onChange, content, disabled]);
|
||||
|
||||
useEffect(() => {
|
||||
function handleClick(e: Event): void {
|
||||
e.preventDefault();
|
||||
if (
|
||||
e.target &&
|
||||
e.target instanceof HTMLAnchorElement &&
|
||||
e.target.getAttribute("data-mention-type") === "user"
|
||||
) {
|
||||
const parsedLink = parsePermalink(e.target.href);
|
||||
if (room && parsedLink?.userId)
|
||||
defaultDispatcher.dispatch({
|
||||
action: Action.ViewUser,
|
||||
member: room.getMember(parsedLink.userId),
|
||||
});
|
||||
}
|
||||
}
|
||||
const isReady = isWysiwygReady && !disabled;
|
||||
useSetCursorPosition(!isReady, ref);
|
||||
|
||||
const mentions = ref.current?.querySelectorAll("a[data-mention-type]");
|
||||
if (mentions) {
|
||||
mentions.forEach((mention) => mention.addEventListener("click", handleClick));
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (mentions) mentions.forEach((mention) => mention.removeEventListener("click", handleClick));
|
||||
};
|
||||
}, [ref, room, content]);
|
||||
const { isFocused, onFocus } = useIsFocused();
|
||||
const computedPlaceholder = (!content && placeholder) || undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -108,7 +70,6 @@ export const WysiwygComposer = memo(function WysiwygComposer({
|
|||
onFocus={onFocus}
|
||||
onBlur={onFocus}
|
||||
>
|
||||
<WysiwygAutocomplete ref={autocompleteRef} suggestion={suggestion} handleMention={wysiwyg.mention} />
|
||||
<FormattingButtons composer={wysiwyg} actionStates={actionStates} />
|
||||
<Editor
|
||||
ref={ref}
|
||||
|
|
|
@ -32,11 +32,9 @@ import { useMatrixClientContext } from "../../../../../contexts/MatrixClientCont
|
|||
import { isCaretAtEnd, isCaretAtStart } from "../utils/selection";
|
||||
import { getEventsFromEditorStateTransfer, getEventsFromRoom } from "../utils/event";
|
||||
import { endEditing } from "../utils/editing";
|
||||
import Autocomplete from "../../Autocomplete";
|
||||
|
||||
export function useInputEventProcessor(
|
||||
onSend: () => void,
|
||||
autocompleteRef: React.RefObject<Autocomplete>,
|
||||
initialContent?: string,
|
||||
): (event: WysiwygEvent, composer: Wysiwyg, editor: HTMLElement) => WysiwygEvent | null {
|
||||
const roomContext = useRoomContext();
|
||||
|
@ -53,10 +51,6 @@ export function useInputEventProcessor(
|
|||
const send = (): void => {
|
||||
event.stopPropagation?.();
|
||||
event.preventDefault?.();
|
||||
// do not send the message if we have the autocomplete open, regardless of settings
|
||||
if (autocompleteRef?.current && !autocompleteRef.current.state.hide) {
|
||||
return;
|
||||
}
|
||||
onSend();
|
||||
};
|
||||
|
||||
|
@ -71,13 +65,12 @@ export function useInputEventProcessor(
|
|||
roomContext,
|
||||
composerContext,
|
||||
mxClient,
|
||||
autocompleteRef,
|
||||
);
|
||||
} else {
|
||||
return handleInputEvent(event, send, isCtrlEnterToSend);
|
||||
}
|
||||
},
|
||||
[isCtrlEnterToSend, onSend, initialContent, roomContext, composerContext, mxClient, autocompleteRef],
|
||||
[isCtrlEnterToSend, onSend, initialContent, roomContext, composerContext, mxClient],
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -92,51 +85,12 @@ function handleKeyboardEvent(
|
|||
roomContext: IRoomState,
|
||||
composerContext: ComposerContextState,
|
||||
mxClient: MatrixClient,
|
||||
autocompleteRef: React.RefObject<Autocomplete>,
|
||||
): KeyboardEvent | null {
|
||||
const { editorStateTransfer } = composerContext;
|
||||
const isEditing = Boolean(editorStateTransfer);
|
||||
const isEditorModified = isEditing ? initialContent !== composer.content() : composer.content().length !== 0;
|
||||
const action = getKeyBindingsManager().getMessageComposerAction(event);
|
||||
|
||||
const autocompleteIsOpen = autocompleteRef?.current && !autocompleteRef.current.state.hide;
|
||||
|
||||
// we need autocomplete to take priority when it is open for using enter to select
|
||||
if (autocompleteIsOpen) {
|
||||
let handled = false;
|
||||
const autocompleteAction = getKeyBindingsManager().getAutocompleteAction(event);
|
||||
const component = autocompleteRef.current;
|
||||
if (component && component.countCompletions() > 0) {
|
||||
switch (autocompleteAction) {
|
||||
case KeyBindingAction.ForceCompleteAutocomplete:
|
||||
case KeyBindingAction.CompleteAutocomplete:
|
||||
autocompleteRef.current.onConfirmCompletion();
|
||||
handled = true;
|
||||
break;
|
||||
case KeyBindingAction.PrevSelectionInAutocomplete:
|
||||
autocompleteRef.current.moveSelection(-1);
|
||||
handled = true;
|
||||
break;
|
||||
case KeyBindingAction.NextSelectionInAutocomplete:
|
||||
autocompleteRef.current.moveSelection(1);
|
||||
handled = true;
|
||||
break;
|
||||
case KeyBindingAction.CancelAutocomplete:
|
||||
autocompleteRef.current.onEscape(event as {} as React.KeyboardEvent);
|
||||
handled = true;
|
||||
break;
|
||||
default:
|
||||
break; // don't return anything, allow event to pass through
|
||||
}
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return event;
|
||||
}
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case KeyBindingAction.SendMessage:
|
||||
send();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue