@@ -234,6 +319,7 @@ function TextArea({
'focus:bg-white focus:outline-none focus:shadow-focus-inner',
classes,
)}
+ onInput={(e: Event) => onEditText((e.target as HTMLInputElement).value)}
{...restProps}
ref={textareaRef}
/>
@@ -242,9 +328,30 @@ function TextArea({
open={popoverOpen}
onClose={() => setPopoverOpen(false)}
anchorElementRef={textareaRef}
- classes="p-2"
+ classes="p-1"
>
- Suggestions
+
+ {suggestions.map((s, index) => (
+ -
+ {s.username}
+ {s.displayName}
+
+ ))}
+ {suggestions.length === 0 && (
+ -
+ No matches. You can still write the username
+
+ )}
+
)}
@@ -392,6 +499,13 @@ export type MarkdownEditorProps = {
text: string;
onEditText?: (text: string) => void;
+
+ /**
+ * List of users who have annotated current document and belong to active group.
+ * This is used to populate the @mentions suggestions, when `atMentionsEnabled`
+ * is `true`.
+ */
+ usersWhoAnnotated: UserItem[];
};
/**
@@ -403,6 +517,7 @@ export default function MarkdownEditor({
onEditText = () => {},
text,
textStyle = {},
+ usersWhoAnnotated,
}: MarkdownEditorProps) {
// Whether the preview mode is currently active.
const [preview, setPreview] = useState(false);
@@ -467,12 +582,11 @@ export default function MarkdownEditor({
containerRef={input}
onClick={(e: Event) => e.stopPropagation()}
onKeyDown={handleKeyDown}
- onInput={(e: Event) =>
- onEditText((e.target as HTMLInputElement).value)
- }
+ onEditText={onEditText}
value={text}
style={textStyle}
atMentionsEnabled={atMentionsEnabled}
+ usersWhoAnnotated={usersWhoAnnotated}
/>
)}
diff --git a/src/sidebar/components/test/MarkdownEditor-test.js b/src/sidebar/components/test/MarkdownEditor-test.js
index b8efe9ca920..598a09d0337 100644
--- a/src/sidebar/components/test/MarkdownEditor-test.js
+++ b/src/sidebar/components/test/MarkdownEditor-test.js
@@ -2,7 +2,7 @@ import {
checkAccessibility,
mockImportedComponents,
} from '@hypothesis/frontend-testing';
-import { mount } from '@hypothesis/frontend-testing';
+import { mount, unmountAll } from '@hypothesis/frontend-testing';
import { render } from 'preact';
import { act } from 'preact/test-utils';
@@ -23,6 +23,7 @@ describe('MarkdownEditor', () => {
};
let fakeIsMacOS;
let MarkdownView;
+ let fakeStore;
beforeEach(() => {
fakeMarkdownCommands.convertSelectionToLink.resetHistory();
@@ -30,6 +31,10 @@ describe('MarkdownEditor', () => {
fakeMarkdownCommands.toggleSpanStyle.resetHistory();
fakeIsMacOS = sinon.stub().returns(false);
+ fakeStore = {
+ isFeatureEnabled: sinon.stub().returns(false),
+ };
+
MarkdownView = function MarkdownView() {
return null;
};
@@ -41,11 +46,13 @@ describe('MarkdownEditor', () => {
'../../shared/user-agent': {
isMacOS: fakeIsMacOS,
},
+ '../store': { useSidebarStore: () => fakeStore },
});
});
afterEach(() => {
$imports.$restore();
+ unmountAll();
});
function createComponent(props = {}, mountProps = {}) {
diff --git a/src/sidebar/store/modules/annotations.ts b/src/sidebar/store/modules/annotations.ts
index 1ad0dad2b7b..78034c57d62 100644
--- a/src/sidebar/store/modules/annotations.ts
+++ b/src/sidebar/store/modules/annotations.ts
@@ -8,6 +8,7 @@ import { createSelector } from 'reselect';
import { hasOwn } from '../../../shared/has-own';
import type { Annotation, SavedAnnotation } from '../../../types/api';
import type { HighlightCluster } from '../../../types/shared';
+import { username as getUsername } from '../../helpers/account-id';
import * as metadata from '../../helpers/annotation-metadata';
import { isHighlight, isSaved } from '../../helpers/annotation-metadata';
import { countIf, toTrueMap, trueKeys } from '../../util/collections';
@@ -34,6 +35,11 @@ type AnnotationStub = {
$tag?: string;
};
+export type UserItem = {
+ user: string;
+ displayName: string | null;
+};
+
const initialState = {
annotations: [],
highlighted: {},
@@ -567,6 +573,37 @@ const savedAnnotations = createSelector(
annotations => annotations.filter(ann => isSaved(ann)) as SavedAnnotation[],
);
+/**
+ * Return the list of unique users who authored any annotation, ordered by username.
+ */
+const usersWhoAnnotated = createSelector(
+ (state: State) => state.annotations,
+ annotations => {
+ const usersMap = new Map<
+ string,
+ { user: string; username: string; displayName: string | null }
+ >();
+ annotations.forEach(anno => {
+ const { user } = anno;
+ const username = getUsername(user);
+ const displayName = anno.user_info?.display_name ?? null;
+
+ // Keep a unique list of users
+ if (!usersMap.has(user)) {
+ usersMap.set(user, { user, username, displayName });
+ }
+ });
+
+ // Sort users by username
+ return [...usersMap.values()].sort((a, b) => {
+ const lowerAUsername = a.username.toLowerCase();
+ const lowerBUsername = b.username.toLowerCase();
+
+ return lowerAUsername.localeCompare(lowerBUsername);
+ });
+ },
+);
+
export const annotationsModule = createStoreModule(initialState, {
namespace: 'annotations',
reducers,
@@ -597,5 +634,6 @@ export const annotationsModule = createStoreModule(initialState, {
noteCount,
orphanCount,
savedAnnotations,
+ usersWhoAnnotated,
},
});