-
Notifications
You must be signed in to change notification settings - Fork 204
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Wrap display-name-based mentions in mention tags #6892
Conversation
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #6892 +/- ##
=======================================
Coverage 99.42% 99.43%
=======================================
Files 273 273
Lines 10523 10562 +39
Branches 2529 2539 +10
=======================================
+ Hits 10463 10502 +39
Misses 60 60 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
3433676
to
c33e390
Compare
* special tag, as long as they are surrounded by "empty" space (space, tab, new | ||
* line, or beginning/end of the whole text). | ||
* Wrap all occurrences of @mention in provided text into the corresponding | ||
* special tag, as long as they are surrounded by boundary chars. | ||
* | ||
* For example: `@someuser` with the `hypothes.is` authority would become | ||
* `<a data-hyp-mention data-userid="acct:[email protected]">@someuser</a>` | ||
*/ | ||
export function wrapMentions(text: string, authority: string): string { |
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.
Maybe this should be renamed to wrapUsernameMentions
for consistency with the new wrapDisplayNameMentions
async save(annotation: Annotation) { | ||
async save( | ||
annotation: Annotation, | ||
mentionsOptions: MentionsOptions = { mentionMode: 'username' }, |
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.
This param should be mandatory, as some cases where annotations are saved are currently not taking into consideration the mention mode:
- Autosave.
- Annotations import.
I'll address those separately.
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.
OK. Worth adding a TODO
here about this?
c33e390
to
299df1b
Compare
const usersDisplayNamesMapRef = useRef<Map<string, UserItem>>( | ||
new Map( | ||
mentionMode === 'username' | ||
? [] | ||
: // If the annotation is being edited, it may have mentions. Use them to | ||
// initialize the display names map | ||
annotation.mentions | ||
?.filter(mention => !!mention.display_name) | ||
.map(({ userid, username, display_name: displayName }) => [ | ||
displayName!, | ||
{ userid, username, displayName }, | ||
]), | ||
), | ||
); |
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 not 100% sure if a ref is the right way to track the map of display names. If the annotation changed while the editor is open, it could cause the old mentions to still be incorrectly tracked, but as far as I can tell, that's something that cannot happen, as every annotation has its own editor component instance.
I didn't use a memoized value because the map starts with an initial list of students, but then it needs to be editable.
I didn't use a regular piece of state (useState
) because the component doesn't need to re-render when the map changes. Also, trying to address the issue I explained above with a piece of state would require updating it in a side effect, which is tricky to do right and usually discouraged.
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.
Actually, I'm thinking a possible solution would be to not initialize this here based on annotation.mentions
. Instead, use it to track inserted suggestions only, and at the moment of saving the annotation, then "merging" that with annotation.mentions
. That way the map will match the annotation at the moment of saving.
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.
When you begin editing an annotation, a new draft is created with tags and text initialized from the values at the point when editing starts. If the annotation were updated on another system while the draft was being edited, what we probably ought to do is inform the user of the conflict when they try to save. This has never been an issue in practice so we haven't implemented it. The mention text <-> user info map is initialized here at the same time as the draft, which I think is the correct thing to do.
299df1b
to
9e08dbb
Compare
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.
This generally looks good and works as expected. I noted a few issues that can be handled in follow up work (handling display names containing square brackets, handling display name changes to a mentioned user when editing an existing annotation) and requested a few comments and small changes in the tests.
mentionMode === 'username' | ||
? [] | ||
: // If the annotation is being edited, it may have mentions. Use them to | ||
// initialize the display names map |
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.
What will happen when editing an existing annotation with a mention, where the user has changed their display name since the mention was originally created? To handle that I think we'd need to use the current display name when unwrapping the mention, rather than whatever the mention text was originally. I think that can be left out of this PR, but should be tracked somewhere.
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.
Yeah, good point. In fact, I think this also applies to username-based mentions. If their username has changed, we should unwrap as the most recent username, otherwise when wrapping back it will generate an invalid userid.
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 created this issue to track it #6906
toastMessenger.success(successMessage, { visuallyHidden: true }); | ||
usersDisplayNamesMapRef.current = new Map(); |
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.
You could also use the map's clear
method. This would require that none of the functions that the map has been passed to ever modify the map.
src/sidebar/components/Annotation/test/AnnotationEditor-test.js
Outdated
Show resolved
Hide resolved
src/sidebar/components/Annotation/test/AnnotationEditor-test.js
Outdated
Show resolved
Hide resolved
// Pattern that matches display names. | ||
// Display names can have any amount of characters, except opening or closing | ||
// square brackets, which are used to delimit the display name itself. | ||
const DISPLAY_NAME_PAT = String.raw`[^\]^[]*`; |
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.
Display names in Hypothesis can contain square brackets, although we expect this to be rare. When inserting mentions and unwrapping text we should do something sensible, such as stripping these characters from the inserted text. This can be handled in a follow-up PR as long as we're tracking it somewhere.
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 see this is tracked in #6891.
} | ||
|
||
const [atChar, ...rest] = mention; | ||
return `${atChar}[${rest.join('')}]`; |
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.
As noted above, we'll need to strip out or otherwise handle square brackets in the display name (rest
) here.
: mentionsOptions.mentionMode === 'display-name' | ||
? (text: string) => | ||
wrapDisplayNameMentions(text, mentionsOptions.usersMap) | ||
: (text: string) => wrapMentions(text, authority); |
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.
This is OK, although I find that ternaries in JS can get difficult to follow when nested.
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.
Yeah, I tend to agree. Let me see if I can find a better way to express this.
async save(annotation: Annotation) { | ||
async save( | ||
annotation: Annotation, | ||
mentionsOptions: MentionsOptions = { mentionMode: 'username' }, |
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.
OK. Worth adding a TODO
here about this?
9e08dbb
to
dd60dfe
Compare
Part of #6886
This PR ensures that display-name-based mentions can be converted to mention tags while saving the annotation, and from mention tags back to plain text when entering edit mode.
display-name-mentions-2025-03-18_10.33.44.mp4
There are a few considerations/topics to discuss:
The later is in fact tricky to handle, as one could export annotations from a public context (where username-based mentions were used), and the import them in a display-name-mention context.
I'll address this issue separately.
If an "unknown" mention is found (for example, one that was manually typed), we need to decide what to do. This PR simply keeps that as plain text as written, but we may try to handle it as an invalid mention instead.
Not covered by this PR: