diff --git a/api/resolvers/user.js b/api/resolvers/user.js index f872e8d3..3deb0c47 100644 --- a/api/resolvers/user.js +++ b/api/resolvers/user.js @@ -99,6 +99,31 @@ export default { users } }, + userSuggestions: async (parent, { q, limit = 5 }, { models }) => { + let users = [] + if (q) { + users = await models.$queryRaw` + SELECT name + FROM users + WHERE ( + id > ${RESERVED_MAX_USER_ID} OR id IN (${ANON_USER_ID}, ${DELETE_USER_ID}) + ) + AND SIMILARITY(name, ${q}) > 0.1 + ORDER BY SIMILARITY(name, ${q}) DESC + LIMIT ${limit}` + } else { + users = await models.$queryRaw` + SELECT name + FROM user_stats_days + JOIN users on users.id = user_stats_days.id + WHERE NOT users."hideFromTopUsers" + AND user_stats_days.day = (SELECT max(day) FROM user_stats_days) + ORDER BY msats_stacked DESC, users.created_at ASC + LIMIT ${limit}` + } + + return users + }, topUsers: async (parent, { cursor, when, by, from, to, limit = LIMIT }, { models, me }) => { const decodedCursor = decodeCursor(cursor) const range = whenRange(when, from, to || decodeCursor.time) diff --git a/api/typeDefs/user.js b/api/typeDefs/user.js index 620c791c..2fef0bed 100644 --- a/api/typeDefs/user.js +++ b/api/typeDefs/user.js @@ -10,6 +10,7 @@ export default gql` topUsers(cursor: String, when: String, from: String, to: String, by: String, limit: Int): Users topCowboys(cursor: String): Users searchUsers(q: String!, limit: Int, similarity: Float): [User!]! + userSuggestions(q: String, limit: Int): [User!]! hasNewNotes: Boolean! } diff --git a/components/form.js b/components/form.js index 874521b1..e6ee724d 100644 --- a/components/form.js +++ b/components/form.js @@ -15,7 +15,7 @@ import Text from '../components/text' import AddIcon from '../svgs/add-fill.svg' import CloseIcon from '../svgs/close-line.svg' import { gql, useLazyQuery } from '@apollo/client' -import { TOP_USERS, USER_SEARCH } from '../fragments/users' +import { USER_SUGGESTIONS } from '../fragments/users' import TextareaAutosize from 'react-textarea-autosize' import { useToast } from './toast' import { useInvoiceable } from './invoice' @@ -97,7 +97,6 @@ export function InputSkeleton ({ label, hint }) { ) } -const DEFAULT_MENTION_INDICES = { start: -1, end: -1 } export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKeyDown, innerRef, ...props }) { const [tab, setTab] = useState('write') const [, meta, helpers] = useField(props) @@ -108,18 +107,18 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe const { merge, setDisabled: setSubmitDisabled } = useFeeButton() const toaster = useToast() const [updateImageFeesInfo] = useLazyQuery(gql` - query imageFeesInfo($s3Keys: [Int]!) { - imageFeesInfo(s3Keys: $s3Keys) { - totalFees - nUnpaid - imageFee - bytes24h - } - }`, { + query imageFeesInfo($s3Keys: [Int]!) { + imageFeesInfo(s3Keys: $s3Keys) { + totalFees + nUnpaid + imageFee + bytes24h + } + }`, { fetchPolicy: 'no-cache', nextFetchPolicy: 'no-cache', onError: (err) => { - console.log(err) + console.error(err) toaster.danger(err.message || err.toString?.()) }, onCompleted: ({ imageFeesInfo }) => { @@ -157,19 +156,18 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe } }, [innerRef, selectionRange.start, selectionRange.end]) - const [mentionQuery, setMentionQuery] = useState() - const [mentionIndices, setMentionIndices] = useState(DEFAULT_MENTION_INDICES) - const [userSuggestDropdownStyle, setUserSuggestDropdownStyle] = useState({}) + const [mention, setMention] = useState() const insertMention = useCallback((name) => { - const { start, end } = mentionIndices - const first = `${innerRef.current.value.substring(0, start)}@${name}` - const second = innerRef.current.value.substring(end) + if (mention?.start === undefined || mention?.end === undefined) return + const { start, end } = mention + setMention(undefined) + const first = `${meta?.value.substring(0, start)}@${name}` + const second = meta?.value.substring(end) const updatedValue = `${first}${second}` - innerRef.current.value = updatedValue helpers.setValue(updatedValue) setSelectionRange({ start: first.length, end: first.length }) innerRef.current.focus() - }, [mentionIndices, innerRef, helpers?.setValue]) + }, [mention, meta?.value, helpers?.setValue]) const imageFeesUpdate = useDebounceCallback( (text) => { @@ -181,16 +179,23 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe if (onChange) onChange(formik, e) // check for mention editing const { value, selectionStart } = e.target + imageFeesUpdate(value) + + if (!value || selectionStart === undefined) { + setMention(undefined) + return + } + let priorSpace = -1 for (let i = selectionStart - 1; i >= 0; i--) { - if (/\s|\n/.test(value[i])) { + if (/\s/.test(value[i])) { priorSpace = i break } } let nextSpace = value.length for (let i = selectionStart; i <= value.length; i++) { - if (/\s|\n/.test(value[i])) { + if (/\b/.test(value[i])) { nextSpace = i break } @@ -198,22 +203,22 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe const currentSegment = value.substring(priorSpace + 1, nextSpace) // set the query to the current character segment and note where it appears - if (/^@[\w_]*$/.test(currentSegment)) { - setMentionQuery(currentSegment) - setMentionIndices({ start: priorSpace + 1, end: nextSpace }) + if (/^@\w*$/.test(currentSegment)) { const { top, left } = textAreaCaret(e.target, e.target.selectionStart) - setUserSuggestDropdownStyle({ - position: 'absolute', - top: `${top + Number(window.getComputedStyle(e.target).lineHeight.replace('px', ''))}px`, - left: `${left}px` + setMention({ + query: currentSegment, + start: priorSpace + 1, + end: nextSpace, + style: { + position: 'absolute', + top: `${top + Number(window.getComputedStyle(e.target).lineHeight.replace('px', ''))}px`, + left: `${left}px` + } }) } else { - setMentionQuery(undefined) - setMentionIndices(DEFAULT_MENTION_INDICES) + setMention(undefined) } - - imageFeesUpdate(value) - }, [onChange, setMentionQuery, setMentionIndices, setUserSuggestDropdownStyle, imageFeesUpdate]) + }, [onChange, setMention, imageFeesUpdate]) const onKeyDownInner = useCallback((userSuggestOnKeyDown) => { return (e) => { @@ -313,9 +318,9 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe