user suggestions on forward

This commit is contained in:
keyan 2022-08-26 17:20:09 -05:00
parent 016e357ebd
commit a5d1d8dc0f
10 changed files with 101 additions and 6 deletions

View File

@ -134,6 +134,10 @@ export default {
cursor: users.length === LIMIT ? nextCursorEncoded(decodedCursor) : null, cursor: users.length === LIMIT ? nextCursorEncoded(decodedCursor) : null,
users users
} }
},
searchUsers: async (parent, { name }, { models }) => {
return await models.$queryRaw`
SELECT * FROM users where id > 615 AND SIMILARITY(name, ${name}) > .1 ORDER BY SIMILARITY(name, ${name}) DESC LIMIT 5`
} }
}, },

View File

@ -8,6 +8,7 @@ export default gql`
users: [User!] users: [User!]
nameAvailable(name: String!): Boolean! nameAvailable(name: String!): Boolean!
topUsers(cursor: String, within: String!, userType: String!): TopUsers topUsers(cursor: String, within: String!, userType: String!): TopUsers
searchUsers(name: String!): [User!]!
} }
type Users { type Users {

View File

@ -1,6 +1,6 @@
import AccordianItem from './accordian-item' import AccordianItem from './accordian-item'
import * as Yup from 'yup' import * as Yup from 'yup'
import { Input } from './form' import { Input, InputUserSuggest } from './form'
import { InputGroup } from 'react-bootstrap' import { InputGroup } from 'react-bootstrap'
import { BOOST_MIN } from '../lib/constants' import { BOOST_MIN } from '../lib/constants'
import { NAME_QUERY } from '../fragments/users' import { NAME_QUERY } from '../fragments/users'
@ -69,7 +69,7 @@ export default function AdvPostForm ({ edit }) {
hint={<span className='text-muted'>ranks posts higher temporarily based on the amount</span>} hint={<span className='text-muted'>ranks posts higher temporarily based on the amount</span>}
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>} append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
/> />
<Input <InputUserSuggest
label={<>forward sats to</>} label={<>forward sats to</>}
name='forward' name='forward'
hint={<span className='text-muted'>100% of sats will be sent to this user</span>} hint={<span className='text-muted'>100% of sats will be sent to this user</span>}

View File

@ -41,6 +41,7 @@ export function DiscussionForm ({
initial={{ initial={{
title: item?.title || '', title: item?.title || '',
text: item?.text || '', text: item?.text || '',
suggest: '',
...AdvPostInitial({ forward: item?.fwdUser?.name }) ...AdvPostInitial({ forward: item?.fwdUser?.name })
}} }}
schema={DiscussionSchema} schema={DiscussionSchema}

View File

@ -6,13 +6,15 @@ import { Formik, Form as FormikForm, useFormikContext, useField, FieldArray } fr
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import copy from 'clipboard-copy' import copy from 'clipboard-copy'
import Thumb from '../svgs/thumb-up-fill.svg' import Thumb from '../svgs/thumb-up-fill.svg'
import { Col, Nav } from 'react-bootstrap' import { Col, Dropdown, Nav } from 'react-bootstrap'
import Markdown from '../svgs/markdown-line.svg' import Markdown from '../svgs/markdown-line.svg'
import styles from './form.module.css' import styles from './form.module.css'
import Text from '../components/text' import Text from '../components/text'
import AddIcon from '../svgs/add-fill.svg' import AddIcon from '../svgs/add-fill.svg'
import { mdHas } from '../lib/md' import { mdHas } from '../lib/md'
import CloseIcon from '../svgs/close-line.svg' import CloseIcon from '../svgs/close-line.svg'
import { useLazyQuery } from '@apollo/client'
import { USER_SEARCH } from '../fragments/users'
export function SubmitButton ({ export function SubmitButton ({
children, variant, value, onClick, ...props children, variant, value, onClick, ...props
@ -130,7 +132,7 @@ function FormGroup ({ className, label, children }) {
function InputInner ({ function InputInner ({
prepend, append, hint, showValid, onChange, overrideValue, prepend, append, hint, showValid, onChange, overrideValue,
innerRef, storageKeyPrefix, noForm, clear, ...props innerRef, storageKeyPrefix, noForm, clear, onKeyDown, ...props
}) { }) {
const [field, meta, helpers] = noForm ? [{}, {}, {}] : useField(props) const [field, meta, helpers] = noForm ? [{}, {}, {}] : useField(props)
const formik = noForm ? null : useFormikContext() const formik = noForm ? null : useFormikContext()
@ -168,6 +170,7 @@ function InputInner ({
if (e.keyCode === 13 && (e.metaKey || e.ctrlKey)) { if (e.keyCode === 13 && (e.metaKey || e.ctrlKey)) {
formik?.submitForm() formik?.submitForm()
} }
if (onKeyDown) onKeyDown(e)
}} }}
ref={innerRef} ref={innerRef}
{...field} {...props} {...field} {...props}
@ -215,6 +218,76 @@ function InputInner ({
) )
} }
export function InputUserSuggest ({ label, groupClassName, ...props }) {
const [getSuggestions] = useLazyQuery(USER_SEARCH, {
fetchPolicy: 'network-only',
onCompleted: data => {
setSuggestions({ array: data.searchUsers, index: 0 })
}
})
const INITIAL_SUGGESTIONS = { array: [], index: 0 }
const [suggestions, setSuggestions] = useState(INITIAL_SUGGESTIONS)
const [ovalue, setOValue] = useState()
return (
<FormGroup label={label} className={groupClassName}>
<InputInner
{...props}
autoComplete='off'
onChange={(_, e) => getSuggestions({ variables: { name: e.target.value } })}
overrideValue={ovalue}
onKeyDown={(e) => {
switch (e.code) {
case 'ArrowUp':
e.preventDefault()
setSuggestions(
{
...suggestions,
index: Math.max(suggestions.index - 1, 0)
})
break
case 'ArrowDown':
e.preventDefault()
setSuggestions(
{
...suggestions,
index: Math.min(suggestions.index + 1, suggestions.array.length - 1)
})
break
case 'Enter':
e.preventDefault()
setOValue(suggestions.array[suggestions.index].name)
setSuggestions(INITIAL_SUGGESTIONS)
break
case 'Escape':
e.preventDefault()
setSuggestions(INITIAL_SUGGESTIONS)
break
default:
break
}
}}
/>
<Dropdown show={suggestions.array.length > 0}>
<Dropdown.Menu className={styles.suggestionsMenu}>
{suggestions.array.map((v, i) =>
<Dropdown.Item
key={v.name}
active={suggestions.index === i}
onClick={() => {
setOValue(v.name)
setSuggestions(INITIAL_SUGGESTIONS)
}}
>
{v.name}
</Dropdown.Item>)}
</Dropdown.Menu>
</Dropdown>
</FormGroup>
)
}
export function Input ({ label, groupClassName, ...props }) { export function Input ({ label, groupClassName, ...props }) {
return ( return (
<FormGroup label={label} className={groupClassName}> <FormGroup label={label} className={groupClassName}>

View File

@ -22,4 +22,10 @@
.clearButton.isInvalid { .clearButton.isInvalid {
border-color: #c03221; border-color: #c03221;
}
/* https://github.com/react-bootstrap/react-bootstrap/issues/5475 */
.suggestionsMenu {
opacity: 1 !important;
pointer-events: unset !important;
} }

View File

@ -50,7 +50,8 @@ export default function Search ({ sub }) {
required required
autoFocus={showSearch && !atBottom} autoFocus={showSearch && !atBottom}
groupClassName='mr-3 mb-0 flex-grow-1' groupClassName='mr-3 mb-0 flex-grow-1'
className='w-100' className='flex-grow-1'
clear
onChange={async (formik, e) => { onChange={async (formik, e) => {
setSearching(true) setSearching(true)
setQ(e.target.value?.trim()) setQ(e.target.value?.trim())

View File

@ -89,6 +89,14 @@ gql`
} }
` `
export const USER_SEARCH =
gql`
query searchUsers($name: String!) {
searchUsers(name: $name) {
name
}
}`
export const USER_FIELDS = gql` export const USER_FIELDS = gql`
${ITEM_FIELDS} ${ITEM_FIELDS}
fragment UserFields on User { fragment UserFields on User {

View File

@ -87,4 +87,4 @@
"eslint-plugin-compat": "^3.9.0", "eslint-plugin-compat": "^3.9.0",
"standard": "^16.0.3" "standard": "^16.0.3"
} }
} }

View File

@ -0,0 +1 @@
CREATE EXTENSION pg_trgm;