refine mentions
This commit is contained in:
parent
a74c5201bc
commit
47a5b311c3
|
@ -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)
|
||||
|
|
|
@ -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!
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
|||
</Nav>
|
||||
<div className={`position-relative ${tab === 'write' ? '' : 'd-none'}`}>
|
||||
<UserSuggest
|
||||
query={mentionQuery}
|
||||
query={mention?.query}
|
||||
onSelect={insertMention}
|
||||
dropdownStyle={userSuggestDropdownStyle}
|
||||
dropdownStyle={mention?.userSuggestDropdownStyle}
|
||||
>{({ onKeyDown: userSuggestOnKeyDown, resetSuggestions }) => (
|
||||
<InputInner
|
||||
innerRef={innerRef}
|
||||
|
@ -515,20 +520,10 @@ export function UserSuggest ({
|
|||
query, onSelect, dropdownStyle, children,
|
||||
transformUser = user => user, selectWithTab = true, filterUsers = () => true
|
||||
}) {
|
||||
const [getUsers] = useLazyQuery(TOP_USERS, {
|
||||
const [getSuggestions] = useLazyQuery(USER_SUGGESTIONS, {
|
||||
onCompleted: data => {
|
||||
setSuggestions({
|
||||
array: data.topUsers.users
|
||||
.filter((...args) => filterUsers(query, ...args))
|
||||
.map(transformUser),
|
||||
index: 0
|
||||
})
|
||||
}
|
||||
})
|
||||
const [getSuggestions] = useLazyQuery(USER_SEARCH, {
|
||||
onCompleted: data => {
|
||||
setSuggestions({
|
||||
array: data.searchUsers
|
||||
query !== undefined && setSuggestions({
|
||||
array: data.userSuggestions
|
||||
.filter((...args) => filterUsers(query, ...args))
|
||||
.map(transformUser),
|
||||
index: 0
|
||||
|
@ -543,15 +538,11 @@ export function UserSuggest ({
|
|||
if (query !== undefined) {
|
||||
// remove both the leading @ and any @domain after nym
|
||||
const q = query?.replace(/^[@ ]+|[ ]+$/g, '').replace(/@[^\s]*$/, '')
|
||||
if (q === '') {
|
||||
getUsers({ variables: { by: 'stacked', when: 'week', limit: 5 } })
|
||||
} else {
|
||||
getSuggestions({ variables: { q, limit: 5 } })
|
||||
}
|
||||
getSuggestions({ variables: { q, limit: 5 } })
|
||||
} else {
|
||||
resetSuggestions()
|
||||
}
|
||||
}, [query, resetSuggestions, getUsers, getSuggestions])
|
||||
}, [query, resetSuggestions, getSuggestions])
|
||||
|
||||
const onKeyDown = useCallback(e => {
|
||||
switch (e.code) {
|
||||
|
@ -650,7 +641,7 @@ export function InputUserSuggest ({
|
|||
}}
|
||||
overrideValue={ovalue}
|
||||
onKeyDown={onKeyDown}
|
||||
onBlur={() => setTimeout(resetSuggestions, 100)}
|
||||
onBlur={() => setTimeout(resetSuggestions, 500)}
|
||||
/>
|
||||
)}
|
||||
</UserSuggest>
|
||||
|
@ -791,7 +782,7 @@ export function Form ({
|
|||
clearLocalStorage(values)
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
console.error(err)
|
||||
toaster.danger(err.message || err.toString?.())
|
||||
}
|
||||
}, [onSubmit, feeButton?.total, toaster, clearLocalStorage, storageKeyPrefix])
|
||||
|
|
|
@ -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 =
|
||||
gql`
|
||||
query searchUsers($q: String!, $limit: Int, $similarity: Float) {
|
||||
|
|
|
@ -65,6 +65,12 @@ function getClient (uri) {
|
|||
}
|
||||
}
|
||||
},
|
||||
userSuggestions: {
|
||||
keyArgs: ['q', 'limit'],
|
||||
merge (existing, incoming) {
|
||||
return incoming
|
||||
}
|
||||
},
|
||||
topCowboys: {
|
||||
keyArgs: [],
|
||||
merge (existing, incoming) {
|
||||
|
|
Loading…
Reference in New Issue