refine mentions

This commit is contained in:
keyan 2023-11-21 14:49:39 -06:00
parent a74c5201bc
commit 47a5b311c3
5 changed files with 87 additions and 56 deletions

View File

@ -99,6 +99,31 @@ export default {
users 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 }) => { topUsers: async (parent, { cursor, when, by, from, to, limit = LIMIT }, { models, me }) => {
const decodedCursor = decodeCursor(cursor) const decodedCursor = decodeCursor(cursor)
const range = whenRange(when, from, to || decodeCursor.time) const range = whenRange(when, from, to || decodeCursor.time)

View File

@ -10,6 +10,7 @@ export default gql`
topUsers(cursor: String, when: String, from: String, to: String, by: String, limit: Int): Users topUsers(cursor: String, when: String, from: String, to: String, by: String, limit: Int): Users
topCowboys(cursor: String): Users topCowboys(cursor: String): Users
searchUsers(q: String!, limit: Int, similarity: Float): [User!]! searchUsers(q: String!, limit: Int, similarity: Float): [User!]!
userSuggestions(q: String, limit: Int): [User!]!
hasNewNotes: Boolean! hasNewNotes: Boolean!
} }

View File

@ -15,7 +15,7 @@ import Text from '../components/text'
import AddIcon from '../svgs/add-fill.svg' import AddIcon from '../svgs/add-fill.svg'
import CloseIcon from '../svgs/close-line.svg' import CloseIcon from '../svgs/close-line.svg'
import { gql, useLazyQuery } from '@apollo/client' 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 TextareaAutosize from 'react-textarea-autosize'
import { useToast } from './toast' import { useToast } from './toast'
import { useInvoiceable } from './invoice' 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 }) { export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKeyDown, innerRef, ...props }) {
const [tab, setTab] = useState('write') const [tab, setTab] = useState('write')
const [, meta, helpers] = useField(props) const [, meta, helpers] = useField(props)
@ -108,18 +107,18 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
const { merge, setDisabled: setSubmitDisabled } = useFeeButton() const { merge, setDisabled: setSubmitDisabled } = useFeeButton()
const toaster = useToast() const toaster = useToast()
const [updateImageFeesInfo] = useLazyQuery(gql` const [updateImageFeesInfo] = useLazyQuery(gql`
query imageFeesInfo($s3Keys: [Int]!) { query imageFeesInfo($s3Keys: [Int]!) {
imageFeesInfo(s3Keys: $s3Keys) { imageFeesInfo(s3Keys: $s3Keys) {
totalFees totalFees
nUnpaid nUnpaid
imageFee imageFee
bytes24h bytes24h
} }
}`, { }`, {
fetchPolicy: 'no-cache', fetchPolicy: 'no-cache',
nextFetchPolicy: 'no-cache', nextFetchPolicy: 'no-cache',
onError: (err) => { onError: (err) => {
console.log(err) console.error(err)
toaster.danger(err.message || err.toString?.()) toaster.danger(err.message || err.toString?.())
}, },
onCompleted: ({ imageFeesInfo }) => { onCompleted: ({ imageFeesInfo }) => {
@ -157,19 +156,18 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
} }
}, [innerRef, selectionRange.start, selectionRange.end]) }, [innerRef, selectionRange.start, selectionRange.end])
const [mentionQuery, setMentionQuery] = useState() const [mention, setMention] = useState()
const [mentionIndices, setMentionIndices] = useState(DEFAULT_MENTION_INDICES)
const [userSuggestDropdownStyle, setUserSuggestDropdownStyle] = useState({})
const insertMention = useCallback((name) => { const insertMention = useCallback((name) => {
const { start, end } = mentionIndices if (mention?.start === undefined || mention?.end === undefined) return
const first = `${innerRef.current.value.substring(0, start)}@${name}` const { start, end } = mention
const second = innerRef.current.value.substring(end) setMention(undefined)
const first = `${meta?.value.substring(0, start)}@${name}`
const second = meta?.value.substring(end)
const updatedValue = `${first}${second}` const updatedValue = `${first}${second}`
innerRef.current.value = updatedValue
helpers.setValue(updatedValue) helpers.setValue(updatedValue)
setSelectionRange({ start: first.length, end: first.length }) setSelectionRange({ start: first.length, end: first.length })
innerRef.current.focus() innerRef.current.focus()
}, [mentionIndices, innerRef, helpers?.setValue]) }, [mention, meta?.value, helpers?.setValue])
const imageFeesUpdate = useDebounceCallback( const imageFeesUpdate = useDebounceCallback(
(text) => { (text) => {
@ -181,16 +179,23 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
if (onChange) onChange(formik, e) if (onChange) onChange(formik, e)
// check for mention editing // check for mention editing
const { value, selectionStart } = e.target const { value, selectionStart } = e.target
imageFeesUpdate(value)
if (!value || selectionStart === undefined) {
setMention(undefined)
return
}
let priorSpace = -1 let priorSpace = -1
for (let i = selectionStart - 1; i >= 0; i--) { for (let i = selectionStart - 1; i >= 0; i--) {
if (/\s|\n/.test(value[i])) { if (/\s/.test(value[i])) {
priorSpace = i priorSpace = i
break break
} }
} }
let nextSpace = value.length let nextSpace = value.length
for (let i = selectionStart; i <= value.length; i++) { for (let i = selectionStart; i <= value.length; i++) {
if (/\s|\n/.test(value[i])) { if (/\b/.test(value[i])) {
nextSpace = i nextSpace = i
break break
} }
@ -198,22 +203,22 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
const currentSegment = value.substring(priorSpace + 1, nextSpace) const currentSegment = value.substring(priorSpace + 1, nextSpace)
// set the query to the current character segment and note where it appears // set the query to the current character segment and note where it appears
if (/^@[\w_]*$/.test(currentSegment)) { if (/^@\w*$/.test(currentSegment)) {
setMentionQuery(currentSegment)
setMentionIndices({ start: priorSpace + 1, end: nextSpace })
const { top, left } = textAreaCaret(e.target, e.target.selectionStart) const { top, left } = textAreaCaret(e.target, e.target.selectionStart)
setUserSuggestDropdownStyle({ setMention({
position: 'absolute', query: currentSegment,
top: `${top + Number(window.getComputedStyle(e.target).lineHeight.replace('px', ''))}px`, start: priorSpace + 1,
left: `${left}px` end: nextSpace,
style: {
position: 'absolute',
top: `${top + Number(window.getComputedStyle(e.target).lineHeight.replace('px', ''))}px`,
left: `${left}px`
}
}) })
} else { } else {
setMentionQuery(undefined) setMention(undefined)
setMentionIndices(DEFAULT_MENTION_INDICES)
} }
}, [onChange, setMention, imageFeesUpdate])
imageFeesUpdate(value)
}, [onChange, setMentionQuery, setMentionIndices, setUserSuggestDropdownStyle, imageFeesUpdate])
const onKeyDownInner = useCallback((userSuggestOnKeyDown) => { const onKeyDownInner = useCallback((userSuggestOnKeyDown) => {
return (e) => { return (e) => {
@ -313,9 +318,9 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
</Nav> </Nav>
<div className={`position-relative ${tab === 'write' ? '' : 'd-none'}`}> <div className={`position-relative ${tab === 'write' ? '' : 'd-none'}`}>
<UserSuggest <UserSuggest
query={mentionQuery} query={mention?.query}
onSelect={insertMention} onSelect={insertMention}
dropdownStyle={userSuggestDropdownStyle} dropdownStyle={mention?.userSuggestDropdownStyle}
>{({ onKeyDown: userSuggestOnKeyDown, resetSuggestions }) => ( >{({ onKeyDown: userSuggestOnKeyDown, resetSuggestions }) => (
<InputInner <InputInner
innerRef={innerRef} innerRef={innerRef}
@ -515,20 +520,10 @@ export function UserSuggest ({
query, onSelect, dropdownStyle, children, query, onSelect, dropdownStyle, children,
transformUser = user => user, selectWithTab = true, filterUsers = () => true transformUser = user => user, selectWithTab = true, filterUsers = () => true
}) { }) {
const [getUsers] = useLazyQuery(TOP_USERS, { const [getSuggestions] = useLazyQuery(USER_SUGGESTIONS, {
onCompleted: data => { onCompleted: data => {
setSuggestions({ query !== undefined && setSuggestions({
array: data.topUsers.users array: data.userSuggestions
.filter((...args) => filterUsers(query, ...args))
.map(transformUser),
index: 0
})
}
})
const [getSuggestions] = useLazyQuery(USER_SEARCH, {
onCompleted: data => {
setSuggestions({
array: data.searchUsers
.filter((...args) => filterUsers(query, ...args)) .filter((...args) => filterUsers(query, ...args))
.map(transformUser), .map(transformUser),
index: 0 index: 0
@ -543,15 +538,11 @@ export function UserSuggest ({
if (query !== undefined) { if (query !== undefined) {
// remove both the leading @ and any @domain after nym // remove both the leading @ and any @domain after nym
const q = query?.replace(/^[@ ]+|[ ]+$/g, '').replace(/@[^\s]*$/, '') const q = query?.replace(/^[@ ]+|[ ]+$/g, '').replace(/@[^\s]*$/, '')
if (q === '') { getSuggestions({ variables: { q, limit: 5 } })
getUsers({ variables: { by: 'stacked', when: 'week', limit: 5 } })
} else {
getSuggestions({ variables: { q, limit: 5 } })
}
} else { } else {
resetSuggestions() resetSuggestions()
} }
}, [query, resetSuggestions, getUsers, getSuggestions]) }, [query, resetSuggestions, getSuggestions])
const onKeyDown = useCallback(e => { const onKeyDown = useCallback(e => {
switch (e.code) { switch (e.code) {
@ -650,7 +641,7 @@ export function InputUserSuggest ({
}} }}
overrideValue={ovalue} overrideValue={ovalue}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
onBlur={() => setTimeout(resetSuggestions, 100)} onBlur={() => setTimeout(resetSuggestions, 500)}
/> />
)} )}
</UserSuggest> </UserSuggest>
@ -791,7 +782,7 @@ export function Form ({
clearLocalStorage(values) clearLocalStorage(values)
} }
} catch (err) { } catch (err) {
console.log(err) console.error(err)
toaster.danger(err.message || err.toString?.()) toaster.danger(err.message || err.toString?.())
} }
}, [onSubmit, feeButton?.total, toaster, clearLocalStorage, storageKeyPrefix]) }, [onSubmit, feeButton?.total, toaster, clearLocalStorage, storageKeyPrefix])

View File

@ -126,6 +126,14 @@ gql`
} }
` `
export const USER_SUGGESTIONS =
gql`
query userSuggestions($q: String!, $limit: Int) {
userSuggestions(q: $q, limit: $limit) {
name
}
}`
export const USER_SEARCH = export const USER_SEARCH =
gql` gql`
query searchUsers($q: String!, $limit: Int, $similarity: Float) { query searchUsers($q: String!, $limit: Int, $similarity: Float) {

View File

@ -65,6 +65,12 @@ function getClient (uri) {
} }
} }
}, },
userSuggestions: {
keyArgs: ['q', 'limit'],
merge (existing, incoming) {
return incoming
}
},
topCowboys: { topCowboys: {
keyArgs: [], keyArgs: [],
merge (existing, incoming) { merge (existing, incoming) {