refine mentions
This commit is contained in:
parent
a74c5201bc
commit
47a5b311c3
|
@ -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)
|
||||||
|
|
|
@ -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!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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])
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in New Issue