-
-
Notifications
You must be signed in to change notification settings - Fork 833
Mentions as links rte #10422
Mentions as links rte #10422
Changes from 49 commits
177d861
a52aa38
1a17948
05cc8a3
be01b92
7ba881d
8abcb62
03c6509
cbc3723
6b34a80
a4e1cdf
1c34cdc
ff12d92
8e45663
2ad0eab
191f58f
37b2043
76eb06f
74c3a23
013c22e
ab0e84b
99a98f1
631ec37
14cd947
8fe6390
a97d57c
9d4b330
cf9456c
9c2e71a
d3bf2f5
84576bc
26dfec7
036a04b
75ecc15
a48f87b
587ea1a
6c4d893
5616f99
d47234b
78100d9
392bc86
7457b4e
a834963
0b1e510
cde55ab
57d4db5
21cc283
c3d3da8
ec61bef
9b964d1
c277cbe
108b3d8
e2522d5
b5a5af9
6501013
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
/* | ||
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, useRef } 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 { | ||
suggestion: MappedSuggestion | null; | ||
handleMention: FormattingFunctions["mention"]; | ||
} | ||
|
||
// Helper function that takes the rust suggestion and builds the query for the | ||
// Autocomplete. This will change as we implement / commands. | ||
// Returns an empty string if we don't want to show the suggestion menu. | ||
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}`; | ||
} | ||
|
||
// Helper function to get the mention text for a room as this is less straightforward | ||
// than it is for determining the text we display for a user. | ||
// TODO determine if it's worth bringing the switch case into this function to make it | ||
// into a more general `getMentionText` component | ||
function getRoomMentionText(completion: ICompletion, client: MatrixClient): string { | ||
const alias = completion.completion; | ||
const roomId = completion.completionId; | ||
|
||
let roomForAutocomplete: Room | undefined; | ||
if (roomId || alias[0] !== "#") { | ||
roomForAutocomplete = client.getRoom(roomId || alias) ?? undefined; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I found this logic slightly confusing, and I think it also does something surprising (but probably harmless) when there is no roomId and alias does not start with It might be easier to understand if we separated the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm reticent to change the behaviour as this comes from Let me know what you think about the change. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The change looks good. Could you put a comment in? Maybe something like |
||
} else { | ||
roomForAutocomplete = client.getRooms().find((r) => { | ||
return r.getCanonicalAlias() === alias || r.getAltAliases().includes(alias); | ||
}); | ||
} | ||
|
||
return roomForAutocomplete?.name || alias; | ||
} | ||
|
||
const WysiwygAutocomplete = forwardRef( | ||
andybalaam marked this conversation as resolved.
Show resolved
Hide resolved
|
||
({ suggestion, handleMention }: WysiwygAutocompleteProps, ref: ForwardedRef<Autocomplete>): JSX.Element | null => { | ||
const { room } = useRoomContext(); | ||
const client = useMatrixClientContext(); | ||
|
||
const autocompleteIndexRef = useRef<number>(0); | ||
andybalaam marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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; | ||
} | ||
} | ||
|
||
function handleSelectionChange(completionIndex: number): void { | ||
autocompleteIndexRef.current = completionIndex; | ||
} | ||
|
||
return room ? ( | ||
<div className="mx_SendWysiwygComposer_AutoCompleteWrapper" data-testid="autocomplete-wrapper"> | ||
<Autocomplete | ||
ref={ref} | ||
query={buildQuery(suggestion)} | ||
onConfirm={handleConfirm} | ||
onSelectionChange={handleSelectionChange} | ||
selection={{ start: 0, end: 0 }} | ||
room={room} | ||
/> | ||
</div> | ||
) : null; | ||
}, | ||
); | ||
|
||
WysiwygAutocomplete.displayName = "WysiwygAutocomplete"; | ||
|
||
export { WysiwygAutocomplete }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,15 +14,21 @@ See the License for the specific language governing permissions and | |
limitations under the License. | ||
*/ | ||
|
||
import React, { memo, MutableRefObject, ReactNode, useEffect } from "react"; | ||
import React, { memo, MutableRefObject, ReactNode, useEffect, useRef } 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; | ||
|
@@ -47,21 +53,53 @@ export const WysiwygComposer = memo(function WysiwygComposer({ | |
rightComponent, | ||
children, | ||
}: WysiwygComposerProps) { | ||
const inputEventProcessor = useInputEventProcessor(onSend, initialContent); | ||
const { room } = useRoomContext(); | ||
const autocompleteRef = useRef<Autocomplete | null>(null); | ||
|
||
const { ref, isWysiwygReady, content, actionStates, wysiwyg } = useWysiwyg({ initialContent, inputEventProcessor }); | ||
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); | ||
|
||
useEffect(() => { | ||
if (!disabled && content !== null) { | ||
onChange?.(content); | ||
} | ||
}, [onChange, content, disabled]); | ||
|
||
const isReady = isWysiwygReady && !disabled; | ||
useSetCursorPosition(!isReady, ref); | ||
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 { isFocused, onFocus } = useIsFocused(); | ||
const computedPlaceholder = (!content && placeholder) || undefined; | ||
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]); | ||
Comment on lines
+77
to
+102
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We get link tags (representing mentions) from the rust model in a string of HTML that gets set as inner HTML on any update to the rust model. We will be able to style the links using attributes they have, but for click handling we need to add/remove the handlers on any change of the rust model output. |
||
|
||
return ( | ||
<div | ||
|
@@ -70,6 +108,7 @@ 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} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm fairly sure we write our comments in tsdoc style - I have asked for confirmation in our internal room. Assuming I'm right, please could you use that style here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, tsdoc please.