2021-04-14 00:57:32 +00:00
|
|
|
import Button from 'react-bootstrap/Button'
|
|
|
|
import InputGroup from 'react-bootstrap/InputGroup'
|
|
|
|
import BootstrapForm from 'react-bootstrap/Form'
|
2022-07-30 13:25:46 +00:00
|
|
|
import { Formik, Form as FormikForm, useFormikContext, useField, FieldArray } from 'formik'
|
2023-11-09 00:15:36 +00:00
|
|
|
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
2021-05-13 13:28:38 +00:00
|
|
|
import copy from 'clipboard-copy'
|
2023-07-24 18:35:05 +00:00
|
|
|
import Col from 'react-bootstrap/Col'
|
|
|
|
import Dropdown from 'react-bootstrap/Dropdown'
|
|
|
|
import Nav from 'react-bootstrap/Nav'
|
|
|
|
import Row from 'react-bootstrap/Row'
|
2024-03-20 00:37:31 +00:00
|
|
|
import Markdown from '@/svgs/markdown-line.svg'
|
2024-09-13 14:26:08 +00:00
|
|
|
import AddFileIcon from '@/svgs/file-upload-line.svg'
|
2021-07-01 23:51:58 +00:00
|
|
|
import styles from './form.module.css'
|
2024-03-20 00:37:31 +00:00
|
|
|
import Text from '@/components/text'
|
|
|
|
import AddIcon from '@/svgs/add-fill.svg'
|
|
|
|
import CloseIcon from '@/svgs/close-line.svg'
|
2023-11-06 20:53:33 +00:00
|
|
|
import { gql, useLazyQuery } from '@apollo/client'
|
2024-03-20 00:37:31 +00:00
|
|
|
import { USER_SUGGESTIONS } from '@/fragments/users'
|
2023-07-23 15:08:43 +00:00
|
|
|
import TextareaAutosize from 'react-textarea-autosize'
|
2023-08-25 23:21:51 +00:00
|
|
|
import { useToast } from './toast'
|
2024-03-20 00:37:31 +00:00
|
|
|
import { numWithUnits } from '@/lib/format'
|
2023-10-04 20:10:56 +00:00
|
|
|
import textAreaCaret from 'textarea-caret'
|
2023-10-04 19:44:06 +00:00
|
|
|
import ReactDatePicker from 'react-datepicker'
|
|
|
|
import 'react-datepicker/dist/react-datepicker.css'
|
2023-11-11 00:18:10 +00:00
|
|
|
import useDebounceCallback, { debounce } from './use-debounce-callback'
|
2024-09-13 14:26:08 +00:00
|
|
|
import { FileUpload } from './file-upload'
|
2024-03-20 00:37:31 +00:00
|
|
|
import { AWS_S3_URL_REGEXP } from '@/lib/constants'
|
|
|
|
import { whenRange } from '@/lib/time'
|
2023-11-11 00:18:10 +00:00
|
|
|
import { useFeeButton } from './fee-button'
|
2024-03-20 00:37:31 +00:00
|
|
|
import Thumb from '@/svgs/thumb-up-fill.svg'
|
2024-04-21 22:28:57 +00:00
|
|
|
import Eye from '@/svgs/eye-fill.svg'
|
|
|
|
import EyeClose from '@/svgs/eye-close-line.svg'
|
2024-01-09 01:02:00 +00:00
|
|
|
import Info from './info'
|
2024-05-28 17:18:54 +00:00
|
|
|
import { useMe } from './me'
|
2024-07-01 17:02:29 +00:00
|
|
|
import classNames from 'classnames'
|
2024-05-28 17:18:54 +00:00
|
|
|
|
|
|
|
export class SessionRequiredError extends Error {
|
|
|
|
constructor () {
|
|
|
|
super('session required')
|
|
|
|
this.name = 'SessionRequiredError'
|
|
|
|
}
|
|
|
|
}
|
2021-04-14 00:57:32 +00:00
|
|
|
|
2022-02-17 17:23:43 +00:00
|
|
|
export function SubmitButton ({
|
2024-09-19 18:13:14 +00:00
|
|
|
children, variant, valueName = 'submit', value, onClick, disabled, appendText, submittingText,
|
2024-07-01 17:02:29 +00:00
|
|
|
className, ...props
|
2022-02-17 17:23:43 +00:00
|
|
|
}) {
|
2023-08-31 02:48:49 +00:00
|
|
|
const formik = useFormikContext()
|
|
|
|
|
2023-12-10 21:41:20 +00:00
|
|
|
disabled ||= formik.isSubmitting
|
2024-07-01 17:02:29 +00:00
|
|
|
submittingText ||= children
|
2023-12-10 21:41:20 +00:00
|
|
|
|
2021-04-14 00:57:32 +00:00
|
|
|
return (
|
|
|
|
<Button
|
|
|
|
variant={variant || 'main'}
|
2024-09-25 18:32:52 +00:00
|
|
|
className={classNames(formik.isSubmitting && 'pulse', className)}
|
2021-04-14 00:57:32 +00:00
|
|
|
type='submit'
|
2023-12-10 21:41:20 +00:00
|
|
|
disabled={disabled}
|
2021-09-10 18:55:36 +00:00
|
|
|
onClick={value
|
|
|
|
? e => {
|
2024-09-19 18:13:14 +00:00
|
|
|
formik.setFieldValue(valueName, value)
|
2023-07-25 14:14:45 +00:00
|
|
|
onClick && onClick(e)
|
|
|
|
}
|
2021-09-10 18:55:36 +00:00
|
|
|
: onClick}
|
2021-04-14 00:57:32 +00:00
|
|
|
{...props}
|
|
|
|
>
|
2024-07-01 17:02:29 +00:00
|
|
|
{formik.isSubmitting ? submittingText : children}{!disabled && appendText && <small> {appendText}</small>}
|
2021-04-14 00:57:32 +00:00
|
|
|
</Button>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2024-10-04 20:00:13 +00:00
|
|
|
export function CopyInput (props) {
|
2023-10-21 00:25:22 +00:00
|
|
|
const toaster = useToast()
|
2023-11-11 00:18:10 +00:00
|
|
|
const [copied, setCopied] = useState(false)
|
2021-05-13 13:28:38 +00:00
|
|
|
|
2024-10-04 20:00:13 +00:00
|
|
|
const handleClick = async () => {
|
2023-10-21 00:25:22 +00:00
|
|
|
try {
|
2024-10-04 20:00:13 +00:00
|
|
|
await copy(props.placeholder)
|
2023-10-21 00:25:22 +00:00
|
|
|
toaster.success('copied')
|
2023-11-11 00:18:10 +00:00
|
|
|
setCopied(true)
|
|
|
|
setTimeout(() => setCopied(false), 1500)
|
2023-10-21 00:25:22 +00:00
|
|
|
} catch (err) {
|
|
|
|
toaster.danger('failed to copy')
|
|
|
|
}
|
2021-05-13 13:28:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Input
|
2024-10-04 20:00:13 +00:00
|
|
|
onClick={handleClick}
|
2021-06-02 23:15:28 +00:00
|
|
|
append={
|
2024-10-04 20:00:13 +00:00
|
|
|
<Button
|
|
|
|
className={styles.appendButton}
|
|
|
|
size={props.size}
|
|
|
|
onClick={handleClick}
|
|
|
|
>{copied ? <Thumb width={18} height={18} /> : 'copy'}
|
|
|
|
</Button>
|
2023-10-21 00:25:22 +00:00
|
|
|
}
|
2021-05-13 13:28:38 +00:00
|
|
|
{...props}
|
|
|
|
/>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-01-23 17:21:55 +00:00
|
|
|
export function InputSkeleton ({ label, hint }) {
|
2021-05-13 13:28:38 +00:00
|
|
|
return (
|
|
|
|
<BootstrapForm.Group>
|
|
|
|
{label && <BootstrapForm.Label>{label}</BootstrapForm.Label>}
|
2023-07-31 17:35:58 +00:00
|
|
|
<div className='form-control clouds' style={{ color: 'transparent' }}>.</div>
|
2022-01-23 17:21:55 +00:00
|
|
|
{hint &&
|
|
|
|
<BootstrapForm.Text>
|
|
|
|
{hint}
|
|
|
|
</BootstrapForm.Text>}
|
2021-05-13 13:28:38 +00:00
|
|
|
</BootstrapForm.Group>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-11-06 20:53:33 +00:00
|
|
|
export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKeyDown, innerRef, ...props }) {
|
2021-07-01 23:51:58 +00:00
|
|
|
const [tab, setTab] = useState('write')
|
2023-06-12 22:39:20 +00:00
|
|
|
const [, meta, helpers] = useField(props)
|
|
|
|
const [selectionRange, setSelectionRange] = useState({ start: 0, end: 0 })
|
|
|
|
innerRef = innerRef || useRef(null)
|
2023-11-06 20:53:33 +00:00
|
|
|
const imageUploadRef = useRef(null)
|
2023-10-04 01:12:12 +00:00
|
|
|
const previousTab = useRef(tab)
|
2023-11-13 15:30:01 +00:00
|
|
|
const { merge, setDisabled: setSubmitDisabled } = useFeeButton()
|
2024-06-11 16:22:20 +00:00
|
|
|
|
2024-09-13 14:26:08 +00:00
|
|
|
const [updateUploadFees] = useLazyQuery(gql`
|
|
|
|
query uploadFees($s3Keys: [Int]!) {
|
|
|
|
uploadFees(s3Keys: $s3Keys) {
|
2023-11-21 20:49:39 +00:00
|
|
|
totalFees
|
|
|
|
nUnpaid
|
2024-09-13 14:26:08 +00:00
|
|
|
uploadFees
|
2023-11-21 20:49:39 +00:00
|
|
|
bytes24h
|
|
|
|
}
|
|
|
|
}`, {
|
2023-11-06 20:53:33 +00:00
|
|
|
fetchPolicy: 'no-cache',
|
|
|
|
nextFetchPolicy: 'no-cache',
|
|
|
|
onError: (err) => {
|
2023-11-21 20:49:39 +00:00
|
|
|
console.error(err)
|
2023-11-06 20:53:33 +00:00
|
|
|
},
|
2024-09-13 14:26:08 +00:00
|
|
|
onCompleted: ({ uploadFees }) => {
|
2023-11-11 00:18:10 +00:00
|
|
|
merge({
|
2024-09-13 14:26:08 +00:00
|
|
|
uploadFees: {
|
|
|
|
term: `+ ${numWithUnits(uploadFees.totalFees, { abbreviate: false })}`,
|
|
|
|
label: 'upload fee',
|
2024-09-19 18:13:14 +00:00
|
|
|
op: '+',
|
2024-09-13 14:26:08 +00:00
|
|
|
modifier: cost => cost + uploadFees.totalFees,
|
|
|
|
omit: !uploadFees.totalFees
|
2023-11-11 00:18:10 +00:00
|
|
|
}
|
|
|
|
})
|
2023-11-06 20:53:33 +00:00
|
|
|
}
|
|
|
|
})
|
2021-07-01 23:51:58 +00:00
|
|
|
|
2023-07-23 15:08:43 +00:00
|
|
|
props.as ||= TextareaAutosize
|
|
|
|
props.rows ||= props.minRows || 6
|
|
|
|
|
2021-07-01 23:51:58 +00:00
|
|
|
useEffect(() => {
|
|
|
|
!meta.value && setTab('write')
|
|
|
|
}, [meta.value])
|
|
|
|
|
2023-10-04 01:12:12 +00:00
|
|
|
useEffect(() => {
|
|
|
|
// focus on input when switching to write tab from preview tab
|
|
|
|
if (innerRef?.current && tab === 'write' && previousTab?.current !== 'write') {
|
|
|
|
innerRef.current.focus()
|
|
|
|
}
|
|
|
|
previousTab.current = tab
|
|
|
|
}, [tab])
|
|
|
|
|
2023-06-12 22:39:20 +00:00
|
|
|
useEffect(() => {
|
|
|
|
if (selectionRange.start <= selectionRange.end && innerRef?.current) {
|
|
|
|
const { start, end } = selectionRange
|
|
|
|
const input = innerRef.current
|
|
|
|
input.setSelectionRange(start, end)
|
|
|
|
}
|
2023-06-13 14:19:50 +00:00
|
|
|
}, [innerRef, selectionRange.start, selectionRange.end])
|
2023-06-12 22:39:20 +00:00
|
|
|
|
2023-11-21 20:49:39 +00:00
|
|
|
const [mention, setMention] = useState()
|
2023-10-04 20:10:56 +00:00
|
|
|
const insertMention = useCallback((name) => {
|
2023-11-21 20:49:39 +00:00
|
|
|
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)
|
2023-10-04 20:10:56 +00:00
|
|
|
const updatedValue = `${first}${second}`
|
|
|
|
helpers.setValue(updatedValue)
|
|
|
|
setSelectionRange({ start: first.length, end: first.length })
|
|
|
|
innerRef.current.focus()
|
2023-11-21 20:49:39 +00:00
|
|
|
}, [mention, meta?.value, helpers?.setValue])
|
2023-10-12 17:46:22 +00:00
|
|
|
|
2024-09-13 14:26:08 +00:00
|
|
|
const uploadFeesUpdate = useDebounceCallback(
|
2023-11-06 20:53:33 +00:00
|
|
|
(text) => {
|
|
|
|
const s3Keys = text ? [...text.matchAll(AWS_S3_URL_REGEXP)].map(m => Number(m[1])) : []
|
2024-09-13 14:26:08 +00:00
|
|
|
updateUploadFees({ variables: { s3Keys } })
|
|
|
|
}, 1000, [updateUploadFees])
|
2023-11-06 20:53:33 +00:00
|
|
|
|
2023-10-12 17:46:22 +00:00
|
|
|
const onChangeInner = useCallback((formik, e) => {
|
|
|
|
if (onChange) onChange(formik, e)
|
|
|
|
// check for mention editing
|
|
|
|
const { value, selectionStart } = e.target
|
2024-09-13 14:26:08 +00:00
|
|
|
uploadFeesUpdate(value)
|
2023-11-21 20:49:39 +00:00
|
|
|
|
|
|
|
if (!value || selectionStart === undefined) {
|
|
|
|
setMention(undefined)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-10-12 17:46:22 +00:00
|
|
|
let priorSpace = -1
|
|
|
|
for (let i = selectionStart - 1; i >= 0; i--) {
|
2023-12-03 18:57:59 +00:00
|
|
|
if (/[^\w@]/.test(value[i])) {
|
2023-10-12 17:46:22 +00:00
|
|
|
priorSpace = i
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
let nextSpace = value.length
|
|
|
|
for (let i = selectionStart; i <= value.length; i++) {
|
2023-12-03 18:57:59 +00:00
|
|
|
if (/[^\w]/.test(value[i])) {
|
2023-10-12 17:46:22 +00:00
|
|
|
nextSpace = i
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const currentSegment = value.substring(priorSpace + 1, nextSpace)
|
|
|
|
|
|
|
|
// set the query to the current character segment and note where it appears
|
2023-11-21 20:49:39 +00:00
|
|
|
if (/^@\w*$/.test(currentSegment)) {
|
2023-10-12 17:46:22 +00:00
|
|
|
const { top, left } = textAreaCaret(e.target, e.target.selectionStart)
|
2023-11-21 20:49:39 +00:00
|
|
|
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`
|
|
|
|
}
|
2023-10-12 17:46:22 +00:00
|
|
|
})
|
|
|
|
} else {
|
2023-11-21 20:49:39 +00:00
|
|
|
setMention(undefined)
|
2023-10-12 17:46:22 +00:00
|
|
|
}
|
2024-09-13 14:26:08 +00:00
|
|
|
}, [onChange, setMention, uploadFeesUpdate])
|
2023-10-12 17:46:22 +00:00
|
|
|
|
|
|
|
const onKeyDownInner = useCallback((userSuggestOnKeyDown) => {
|
|
|
|
return (e) => {
|
|
|
|
const metaOrCtrl = e.metaKey || e.ctrlKey
|
|
|
|
if (metaOrCtrl) {
|
|
|
|
if (e.key === 'k') {
|
|
|
|
// some browsers use CTRL+K to focus search bar so we have to prevent that behavior
|
|
|
|
e.preventDefault()
|
|
|
|
insertMarkdownLinkFormatting(innerRef.current, helpers.setValue, setSelectionRange)
|
|
|
|
}
|
|
|
|
if (e.key === 'b') {
|
|
|
|
// some browsers use CTRL+B to open bookmarks so we have to prevent that behavior
|
|
|
|
e.preventDefault()
|
|
|
|
insertMarkdownBoldFormatting(innerRef.current, helpers.setValue, setSelectionRange)
|
|
|
|
}
|
|
|
|
if (e.key === 'i') {
|
|
|
|
// some browsers might use CTRL+I to do something else so prevent that behavior too
|
|
|
|
e.preventDefault()
|
|
|
|
insertMarkdownItalicFormatting(innerRef.current, helpers.setValue, setSelectionRange)
|
|
|
|
}
|
2024-09-24 01:08:37 +00:00
|
|
|
if (e.key === 'u') {
|
|
|
|
// some browsers might use CTRL+U to do something else so prevent that behavior too
|
|
|
|
e.preventDefault()
|
|
|
|
imageUploadRef.current?.click()
|
|
|
|
}
|
2023-10-12 17:46:22 +00:00
|
|
|
if (e.key === 'Tab' && e.altKey) {
|
|
|
|
e.preventDefault()
|
|
|
|
insertMarkdownTabFormatting(innerRef.current, helpers.setValue, setSelectionRange)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!metaOrCtrl) {
|
|
|
|
userSuggestOnKeyDown(e)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (onKeyDown) onKeyDown(e)
|
|
|
|
}
|
|
|
|
}, [innerRef, helpers?.setValue, setSelectionRange, onKeyDown])
|
2023-10-04 20:10:56 +00:00
|
|
|
|
2024-04-08 21:31:02 +00:00
|
|
|
const onPaste = useCallback((event) => {
|
|
|
|
const items = event.clipboardData.items
|
|
|
|
if (items.length === 0) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
let isImagePasted = false
|
|
|
|
const fileList = new window.DataTransfer()
|
|
|
|
for (let i = 0; i < items.length; i++) {
|
|
|
|
const item = items[i]
|
|
|
|
if (item.type.indexOf('image') === 0) {
|
|
|
|
const blob = item.getAsFile()
|
|
|
|
const file = new File([blob], 'image', { type: blob.type })
|
|
|
|
fileList.items.add(file)
|
|
|
|
isImagePasted = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isImagePasted) {
|
|
|
|
event.preventDefault()
|
|
|
|
const changeEvent = new Event('change', { bubbles: true })
|
|
|
|
imageUploadRef.current.files = fileList.files
|
|
|
|
imageUploadRef.current.dispatchEvent(changeEvent)
|
|
|
|
}
|
|
|
|
}, [imageUploadRef])
|
|
|
|
|
2023-11-06 20:53:33 +00:00
|
|
|
const onDrop = useCallback((event) => {
|
|
|
|
event.preventDefault()
|
|
|
|
setDragStyle(null)
|
|
|
|
const changeEvent = new Event('change', { bubbles: true })
|
|
|
|
imageUploadRef.current.files = event.dataTransfer.files
|
|
|
|
imageUploadRef.current.dispatchEvent(changeEvent)
|
|
|
|
}, [imageUploadRef])
|
|
|
|
|
|
|
|
const [dragStyle, setDragStyle] = useState(null)
|
|
|
|
const onDragEnter = useCallback((e) => {
|
|
|
|
setDragStyle('over')
|
|
|
|
}, [setDragStyle])
|
|
|
|
const onDragLeave = useCallback((e) => {
|
|
|
|
setDragStyle(null)
|
|
|
|
}, [setDragStyle])
|
|
|
|
|
2021-07-01 23:51:58 +00:00
|
|
|
return (
|
|
|
|
<FormGroup label={label} className={groupClassName}>
|
|
|
|
<div className={`${styles.markdownInput} ${tab === 'write' ? styles.noTopLeftRadius : ''}`}>
|
|
|
|
<Nav variant='tabs' defaultActiveKey='write' activeKey={tab} onSelect={tab => setTab(tab)}>
|
|
|
|
<Nav.Item>
|
2023-10-26 18:28:12 +00:00
|
|
|
<Nav.Link className='py-1' eventKey='write'>write</Nav.Link>
|
2021-07-01 23:51:58 +00:00
|
|
|
</Nav.Item>
|
|
|
|
<Nav.Item>
|
2023-10-26 18:28:12 +00:00
|
|
|
<Nav.Link className={styles.previewTab} eventKey='preview' disabled={!meta.value}>preview</Nav.Link>
|
2021-07-01 23:51:58 +00:00
|
|
|
</Nav.Item>
|
2023-11-06 20:53:33 +00:00
|
|
|
<span className='ms-auto text-muted d-flex align-items-center'>
|
2024-09-13 14:26:08 +00:00
|
|
|
<FileUpload
|
2023-11-06 20:53:33 +00:00
|
|
|
multiple
|
|
|
|
ref={imageUploadRef}
|
|
|
|
className='d-flex align-items-center me-1'
|
|
|
|
onUpload={file => {
|
2024-02-25 16:18:07 +00:00
|
|
|
const uploadMarker = `![Uploading ${file.name}…]()`
|
|
|
|
const text = innerRef.current.value
|
|
|
|
const cursorPosition = innerRef.current.selectionStart || text.length
|
|
|
|
let preMarker = text.slice(0, cursorPosition)
|
|
|
|
const postMarker = text.slice(cursorPosition)
|
|
|
|
// when uploading multiple files at once, we want to make sure the upload markers are separated by blank lines
|
|
|
|
if (preMarker && !/\n+\s*$/.test(preMarker)) {
|
|
|
|
preMarker += '\n\n'
|
|
|
|
}
|
|
|
|
const newText = preMarker + uploadMarker + postMarker
|
|
|
|
helpers.setValue(newText)
|
2023-11-13 15:30:01 +00:00
|
|
|
setSubmitDisabled?.(true)
|
2023-11-06 20:53:33 +00:00
|
|
|
}}
|
|
|
|
onSuccess={({ url, name }) => {
|
|
|
|
let text = innerRef.current.value
|
2024-05-03 19:09:27 +00:00
|
|
|
text = text.replace(`![Uploading ${name}…]()`, `![](${url})`)
|
2023-11-06 20:53:33 +00:00
|
|
|
helpers.setValue(text)
|
|
|
|
const s3Keys = [...text.matchAll(AWS_S3_URL_REGEXP)].map(m => Number(m[1]))
|
2024-09-13 14:26:08 +00:00
|
|
|
updateUploadFees({ variables: { s3Keys } })
|
2023-11-13 15:30:01 +00:00
|
|
|
setSubmitDisabled?.(false)
|
2023-11-06 20:53:33 +00:00
|
|
|
}}
|
|
|
|
onError={({ name }) => {
|
|
|
|
let text = innerRef.current.value
|
|
|
|
text = text.replace(`![Uploading ${name}…]()`, '')
|
|
|
|
helpers.setValue(text)
|
2023-11-13 15:30:01 +00:00
|
|
|
setSubmitDisabled?.(false)
|
2023-11-06 20:53:33 +00:00
|
|
|
}}
|
|
|
|
>
|
2024-09-13 14:26:08 +00:00
|
|
|
<AddFileIcon width={18} height={18} />
|
|
|
|
</FileUpload>
|
2023-11-06 20:53:33 +00:00
|
|
|
<a
|
|
|
|
className='d-flex align-items-center'
|
|
|
|
href='https://guides.github.com/features/mastering-markdown/' target='_blank' rel='noreferrer'
|
|
|
|
>
|
|
|
|
<Markdown width={18} height={18} />
|
|
|
|
</a>
|
|
|
|
</span>
|
2021-07-01 23:51:58 +00:00
|
|
|
</Nav>
|
2023-10-04 20:10:56 +00:00
|
|
|
<div className={`position-relative ${tab === 'write' ? '' : 'd-none'}`}>
|
|
|
|
<UserSuggest
|
2023-11-21 20:49:39 +00:00
|
|
|
query={mention?.query}
|
2023-10-04 20:10:56 +00:00
|
|
|
onSelect={insertMention}
|
2023-11-22 15:12:43 +00:00
|
|
|
dropdownStyle={mention?.style}
|
2023-10-10 15:15:59 +00:00
|
|
|
>{({ onKeyDown: userSuggestOnKeyDown, resetSuggestions }) => (
|
2023-10-04 20:10:56 +00:00
|
|
|
<InputInner
|
|
|
|
innerRef={innerRef}
|
2023-10-12 17:46:22 +00:00
|
|
|
{...props}
|
|
|
|
onChange={onChangeInner}
|
|
|
|
onKeyDown={onKeyDownInner(userSuggestOnKeyDown)}
|
2023-11-12 20:51:12 +00:00
|
|
|
onBlur={() => setTimeout(resetSuggestions, 500)}
|
2023-11-06 20:53:33 +00:00
|
|
|
onDragEnter={onDragEnter}
|
|
|
|
onDragLeave={onDragLeave}
|
|
|
|
onDrop={onDrop}
|
2024-04-08 21:31:02 +00:00
|
|
|
onPaste={onPaste}
|
2023-11-06 20:53:33 +00:00
|
|
|
className={dragStyle === 'over' ? styles.dragOver : ''}
|
2023-10-04 20:10:56 +00:00
|
|
|
/>)}
|
|
|
|
</UserSuggest>
|
2023-10-04 01:12:12 +00:00
|
|
|
</div>
|
|
|
|
{tab !== 'write' &&
|
|
|
|
<div className='form-group'>
|
|
|
|
<div className={`${styles.text} form-control`}>
|
2024-09-28 21:33:07 +00:00
|
|
|
<Text topLevel={topLevel} tab={tab}>{meta.value}</Text>
|
2023-10-04 01:12:12 +00:00
|
|
|
</div>
|
|
|
|
</div>}
|
2021-07-01 23:51:58 +00:00
|
|
|
</div>
|
|
|
|
</FormGroup>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-06-12 17:29:50 +00:00
|
|
|
function insertMarkdownFormatting (replaceFn, selectFn) {
|
|
|
|
return function (input, setValue, setSelectionRange) {
|
|
|
|
const start = input.selectionStart
|
|
|
|
const end = input.selectionEnd
|
|
|
|
const val = input.value
|
|
|
|
const selectedText = val.substring(start, end)
|
|
|
|
const mdFormatted = replaceFn(selectedText)
|
|
|
|
const newVal = val.substring(0, start) + mdFormatted + val.substring(end)
|
|
|
|
setValue(newVal)
|
|
|
|
// required for undo, see https://stackoverflow.com/a/27028258
|
|
|
|
document.execCommand('insertText', false, mdFormatted)
|
|
|
|
// see https://github.com/facebook/react/issues/6483
|
|
|
|
// for why we don't use `input.setSelectionRange` directly (hint: event order)
|
2023-06-19 18:07:06 +00:00
|
|
|
setSelectionRange(selectFn ? selectFn(start, end, mdFormatted) : { start: start + mdFormatted.length, end: start + mdFormatted.length })
|
2023-06-12 17:29:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-19 18:07:06 +00:00
|
|
|
const insertMarkdownTabFormatting = insertMarkdownFormatting(
|
|
|
|
val => `\t${val}`,
|
|
|
|
(start, end, mdFormatted) => ({ start: start + 1, end: end + 1 }) // move inside tab
|
|
|
|
)
|
2023-06-12 17:29:50 +00:00
|
|
|
const insertMarkdownLinkFormatting = insertMarkdownFormatting(
|
|
|
|
val => `[${val}](url)`,
|
2023-06-19 18:07:06 +00:00
|
|
|
(start, end, mdFormatted) => (
|
|
|
|
start === end
|
|
|
|
? { start: start + 1, end: end + 1 } // move inside brackets
|
|
|
|
: { start: start + mdFormatted.length - 4, end: start + mdFormatted.length - 1 }) // move to select url part
|
|
|
|
)
|
|
|
|
const insertMarkdownBoldFormatting = insertMarkdownFormatting(
|
|
|
|
val => `**${val}**`,
|
|
|
|
(start, end, mdFormatted) => ({ start: start + 2, end: end + 2 }) // move inside bold
|
|
|
|
)
|
|
|
|
const insertMarkdownItalicFormatting = insertMarkdownFormatting(
|
|
|
|
val => `_${val}_`,
|
|
|
|
(start, end, mdFormatted) => ({ start: start + 1, end: end + 1 }) // move inside italic
|
2023-06-12 17:29:50 +00:00
|
|
|
)
|
|
|
|
|
2021-07-01 23:51:58 +00:00
|
|
|
function FormGroup ({ className, label, children }) {
|
2021-04-14 00:57:32 +00:00
|
|
|
return (
|
2023-07-24 18:35:05 +00:00
|
|
|
<BootstrapForm.Group className={`form-group ${className}`}>
|
2021-04-14 00:57:32 +00:00
|
|
|
{label && <BootstrapForm.Label>{label}</BootstrapForm.Label>}
|
2021-07-01 23:51:58 +00:00
|
|
|
{children}
|
|
|
|
</BootstrapForm.Group>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-01-07 18:28:23 +00:00
|
|
|
function InputInner ({
|
2024-03-19 22:23:59 +00:00
|
|
|
prepend, append, hint, warn, showValid, onChange, onBlur, overrideValue, appendValue,
|
2023-10-06 01:33:14 +00:00
|
|
|
innerRef, noForm, clear, onKeyDown, inputGroupClassName, debounce: debounceTime, maxLength,
|
2023-10-04 01:12:12 +00:00
|
|
|
...props
|
2022-01-07 18:28:23 +00:00
|
|
|
}) {
|
2022-08-18 18:15:24 +00:00
|
|
|
const [field, meta, helpers] = noForm ? [{}, {}, {}] : useField(props)
|
|
|
|
const formik = noForm ? null : useFormikContext()
|
2023-05-11 19:34:42 +00:00
|
|
|
const storageKeyPrefix = useContext(StorageKeyPrefixContext)
|
2021-08-22 15:25:17 +00:00
|
|
|
|
2022-01-07 18:28:23 +00:00
|
|
|
const storageKey = storageKeyPrefix ? storageKeyPrefix + '-' + props.name : undefined
|
|
|
|
|
2023-10-12 17:46:22 +00:00
|
|
|
const onKeyDownInner = useCallback((e) => {
|
|
|
|
const metaOrCtrl = e.metaKey || e.ctrlKey
|
|
|
|
if (metaOrCtrl) {
|
|
|
|
if (e.key === 'Enter') formik?.submitForm()
|
|
|
|
}
|
|
|
|
|
|
|
|
if (onKeyDown) onKeyDown(e)
|
|
|
|
}, [formik?.submitForm, onKeyDown])
|
|
|
|
|
|
|
|
const onChangeInner = useCallback((e) => {
|
|
|
|
field?.onChange(e)
|
|
|
|
|
|
|
|
if (storageKey) {
|
|
|
|
window.localStorage.setItem(storageKey, e.target.value)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (onChange) {
|
|
|
|
onChange(formik, e)
|
|
|
|
}
|
|
|
|
}, [field?.onChange, storageKey, onChange])
|
|
|
|
|
|
|
|
const onBlurInner = useCallback((e) => {
|
|
|
|
field?.onBlur?.(e)
|
|
|
|
onBlur && onBlur(e)
|
|
|
|
}, [field?.onBlur, onBlur])
|
|
|
|
|
2021-08-22 15:25:17 +00:00
|
|
|
useEffect(() => {
|
|
|
|
if (overrideValue) {
|
|
|
|
helpers.setValue(overrideValue)
|
2022-01-07 18:28:23 +00:00
|
|
|
if (storageKey) {
|
2023-07-25 14:14:45 +00:00
|
|
|
window.localStorage.setItem(storageKey, overrideValue)
|
2022-01-07 18:28:23 +00:00
|
|
|
}
|
|
|
|
} else if (storageKey) {
|
2023-07-25 14:14:45 +00:00
|
|
|
const draft = window.localStorage.getItem(storageKey)
|
2022-01-07 18:28:23 +00:00
|
|
|
if (draft) {
|
|
|
|
// for some reason we have to turn off validation to get formik to
|
|
|
|
// not assume this is invalid
|
2024-03-07 01:45:00 +00:00
|
|
|
const isNumeric = /^[0-9]+$/.test(draft)
|
|
|
|
const numericExpected = typeof field.value === 'number'
|
2024-03-19 22:23:59 +00:00
|
|
|
helpers.setValue(isNumeric && numericExpected ? parseInt(draft) : draft)
|
2023-11-11 00:18:10 +00:00
|
|
|
onChange && onChange(formik, { target: { value: draft } })
|
2022-01-07 18:28:23 +00:00
|
|
|
}
|
2021-08-22 15:25:17 +00:00
|
|
|
}
|
|
|
|
}, [overrideValue])
|
|
|
|
|
2023-11-21 03:37:57 +00:00
|
|
|
useEffect(() => {
|
|
|
|
if (appendValue) {
|
|
|
|
const updatedValue = meta.value ? `${meta.value}\n${appendValue}` : appendValue
|
|
|
|
helpers.setValue(updatedValue)
|
|
|
|
if (storageKey) {
|
|
|
|
window.localStorage.setItem(storageKey, updatedValue)
|
|
|
|
}
|
|
|
|
innerRef?.current?.focus()
|
|
|
|
}
|
|
|
|
}, [appendValue])
|
|
|
|
|
2023-05-11 19:34:42 +00:00
|
|
|
const invalid = (!formik || formik.submitCount > 0) && meta.touched && meta.error
|
2022-08-25 18:46:07 +00:00
|
|
|
|
2023-10-06 01:33:14 +00:00
|
|
|
useEffect(debounce(() => {
|
|
|
|
if (!noForm && !isNaN(debounceTime) && debounceTime > 0) {
|
|
|
|
formik.validateForm()
|
2023-08-09 22:06:22 +00:00
|
|
|
}
|
2023-10-06 01:33:14 +00:00
|
|
|
}, debounceTime), [noForm, formik, field.value])
|
2023-08-09 22:06:22 +00:00
|
|
|
|
2023-09-12 00:59:36 +00:00
|
|
|
const remaining = maxLength && maxLength - (field.value || '').length
|
|
|
|
|
2021-07-01 23:51:58 +00:00
|
|
|
return (
|
|
|
|
<>
|
2023-08-28 19:31:28 +00:00
|
|
|
<InputGroup hasValidation className={inputGroupClassName}>
|
2023-07-24 18:35:05 +00:00
|
|
|
{prepend}
|
2021-04-14 00:57:32 +00:00
|
|
|
<BootstrapForm.Control
|
2021-09-10 18:55:36 +00:00
|
|
|
ref={innerRef}
|
2023-10-12 17:46:22 +00:00
|
|
|
{...field}
|
|
|
|
{...props}
|
|
|
|
onKeyDown={onKeyDownInner}
|
|
|
|
onChange={onChangeInner}
|
|
|
|
onBlur={onBlurInner}
|
2022-08-25 18:46:07 +00:00
|
|
|
isInvalid={invalid}
|
2022-04-19 18:32:39 +00:00
|
|
|
isValid={showValid && meta.initialValue !== meta.value && meta.touched && !meta.error}
|
2021-04-14 00:57:32 +00:00
|
|
|
/>
|
2023-07-24 18:35:05 +00:00
|
|
|
{(clear && field.value) &&
|
|
|
|
<Button
|
|
|
|
variant={null}
|
|
|
|
onClick={(e) => {
|
|
|
|
helpers.setValue('')
|
|
|
|
if (storageKey) {
|
2023-07-25 14:14:45 +00:00
|
|
|
window.localStorage.removeItem(storageKey)
|
2023-07-24 18:35:05 +00:00
|
|
|
}
|
|
|
|
if (onChange) {
|
|
|
|
onChange(formik, { target: { value: '' } })
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
className={`${styles.clearButton} ${styles.appendButton} ${invalid ? styles.isInvalid : ''}`}
|
|
|
|
><CloseIcon className='fill-grey' height={20} width={20} />
|
|
|
|
</Button>}
|
|
|
|
{append}
|
2021-04-14 00:57:32 +00:00
|
|
|
<BootstrapForm.Control.Feedback type='invalid'>
|
|
|
|
{meta.touched && meta.error}
|
|
|
|
</BootstrapForm.Control.Feedback>
|
|
|
|
</InputGroup>
|
2021-05-25 00:08:56 +00:00
|
|
|
{hint && (
|
|
|
|
<BootstrapForm.Text>
|
|
|
|
{hint}
|
|
|
|
</BootstrapForm.Text>
|
|
|
|
)}
|
2024-03-19 22:23:59 +00:00
|
|
|
{warn && (
|
|
|
|
<BootstrapForm.Text className='text-warning'>
|
|
|
|
{warn}
|
|
|
|
</BootstrapForm.Text>
|
|
|
|
)}
|
|
|
|
{!warn && maxLength && !(meta.touched && meta.error && invalid) && (
|
2023-10-26 20:12:21 +00:00
|
|
|
<BootstrapForm.Text className={remaining < 0 ? 'text-danger' : 'text-muted'}>
|
2023-09-12 00:59:36 +00:00
|
|
|
{`${numWithUnits(remaining, { abbreviate: false, unitSingular: 'character', unitPlural: 'characters' })} remaining`}
|
2023-09-12 00:20:44 +00:00
|
|
|
</BootstrapForm.Text>
|
|
|
|
)}
|
2021-07-01 23:51:58 +00:00
|
|
|
</>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-10-12 17:46:22 +00:00
|
|
|
const INITIAL_SUGGESTIONS = { array: [], index: 0 }
|
2023-10-10 23:15:10 +00:00
|
|
|
export function UserSuggest ({
|
|
|
|
query, onSelect, dropdownStyle, children,
|
|
|
|
transformUser = user => user, selectWithTab = true, filterUsers = () => true
|
|
|
|
}) {
|
2023-11-21 20:49:39 +00:00
|
|
|
const [getSuggestions] = useLazyQuery(USER_SUGGESTIONS, {
|
2023-10-04 20:10:56 +00:00
|
|
|
onCompleted: data => {
|
2023-11-21 20:49:39 +00:00
|
|
|
query !== undefined && setSuggestions({
|
|
|
|
array: data.userSuggestions
|
2023-10-10 23:15:10 +00:00
|
|
|
.filter((...args) => filterUsers(query, ...args))
|
|
|
|
.map(transformUser),
|
|
|
|
index: 0
|
|
|
|
})
|
2022-08-26 22:20:09 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
const [suggestions, setSuggestions] = useState(INITIAL_SUGGESTIONS)
|
2023-10-04 20:10:56 +00:00
|
|
|
const resetSuggestions = useCallback(() => setSuggestions(INITIAL_SUGGESTIONS), [])
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (query !== undefined) {
|
2023-10-12 20:29:22 +00:00
|
|
|
// remove both the leading @ and any @domain after nym
|
|
|
|
const q = query?.replace(/^[@ ]+|[ ]+$/g, '').replace(/@[^\s]*$/, '')
|
2023-11-21 20:49:39 +00:00
|
|
|
getSuggestions({ variables: { q, limit: 5 } })
|
2023-10-04 20:10:56 +00:00
|
|
|
} else {
|
|
|
|
resetSuggestions()
|
|
|
|
}
|
2023-11-21 20:49:39 +00:00
|
|
|
}, [query, resetSuggestions, getSuggestions])
|
2023-10-04 20:10:56 +00:00
|
|
|
|
|
|
|
const onKeyDown = useCallback(e => {
|
|
|
|
switch (e.code) {
|
|
|
|
case 'ArrowUp':
|
|
|
|
if (suggestions.array.length === 0) {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
e.preventDefault()
|
|
|
|
setSuggestions(suggestions =>
|
|
|
|
({
|
|
|
|
...suggestions,
|
|
|
|
index: Math.max(suggestions.index - 1, 0)
|
|
|
|
}))
|
|
|
|
break
|
|
|
|
case 'ArrowDown':
|
|
|
|
if (suggestions.array.length === 0) {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
e.preventDefault()
|
|
|
|
setSuggestions(suggestions =>
|
|
|
|
({
|
|
|
|
...suggestions,
|
|
|
|
index: Math.min(suggestions.index + 1, suggestions.array.length - 1)
|
|
|
|
}))
|
|
|
|
break
|
|
|
|
case 'Tab':
|
|
|
|
case 'Enter':
|
2023-10-10 23:15:10 +00:00
|
|
|
if (e.code === 'Tab' && !selectWithTab) {
|
|
|
|
break
|
|
|
|
}
|
2023-10-04 20:10:56 +00:00
|
|
|
if (suggestions.array?.length === 0) {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
e.preventDefault()
|
|
|
|
onSelect(suggestions.array[suggestions.index].name)
|
|
|
|
resetSuggestions()
|
|
|
|
break
|
|
|
|
case 'Escape':
|
|
|
|
e.preventDefault()
|
|
|
|
resetSuggestions()
|
|
|
|
break
|
|
|
|
default:
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}, [onSelect, resetSuggestions, suggestions])
|
2023-11-12 20:51:12 +00:00
|
|
|
|
2022-08-26 22:20:09 +00:00
|
|
|
return (
|
2023-10-04 20:10:56 +00:00
|
|
|
<>
|
2023-10-10 15:15:59 +00:00
|
|
|
{children?.({ onKeyDown, resetSuggestions })}
|
2023-10-04 20:10:56 +00:00
|
|
|
<Dropdown show={suggestions.array.length > 0} style={dropdownStyle}>
|
2023-07-24 18:35:05 +00:00
|
|
|
<Dropdown.Menu className={styles.suggestionsMenu}>
|
2022-08-26 22:20:09 +00:00
|
|
|
{suggestions.array.map((v, i) =>
|
2023-07-24 18:35:05 +00:00
|
|
|
<Dropdown.Item
|
2022-08-26 22:20:09 +00:00
|
|
|
key={v.name}
|
|
|
|
active={suggestions.index === i}
|
|
|
|
onClick={() => {
|
2023-10-04 20:10:56 +00:00
|
|
|
onSelect(v.name)
|
|
|
|
resetSuggestions()
|
2022-08-26 22:20:09 +00:00
|
|
|
}}
|
|
|
|
>
|
|
|
|
{v.name}
|
2023-07-24 18:35:05 +00:00
|
|
|
</Dropdown.Item>)}
|
|
|
|
</Dropdown.Menu>
|
|
|
|
</Dropdown>
|
2023-10-04 20:10:56 +00:00
|
|
|
</>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-10-12 20:29:22 +00:00
|
|
|
export function InputUserSuggest ({
|
|
|
|
label, groupClassName, transformUser, filterUsers,
|
|
|
|
selectWithTab, onChange, transformQuery, ...props
|
|
|
|
}) {
|
2023-10-04 20:10:56 +00:00
|
|
|
const [ovalue, setOValue] = useState()
|
|
|
|
const [query, setQuery] = useState()
|
|
|
|
return (
|
|
|
|
<FormGroup label={label} className={groupClassName}>
|
|
|
|
<UserSuggest
|
2023-10-10 15:15:59 +00:00
|
|
|
transformUser={transformUser}
|
2023-10-10 23:15:10 +00:00
|
|
|
filterUsers={filterUsers}
|
|
|
|
selectWithTab={selectWithTab}
|
2023-10-12 20:29:22 +00:00
|
|
|
onSelect={(v) => {
|
|
|
|
// HACK ... ovalue does not trigger onChange
|
|
|
|
onChange && onChange(undefined, { target: { value: v } })
|
|
|
|
setOValue(v)
|
|
|
|
}}
|
2023-10-04 20:10:56 +00:00
|
|
|
query={query}
|
|
|
|
>
|
2023-10-10 15:15:59 +00:00
|
|
|
{({ onKeyDown, resetSuggestions }) => (
|
2023-10-04 20:10:56 +00:00
|
|
|
<InputInner
|
|
|
|
{...props}
|
|
|
|
autoComplete='off'
|
2023-10-12 20:29:22 +00:00
|
|
|
onChange={(formik, e) => {
|
|
|
|
onChange && onChange(formik, e)
|
2023-10-04 20:10:56 +00:00
|
|
|
setOValue(e.target.value)
|
|
|
|
setQuery(e.target.value.replace(/^[@ ]+|[ ]+$/g, ''))
|
|
|
|
}}
|
|
|
|
overrideValue={ovalue}
|
|
|
|
onKeyDown={onKeyDown}
|
2023-11-21 20:49:39 +00:00
|
|
|
onBlur={() => setTimeout(resetSuggestions, 500)}
|
2023-10-04 20:10:56 +00:00
|
|
|
/>
|
|
|
|
)}
|
|
|
|
</UserSuggest>
|
2022-08-26 22:20:09 +00:00
|
|
|
</FormGroup>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2024-10-04 20:00:13 +00:00
|
|
|
export function Input ({ label, groupClassName, ...props }) {
|
2021-07-01 23:51:58 +00:00
|
|
|
return (
|
|
|
|
<FormGroup label={label} className={groupClassName}>
|
|
|
|
<InputInner {...props} />
|
|
|
|
</FormGroup>
|
2021-04-14 00:57:32 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
multiple forwards on a post (#403)
* multiple forwards on a post
first phase of the multi-forward support
* update the graphql mutation for discussion posts to accept and validate multiple forwards
* update the discussion form to allow multiple forwards in the UI
* start working on db schema changes
* uncomment db schema, add migration to create the new model, and update create_item, update_item
stored procedures
* Propagate updates from discussion to poll, link, and bounty forms
Update the create, update poll sql functions for multi forward support
* Update gql, typedefs, and resolver to return forwarded users in items responses
* UI changes to show multiple forward recipients, and conditional upvote logic changes
* Update notification text to reflect multiple forwards upon vote action
* Disallow duplicate stacker entries
* reduce duplication in populating adv-post-form initial values
* Update item_act sql function to implement multi-way forwarding
* Update referral functions to scale referral bonuses for forwarded users
* Update notification text to reflect non-100% forwarded sats cases
* Update wallet history sql queries to accommodate multi-forward use cases
* Block zaps for posts you are forwarded zaps at the API layer, in addition
to in the UI
* Delete fwdUserId column from Item table as part of migration
* Fix how we calculate stacked sats after partial forwards in wallet history
* Exclude entries from wallet history that are 0 stacked sats from posts with 100% forwarded to other users
* Fix wallet history query for forwarded stacked sats to be scaled by the fwd pct
* Reduce duplication in adv post form, and do some style tweaks for better layout
* Use MAX_FORWARDS constants
* Address various PR feedback
* first enhancement pass
* enhancement pass too
---------
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2023-08-23 22:44:17 +00:00
|
|
|
export function VariableInput ({ label, groupClassName, name, hint, max, min, readOnlyLen, children, emptyItem = '', ...props }) {
|
2022-07-30 13:25:46 +00:00
|
|
|
return (
|
|
|
|
<FormGroup label={label} className={groupClassName}>
|
multiple forwards on a post (#403)
* multiple forwards on a post
first phase of the multi-forward support
* update the graphql mutation for discussion posts to accept and validate multiple forwards
* update the discussion form to allow multiple forwards in the UI
* start working on db schema changes
* uncomment db schema, add migration to create the new model, and update create_item, update_item
stored procedures
* Propagate updates from discussion to poll, link, and bounty forms
Update the create, update poll sql functions for multi forward support
* Update gql, typedefs, and resolver to return forwarded users in items responses
* UI changes to show multiple forward recipients, and conditional upvote logic changes
* Update notification text to reflect multiple forwards upon vote action
* Disallow duplicate stacker entries
* reduce duplication in populating adv-post-form initial values
* Update item_act sql function to implement multi-way forwarding
* Update referral functions to scale referral bonuses for forwarded users
* Update notification text to reflect non-100% forwarded sats cases
* Update wallet history sql queries to accommodate multi-forward use cases
* Block zaps for posts you are forwarded zaps at the API layer, in addition
to in the UI
* Delete fwdUserId column from Item table as part of migration
* Fix how we calculate stacked sats after partial forwards in wallet history
* Exclude entries from wallet history that are 0 stacked sats from posts with 100% forwarded to other users
* Fix wallet history query for forwarded stacked sats to be scaled by the fwd pct
* Reduce duplication in adv post form, and do some style tweaks for better layout
* Use MAX_FORWARDS constants
* Address various PR feedback
* first enhancement pass
* enhancement pass too
---------
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2023-08-23 22:44:17 +00:00
|
|
|
<FieldArray name={name} hasValidation>
|
2022-07-30 13:25:46 +00:00
|
|
|
{({ form, ...fieldArrayHelpers }) => {
|
2022-08-18 18:15:24 +00:00
|
|
|
const options = form.values[name]
|
2022-07-30 13:25:46 +00:00
|
|
|
return (
|
|
|
|
<>
|
2023-01-07 00:53:09 +00:00
|
|
|
{options?.map((_, i) => (
|
2022-07-30 13:25:46 +00:00
|
|
|
<div key={i}>
|
2023-07-24 18:35:05 +00:00
|
|
|
<Row className='mb-2'>
|
2022-07-30 13:25:46 +00:00
|
|
|
<Col>
|
multiple forwards on a post (#403)
* multiple forwards on a post
first phase of the multi-forward support
* update the graphql mutation for discussion posts to accept and validate multiple forwards
* update the discussion form to allow multiple forwards in the UI
* start working on db schema changes
* uncomment db schema, add migration to create the new model, and update create_item, update_item
stored procedures
* Propagate updates from discussion to poll, link, and bounty forms
Update the create, update poll sql functions for multi forward support
* Update gql, typedefs, and resolver to return forwarded users in items responses
* UI changes to show multiple forward recipients, and conditional upvote logic changes
* Update notification text to reflect multiple forwards upon vote action
* Disallow duplicate stacker entries
* reduce duplication in populating adv-post-form initial values
* Update item_act sql function to implement multi-way forwarding
* Update referral functions to scale referral bonuses for forwarded users
* Update notification text to reflect non-100% forwarded sats cases
* Update wallet history sql queries to accommodate multi-forward use cases
* Block zaps for posts you are forwarded zaps at the API layer, in addition
to in the UI
* Delete fwdUserId column from Item table as part of migration
* Fix how we calculate stacked sats after partial forwards in wallet history
* Exclude entries from wallet history that are 0 stacked sats from posts with 100% forwarded to other users
* Fix wallet history query for forwarded stacked sats to be scaled by the fwd pct
* Reduce duplication in adv post form, and do some style tweaks for better layout
* Use MAX_FORWARDS constants
* Address various PR feedback
* first enhancement pass
* enhancement pass too
---------
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2023-08-23 22:44:17 +00:00
|
|
|
{children
|
|
|
|
? children({ index: i, readOnly: i < readOnlyLen, placeholder: i >= min ? 'optional' : undefined })
|
|
|
|
: <InputInner name={`${name}[${i}]`} {...props} readOnly={i < readOnlyLen} placeholder={i >= min ? 'optional' : undefined} />}
|
2022-07-30 13:25:46 +00:00
|
|
|
</Col>
|
multiple forwards on a post (#403)
* multiple forwards on a post
first phase of the multi-forward support
* update the graphql mutation for discussion posts to accept and validate multiple forwards
* update the discussion form to allow multiple forwards in the UI
* start working on db schema changes
* uncomment db schema, add migration to create the new model, and update create_item, update_item
stored procedures
* Propagate updates from discussion to poll, link, and bounty forms
Update the create, update poll sql functions for multi forward support
* Update gql, typedefs, and resolver to return forwarded users in items responses
* UI changes to show multiple forward recipients, and conditional upvote logic changes
* Update notification text to reflect multiple forwards upon vote action
* Disallow duplicate stacker entries
* reduce duplication in populating adv-post-form initial values
* Update item_act sql function to implement multi-way forwarding
* Update referral functions to scale referral bonuses for forwarded users
* Update notification text to reflect non-100% forwarded sats cases
* Update wallet history sql queries to accommodate multi-forward use cases
* Block zaps for posts you are forwarded zaps at the API layer, in addition
to in the UI
* Delete fwdUserId column from Item table as part of migration
* Fix how we calculate stacked sats after partial forwards in wallet history
* Exclude entries from wallet history that are 0 stacked sats from posts with 100% forwarded to other users
* Fix wallet history query for forwarded stacked sats to be scaled by the fwd pct
* Reduce duplication in adv post form, and do some style tweaks for better layout
* Use MAX_FORWARDS constants
* Address various PR feedback
* first enhancement pass
* enhancement pass too
---------
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2023-08-23 22:44:17 +00:00
|
|
|
<Col className='d-flex ps-0' xs='auto'>
|
|
|
|
{options.length - 1 === i && options.length !== max
|
|
|
|
? <AddIcon className='fill-grey align-self-center justify-self-center pointer' onClick={() => fieldArrayHelpers.push(emptyItem)} />
|
|
|
|
// filler div for col alignment across rows
|
|
|
|
: <div style={{ width: '24px', height: '24px' }} />}
|
|
|
|
</Col>
|
|
|
|
{options.length - 1 === i &&
|
|
|
|
<>
|
|
|
|
{hint && <BootstrapForm.Text>{hint}</BootstrapForm.Text>}
|
|
|
|
{form.touched[name] && typeof form.errors[name] === 'string' &&
|
|
|
|
<div className='invalid-feedback d-block'>{form.errors[name]}</div>}
|
|
|
|
</>}
|
2023-07-24 18:35:05 +00:00
|
|
|
</Row>
|
2022-07-30 13:25:46 +00:00
|
|
|
</div>
|
|
|
|
))}
|
|
|
|
</>
|
|
|
|
)
|
|
|
|
}}
|
|
|
|
</FieldArray>
|
|
|
|
</FormGroup>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-11-21 23:32:22 +00:00
|
|
|
export function Checkbox ({
|
|
|
|
children, label, groupClassName, type = 'checkbox',
|
|
|
|
hiddenLabel, extra, handleChange, inline, disabled, ...props
|
|
|
|
}) {
|
2021-09-12 16:55:38 +00:00
|
|
|
// React treats radios and checkbox inputs differently other input types, select, and textarea.
|
|
|
|
// Formik does this too! When you specify `type` to useField(), it will
|
|
|
|
// return the correct bag of props for you
|
2023-11-21 23:32:22 +00:00
|
|
|
const [field, meta, helpers] = useField({ ...props, type })
|
2021-09-12 16:55:38 +00:00
|
|
|
return (
|
2023-07-24 18:35:05 +00:00
|
|
|
<FormGroup className={groupClassName}>
|
2022-03-07 21:50:13 +00:00
|
|
|
{hiddenLabel && <BootstrapForm.Label className='invisible'>{label}</BootstrapForm.Label>}
|
|
|
|
<BootstrapForm.Check
|
|
|
|
id={props.id || props.name}
|
|
|
|
inline={inline}
|
|
|
|
>
|
|
|
|
<BootstrapForm.Check.Input
|
2023-11-21 23:32:22 +00:00
|
|
|
isInvalid={meta.touched && meta.error}
|
|
|
|
{...field} {...props} disabled={disabled} type={type} onChange={(e) => {
|
2022-03-07 21:50:13 +00:00
|
|
|
field.onChange(e)
|
2023-06-12 18:03:44 +00:00
|
|
|
handleChange && handleChange(e.target.checked, helpers.setValue)
|
2022-03-07 21:50:13 +00:00
|
|
|
}}
|
|
|
|
/>
|
2023-08-04 00:14:04 +00:00
|
|
|
<BootstrapForm.Check.Label className={'d-inline-flex flex-nowrap align-items-center' + (disabled ? ' text-muted' : '')}>
|
2022-03-07 21:50:13 +00:00
|
|
|
<div className='flex-grow-1'>{label}</div>
|
|
|
|
{extra &&
|
|
|
|
<div className={styles.checkboxExtra}>
|
|
|
|
{extra}
|
|
|
|
</div>}
|
|
|
|
</BootstrapForm.Check.Label>
|
|
|
|
</BootstrapForm.Check>
|
2023-07-24 18:35:05 +00:00
|
|
|
</FormGroup>
|
2021-09-12 16:55:38 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-11-21 23:32:22 +00:00
|
|
|
export function CheckboxGroup ({ label, groupClassName, children, ...props }) {
|
|
|
|
const [, meta] = useField(props)
|
|
|
|
return (
|
|
|
|
<FormGroup label={label} className={groupClassName}>
|
|
|
|
{children}
|
|
|
|
{/* force the feedback to display with d-block */}
|
|
|
|
<BootstrapForm.Control.Feedback className='d-block' type='invalid'>
|
|
|
|
{meta.touched && meta.error}
|
|
|
|
</BootstrapForm.Control.Feedback>
|
|
|
|
</FormGroup>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-05-11 19:34:42 +00:00
|
|
|
const StorageKeyPrefixContext = createContext()
|
|
|
|
|
2021-04-14 00:57:32 +00:00
|
|
|
export function Form ({
|
Wallet definitions with uniform interface (#1243)
* wip: Use uniform interface for wallets
* Fix import error
* Update wallet logging + other stuff
* add canPay and canSend to wallet definition
* rename 'default payment method' to 'enabled' and add enable + disable method
* Set canPay, canReceive in useWallet
* Enable wallet if just configured
* Don't pass logger to sendPayment
* Add logging to attach & detach
* Add schema to wallet def
* Add NWC wallet
* Fix unused isDefault saved in config
* Fix enableWallet
* wrong storage key was used
* broke if wallets with no configs existed
* Run validation during save
* Use INFO level for 'wallet disabled' message
* Pass config with spread operator
* Support help, optional, hint in wallet fields
* wip: Add LNC
* Fix 20s page load for /settings/wallets.json?nodata=true
For some reason, if nodata is passed (which is the case if going back), the page takes 20s to load.
* Fix extremely slow page load for LNC import
I noticed that the combination of
```
import { Form, PasswordInput, SubmitButton } from '@/components/form'
```
in components/wallet/lnc.js and the dynamic import via `await import` in components/wallet/index.js caused extremely slow page loads.
* Use normal imports
* Revert "Fix 20s page load for /settings/wallets.json?nodata=true"
This reverts commit deb476b3a966569fefcfdf4082d6b64f90fbd0a2.
Not using the dynamic import for LNC fixed the slow page load with ?nodata=true.
* Remove follow and show recent logs first
* Fix position of log start marker
* Add FIXMEs for LNC
I can't get LNC to connect. It just hangs forever on lnc.connect(). See FIXMEs.
* Remove logger.error since already handled in useWallet
* Don't require destructuring to pass props to input
* wip: Add LND autowithdrawals
* receiving wallets need to export 'server' object field
* don't print macaroon error stack
* fix missing wallet logs order update
* mark autowithdrawl settings as required
* fix server wallet logs deletion
* remove canPay and canReceive since it was confusing where it is available
TODO
* also use numeric priority for sending wallets to be consistent with how status for receiving wallets is determined
* define createInvoice function in wallet definition
* consistent wallet logs: sending wallets use 'wallet attached'+'wallet enabled/disabled' whereas receiving wallets use 'wallet created/updated'
* see FIXMEs
* Fix TypeError
* Fix sendPayment called with empty config
* removed useEffect such that config is available on first render
* fix hydration error using dynamic import without SSR
* Fix confusing UX around enabled
* Remove FIXMEs
Rebase on master seemed to have fixed these, weird
* Use same error format in toast and wallet log
* Fix usage of conditional hooks in useConfig
* Fix isConfigured
* Fix delete wallet logs on server
* Fix wallet logs refetch
onError does not exist on client.mutate
* Fix TypeError in isConfigured if no enabled wallet found
* Only include local/server config if required
* Fix another hydration error
* Fix server config not updated after save or detach
* Also use 'enabled' for server wallets
* Fix wallet logs not updated after server delete
* Consistent logs between local and server wallets
* 'wallet attached' on create
* 'wallet updated' on config updates
* 'wallet enabled' and 'wallet disabled' if checkbox changed
* 'wallet detached' on delete
* Also enable server wallets on create
* Disable checkbox if not configured yet
* Move all validation schema into lib/validate
* Implement drag & drop w/o persistence
* Use dynamic import for WalletCard
This fixes a lot of issues with hydration
* Save order as priority
* Fix autowithdrawSettings not applied
Form requires config in flat format but mutation requires autowithdraw settings in a separate 'settings' field.
I have decided that config will be in flat form format. It will be transformed into mutation format during save.
* Save dedicated enabled flag for server wallets
* wallet table now contains boolean column 'enabled'
* 'priority' is now a number everywhere
* use consistent order between how autowithdrawals are attempted and server wallets cards
* Fix onCanceled missing
* Fix typo
* Fix noisy changes in lib/validate
I moved the schema for lnbits, nwc and lnc out of lib/validate only to put them back in there later.
This commit should make the changeset cleaner by removing noise.
* Split arguments into [value,] config, context
* Run lnbits url.replace in validate and sendPayment
* Remove unnecessary WALLETS_QUERY
* Generate wallet mutation from fields
* Generate wallet resolver from fields
* Fix import inconsistency between app and worker
* Use wallet.createInvoice for autowithdrawals
* Fix success autowithdrawal log
* Fix wallet security banner shown for server wallets
* Add autowithdrawal to lightning address
* Add optional wallet short name for logging
* Fix draggable
* Fix autowithdraw loop
* Add missing hints
* Add CLN autowithdrawal
* Detach wallets and delete logs on logout
* Remove Wallet in lib/constants
* Use inject function for resolvers and typeDefs
* Fix priority ignored when fetching enabled wallet
* Fix draggable false on first page load due to SSR
* Use touches instead of dnd on mobile
Browsers don't support drag events for touch devices.
To have a consistent implementation for desktop and mobile, we would need to use mousedown/touchstart, mouseup/touchend and mousemove/touchmove.
For now, this commit makes changing the order possible on touch devices with simple touches.
* Fix duplicate CLN error
* Fix autowithdraw priority order
* Fix error per invalid bip39 word
* Update LNC code
* remove LNC FIXMEs
Mhh, I guess the TURN server was down or something? It now magically works. Or maybe it only works once per mnemonic?
* also removed the lnc.lnd.lightning.getInfo() call since we don't ask and need permission for this RPC for payments.
* setting a password does not work though. It fails with 'The password provided is not valid' which is triggered at https://github.com/lightninglabs/lnc-web/blob/main/lib/util/credentialStore.ts#L81.
* Fix order if wallet with no priority exists
* Use common sort
* Add link to lnbits.com
* Add example wallet def
* Remove TODOs
TODO in components/wallet-logger.js was handled.
I don't see a need for the TODO in lib/wallet.js anymore. This function will only be called with the wallet of type LIGHTNING_ADDRESS anyway.
* Remove console.log
* Toast priority save errors
* Fix leaking relay connections
* Remove 'tor or clearnet' hint for LN addresses
* Remove React dependency from wallet definitions
* Generate resolver name from walletField
* Move wallets into top level directory wallet/
* Put wallets into own folder
* Fix generateMutation
* remove resolverName property from wallet defs
* move function into lib/wallet
* use function in generateMutation on client to fix wrongly generated mutation
* Separate client and server imports by files
* wallets now consist of an index.js, a client.js and a server.js file
* client.js is imported on the client and contains the client portion
* server.js is imported on the server and contains the server porition
* both reexport index.js so everything in index.js can be shared by client and server
* every wallet contains a client.js file since they are all imported on the client to show the cards
* client.js of every wallet is reexported as an array in wallets/client.js
* server.js of every wallet is reexported as an array in wallets/server.js
FIXME: for some reason, worker does not properly import the default export of wallets/server.js
* Fix worker import of wallets/server
* Fix wallet.server usage
* I removed wallet.server in a previous commit
* the client couldn't determine which wallet was stored on the server since all server specific fields were set in server.js
* walletType and walletField are now set in index.js
* walletType is now used to determine if a wallet is stored on the server
* also included some formatting changes
* Fix w.default usage
Since package.json with { "type": "module" } was added, this is no longer needed.
* Fix id access in walletPrioritySort
* Fix autowithdrawal error log
* Generate validation schema for LNbits
* Generate validation schema for NWC
* Rename to torAllowed
* Generate validation schema for LNC
* Generate validation schema for LND
* Generate validation schema for LnAddr
* Remove stringTypes
* Generate validation schema for CLN
* Make clear that message belongs to test
* validate.message was used in tandem with validate.test
* it might be confused as the message if the validation for validate.type failed
* now validate.test can be a function or an object of { test, message } shape which matches Yup.test
* Remove validate.schema as a trap door
* make lnc work
* Return null if no wallet was found
* Revert code around schema generation
* Transform autowithdrawSchemaMembers into an object
* Rename schema to yupSchema
* Fix missing required for LNbits adminKey
* Support formik form-level validation
* Fix missing addWalletLog import
* Fix missing space after =
* fix merge conflict resolution mistake
* remove non-custodial* badges
* create guides for attaching wallets in sndev
* Use built-in formik validation or Yup schema but not both
* Rename: validate -> testConnectClient, testConnect -> testConnectServer
* make lnaddr autowithdraw work in dev
* move ATTACH docs to ./wallets and add lnaddr doc
* Fix missing rename: yupSchema -> fieldValidation
* Remove unused context
* Add documentation how to add wallets
---------
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-07-20 22:51:46 +00:00
|
|
|
initial, validate, schema, onSubmit, children, initialError, validateImmediately,
|
2024-09-03 14:15:04 +00:00
|
|
|
storageKeyPrefix, validateOnChange = true, requireSession, innerRef, enableReinitialize,
|
2024-07-01 17:02:29 +00:00
|
|
|
...props
|
2021-04-14 00:57:32 +00:00
|
|
|
}) {
|
2023-08-25 23:21:51 +00:00
|
|
|
const toaster = useToast()
|
2023-09-24 01:14:49 +00:00
|
|
|
const initialErrorToasted = useRef(false)
|
2024-09-12 18:05:11 +00:00
|
|
|
const { me } = useMe()
|
2024-05-28 17:18:54 +00:00
|
|
|
|
2023-08-25 23:21:51 +00:00
|
|
|
useEffect(() => {
|
2023-10-06 20:04:50 +00:00
|
|
|
if (initialError && !initialErrorToasted.current) {
|
2024-01-26 00:04:56 +00:00
|
|
|
toaster.danger('form error: ' + initialError.message || initialError.toString?.())
|
2023-09-24 01:14:49 +00:00
|
|
|
initialErrorToasted.current = true
|
2023-08-25 23:21:51 +00:00
|
|
|
}
|
|
|
|
}, [])
|
2021-04-14 00:57:32 +00:00
|
|
|
|
2023-10-12 17:46:22 +00:00
|
|
|
const clearLocalStorage = useCallback((values) => {
|
2023-08-31 15:15:52 +00:00
|
|
|
Object.keys(values).forEach(v => {
|
|
|
|
window.localStorage.removeItem(storageKeyPrefix + '-' + v)
|
|
|
|
if (Array.isArray(values[v])) {
|
|
|
|
values[v].forEach(
|
|
|
|
(iv, i) => {
|
|
|
|
Object.keys(iv).forEach(k => {
|
|
|
|
window.localStorage.removeItem(`${storageKeyPrefix}-${v}[${i}].${k}`)
|
|
|
|
})
|
|
|
|
window.localStorage.removeItem(`${storageKeyPrefix}-${v}[${i}]`)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
2023-10-12 17:46:22 +00:00
|
|
|
}, [storageKeyPrefix])
|
2023-08-31 15:15:52 +00:00
|
|
|
|
2024-05-28 17:18:54 +00:00
|
|
|
const onSubmitInner = useCallback(async ({ amount, ...values }, ...args) => {
|
|
|
|
const variables = { amount, ...values }
|
2024-07-01 17:02:29 +00:00
|
|
|
if (requireSession && !me) {
|
|
|
|
throw new SessionRequiredError()
|
|
|
|
}
|
|
|
|
|
2023-10-12 17:46:22 +00:00
|
|
|
try {
|
|
|
|
if (onSubmit) {
|
2024-07-01 17:02:29 +00:00
|
|
|
await onSubmit(variables, ...args)
|
2023-10-12 17:46:22 +00:00
|
|
|
}
|
|
|
|
} catch (err) {
|
2024-07-01 17:02:29 +00:00
|
|
|
console.log(err.message, err)
|
|
|
|
toaster.danger(err.message ?? err.toString?.())
|
|
|
|
return
|
2023-10-12 17:46:22 +00:00
|
|
|
}
|
2024-07-01 17:02:29 +00:00
|
|
|
|
|
|
|
if (!storageKeyPrefix) return
|
|
|
|
clearLocalStorage(values)
|
|
|
|
}, [me, onSubmit, clearLocalStorage, storageKeyPrefix])
|
2023-10-12 17:46:22 +00:00
|
|
|
|
2021-04-14 00:57:32 +00:00
|
|
|
return (
|
|
|
|
<Formik
|
|
|
|
initialValues={initial}
|
2024-09-03 14:15:04 +00:00
|
|
|
enableReinitialize={enableReinitialize}
|
2023-08-09 22:06:22 +00:00
|
|
|
validateOnChange={validateOnChange}
|
Wallet definitions with uniform interface (#1243)
* wip: Use uniform interface for wallets
* Fix import error
* Update wallet logging + other stuff
* add canPay and canSend to wallet definition
* rename 'default payment method' to 'enabled' and add enable + disable method
* Set canPay, canReceive in useWallet
* Enable wallet if just configured
* Don't pass logger to sendPayment
* Add logging to attach & detach
* Add schema to wallet def
* Add NWC wallet
* Fix unused isDefault saved in config
* Fix enableWallet
* wrong storage key was used
* broke if wallets with no configs existed
* Run validation during save
* Use INFO level for 'wallet disabled' message
* Pass config with spread operator
* Support help, optional, hint in wallet fields
* wip: Add LNC
* Fix 20s page load for /settings/wallets.json?nodata=true
For some reason, if nodata is passed (which is the case if going back), the page takes 20s to load.
* Fix extremely slow page load for LNC import
I noticed that the combination of
```
import { Form, PasswordInput, SubmitButton } from '@/components/form'
```
in components/wallet/lnc.js and the dynamic import via `await import` in components/wallet/index.js caused extremely slow page loads.
* Use normal imports
* Revert "Fix 20s page load for /settings/wallets.json?nodata=true"
This reverts commit deb476b3a966569fefcfdf4082d6b64f90fbd0a2.
Not using the dynamic import for LNC fixed the slow page load with ?nodata=true.
* Remove follow and show recent logs first
* Fix position of log start marker
* Add FIXMEs for LNC
I can't get LNC to connect. It just hangs forever on lnc.connect(). See FIXMEs.
* Remove logger.error since already handled in useWallet
* Don't require destructuring to pass props to input
* wip: Add LND autowithdrawals
* receiving wallets need to export 'server' object field
* don't print macaroon error stack
* fix missing wallet logs order update
* mark autowithdrawl settings as required
* fix server wallet logs deletion
* remove canPay and canReceive since it was confusing where it is available
TODO
* also use numeric priority for sending wallets to be consistent with how status for receiving wallets is determined
* define createInvoice function in wallet definition
* consistent wallet logs: sending wallets use 'wallet attached'+'wallet enabled/disabled' whereas receiving wallets use 'wallet created/updated'
* see FIXMEs
* Fix TypeError
* Fix sendPayment called with empty config
* removed useEffect such that config is available on first render
* fix hydration error using dynamic import without SSR
* Fix confusing UX around enabled
* Remove FIXMEs
Rebase on master seemed to have fixed these, weird
* Use same error format in toast and wallet log
* Fix usage of conditional hooks in useConfig
* Fix isConfigured
* Fix delete wallet logs on server
* Fix wallet logs refetch
onError does not exist on client.mutate
* Fix TypeError in isConfigured if no enabled wallet found
* Only include local/server config if required
* Fix another hydration error
* Fix server config not updated after save or detach
* Also use 'enabled' for server wallets
* Fix wallet logs not updated after server delete
* Consistent logs between local and server wallets
* 'wallet attached' on create
* 'wallet updated' on config updates
* 'wallet enabled' and 'wallet disabled' if checkbox changed
* 'wallet detached' on delete
* Also enable server wallets on create
* Disable checkbox if not configured yet
* Move all validation schema into lib/validate
* Implement drag & drop w/o persistence
* Use dynamic import for WalletCard
This fixes a lot of issues with hydration
* Save order as priority
* Fix autowithdrawSettings not applied
Form requires config in flat format but mutation requires autowithdraw settings in a separate 'settings' field.
I have decided that config will be in flat form format. It will be transformed into mutation format during save.
* Save dedicated enabled flag for server wallets
* wallet table now contains boolean column 'enabled'
* 'priority' is now a number everywhere
* use consistent order between how autowithdrawals are attempted and server wallets cards
* Fix onCanceled missing
* Fix typo
* Fix noisy changes in lib/validate
I moved the schema for lnbits, nwc and lnc out of lib/validate only to put them back in there later.
This commit should make the changeset cleaner by removing noise.
* Split arguments into [value,] config, context
* Run lnbits url.replace in validate and sendPayment
* Remove unnecessary WALLETS_QUERY
* Generate wallet mutation from fields
* Generate wallet resolver from fields
* Fix import inconsistency between app and worker
* Use wallet.createInvoice for autowithdrawals
* Fix success autowithdrawal log
* Fix wallet security banner shown for server wallets
* Add autowithdrawal to lightning address
* Add optional wallet short name for logging
* Fix draggable
* Fix autowithdraw loop
* Add missing hints
* Add CLN autowithdrawal
* Detach wallets and delete logs on logout
* Remove Wallet in lib/constants
* Use inject function for resolvers and typeDefs
* Fix priority ignored when fetching enabled wallet
* Fix draggable false on first page load due to SSR
* Use touches instead of dnd on mobile
Browsers don't support drag events for touch devices.
To have a consistent implementation for desktop and mobile, we would need to use mousedown/touchstart, mouseup/touchend and mousemove/touchmove.
For now, this commit makes changing the order possible on touch devices with simple touches.
* Fix duplicate CLN error
* Fix autowithdraw priority order
* Fix error per invalid bip39 word
* Update LNC code
* remove LNC FIXMEs
Mhh, I guess the TURN server was down or something? It now magically works. Or maybe it only works once per mnemonic?
* also removed the lnc.lnd.lightning.getInfo() call since we don't ask and need permission for this RPC for payments.
* setting a password does not work though. It fails with 'The password provided is not valid' which is triggered at https://github.com/lightninglabs/lnc-web/blob/main/lib/util/credentialStore.ts#L81.
* Fix order if wallet with no priority exists
* Use common sort
* Add link to lnbits.com
* Add example wallet def
* Remove TODOs
TODO in components/wallet-logger.js was handled.
I don't see a need for the TODO in lib/wallet.js anymore. This function will only be called with the wallet of type LIGHTNING_ADDRESS anyway.
* Remove console.log
* Toast priority save errors
* Fix leaking relay connections
* Remove 'tor or clearnet' hint for LN addresses
* Remove React dependency from wallet definitions
* Generate resolver name from walletField
* Move wallets into top level directory wallet/
* Put wallets into own folder
* Fix generateMutation
* remove resolverName property from wallet defs
* move function into lib/wallet
* use function in generateMutation on client to fix wrongly generated mutation
* Separate client and server imports by files
* wallets now consist of an index.js, a client.js and a server.js file
* client.js is imported on the client and contains the client portion
* server.js is imported on the server and contains the server porition
* both reexport index.js so everything in index.js can be shared by client and server
* every wallet contains a client.js file since they are all imported on the client to show the cards
* client.js of every wallet is reexported as an array in wallets/client.js
* server.js of every wallet is reexported as an array in wallets/server.js
FIXME: for some reason, worker does not properly import the default export of wallets/server.js
* Fix worker import of wallets/server
* Fix wallet.server usage
* I removed wallet.server in a previous commit
* the client couldn't determine which wallet was stored on the server since all server specific fields were set in server.js
* walletType and walletField are now set in index.js
* walletType is now used to determine if a wallet is stored on the server
* also included some formatting changes
* Fix w.default usage
Since package.json with { "type": "module" } was added, this is no longer needed.
* Fix id access in walletPrioritySort
* Fix autowithdrawal error log
* Generate validation schema for LNbits
* Generate validation schema for NWC
* Rename to torAllowed
* Generate validation schema for LNC
* Generate validation schema for LND
* Generate validation schema for LnAddr
* Remove stringTypes
* Generate validation schema for CLN
* Make clear that message belongs to test
* validate.message was used in tandem with validate.test
* it might be confused as the message if the validation for validate.type failed
* now validate.test can be a function or an object of { test, message } shape which matches Yup.test
* Remove validate.schema as a trap door
* make lnc work
* Return null if no wallet was found
* Revert code around schema generation
* Transform autowithdrawSchemaMembers into an object
* Rename schema to yupSchema
* Fix missing required for LNbits adminKey
* Support formik form-level validation
* Fix missing addWalletLog import
* Fix missing space after =
* fix merge conflict resolution mistake
* remove non-custodial* badges
* create guides for attaching wallets in sndev
* Use built-in formik validation or Yup schema but not both
* Rename: validate -> testConnectClient, testConnect -> testConnectServer
* make lnaddr autowithdraw work in dev
* move ATTACH docs to ./wallets and add lnaddr doc
* Fix missing rename: yupSchema -> fieldValidation
* Remove unused context
* Add documentation how to add wallets
---------
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-07-20 22:51:46 +00:00
|
|
|
validate={validate}
|
2021-04-14 00:57:32 +00:00
|
|
|
validationSchema={schema}
|
2021-05-25 00:08:56 +00:00
|
|
|
initialTouched={validateImmediately && initial}
|
|
|
|
validateOnBlur={false}
|
2023-10-12 17:46:22 +00:00
|
|
|
onSubmit={onSubmitInner}
|
2023-10-04 01:12:12 +00:00
|
|
|
innerRef={innerRef}
|
2021-04-14 00:57:32 +00:00
|
|
|
>
|
|
|
|
<FormikForm {...props} noValidate>
|
2023-05-11 19:34:42 +00:00
|
|
|
<StorageKeyPrefixContext.Provider value={storageKeyPrefix}>
|
2021-04-24 21:05:07 +00:00
|
|
|
{children}
|
2023-05-11 19:34:42 +00:00
|
|
|
</StorageKeyPrefixContext.Provider>
|
|
|
|
</FormikForm>
|
2021-04-24 21:05:07 +00:00
|
|
|
</Formik>
|
|
|
|
)
|
|
|
|
}
|
2022-09-18 01:45:21 +00:00
|
|
|
|
2024-01-09 01:02:00 +00:00
|
|
|
export function Select ({ label, items, info, groupClassName, onChange, noForm, overrideValue, hint, ...props }) {
|
2023-07-23 15:08:43 +00:00
|
|
|
const [field, meta, helpers] = noForm ? [{}, {}, {}] : useField(props)
|
2022-10-20 22:44:44 +00:00
|
|
|
const formik = noForm ? null : useFormikContext()
|
2023-05-11 00:26:07 +00:00
|
|
|
const invalid = meta.touched && meta.error
|
2023-07-23 15:08:43 +00:00
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (overrideValue) {
|
|
|
|
helpers.setValue(overrideValue)
|
|
|
|
}
|
|
|
|
}, [overrideValue])
|
|
|
|
|
2022-09-18 01:45:21 +00:00
|
|
|
return (
|
|
|
|
<FormGroup label={label} className={groupClassName}>
|
2024-01-09 01:02:00 +00:00
|
|
|
<span className='d-flex align-items-center'>
|
|
|
|
<BootstrapForm.Select
|
|
|
|
{...field} {...props}
|
|
|
|
onChange={(e) => {
|
|
|
|
if (field?.onChange) {
|
|
|
|
field.onChange(e)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (onChange) {
|
|
|
|
onChange(formik, e)
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
isInvalid={invalid}
|
|
|
|
>
|
|
|
|
{items.map(item => {
|
|
|
|
if (item && typeof item === 'object') {
|
|
|
|
return (
|
|
|
|
<optgroup key={item.label} label={item.label}>
|
|
|
|
{item.items.map(item => <option key={item}>{item}</option>)}
|
|
|
|
</optgroup>
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
return <option key={item}>{item}</option>
|
|
|
|
}
|
|
|
|
})}
|
|
|
|
</BootstrapForm.Select>
|
|
|
|
{info && <Info>{info}</Info>}
|
|
|
|
</span>
|
2023-05-11 00:26:07 +00:00
|
|
|
<BootstrapForm.Control.Feedback type='invalid'>
|
|
|
|
{meta.touched && meta.error}
|
|
|
|
</BootstrapForm.Control.Feedback>
|
2023-12-23 20:26:16 +00:00
|
|
|
{hint &&
|
|
|
|
<BootstrapForm.Text>
|
|
|
|
{hint}
|
|
|
|
</BootstrapForm.Text>}
|
2022-09-18 01:45:21 +00:00
|
|
|
</FormGroup>
|
|
|
|
)
|
2022-10-04 21:21:42 +00:00
|
|
|
}
|
2023-10-04 19:44:06 +00:00
|
|
|
|
2023-11-09 00:15:36 +00:00
|
|
|
export function DatePicker ({ fromName, toName, noForm, onChange, when, from, to, className, ...props }) {
|
2023-10-04 19:44:06 +00:00
|
|
|
const formik = noForm ? null : useFormikContext()
|
|
|
|
const [,, fromHelpers] = noForm ? [{}, {}, {}] : useField({ ...props, name: fromName })
|
|
|
|
const [,, toHelpers] = noForm ? [{}, {}, {}] : useField({ ...props, name: toName })
|
2023-11-09 00:15:36 +00:00
|
|
|
const { minDate, maxDate } = props
|
|
|
|
|
2023-11-14 16:23:44 +00:00
|
|
|
const [[innerFrom, innerTo], setRange] = useState(whenRange(when, from, to))
|
2023-10-04 19:44:06 +00:00
|
|
|
|
|
|
|
useEffect(() => {
|
2023-11-14 16:23:44 +00:00
|
|
|
setRange(whenRange(when, from, to))
|
2023-11-09 00:15:36 +00:00
|
|
|
if (!noForm) {
|
2024-01-19 21:30:36 +00:00
|
|
|
fromHelpers.setValue(from)
|
|
|
|
toHelpers.setValue(to)
|
2023-11-09 00:15:36 +00:00
|
|
|
}
|
|
|
|
}, [when, from, to])
|
|
|
|
|
|
|
|
const dateFormat = useMemo(() => {
|
|
|
|
const now = new Date(2013, 11, 31)
|
|
|
|
let str = now.toLocaleDateString()
|
|
|
|
str = str.replace('31', 'dd')
|
|
|
|
str = str.replace('12', 'MM')
|
|
|
|
str = str.replace('2013', 'yy')
|
|
|
|
return str
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
const innerOnChange = ([from, to], e) => {
|
2024-01-20 02:38:35 +00:00
|
|
|
if (from) {
|
|
|
|
from = new Date(new Date(from).setHours(0, 0, 0, 0))
|
|
|
|
}
|
|
|
|
if (to) {
|
|
|
|
to = new Date(new Date(to).setHours(23, 59, 59, 999))
|
|
|
|
}
|
2024-01-19 21:14:52 +00:00
|
|
|
setRange([from, to])
|
2023-11-09 00:15:36 +00:00
|
|
|
if (!noForm) {
|
2023-10-04 19:44:06 +00:00
|
|
|
fromHelpers.setValue(from)
|
|
|
|
toHelpers.setValue(to)
|
|
|
|
}
|
2024-01-19 21:14:52 +00:00
|
|
|
if (!from || !to) return
|
|
|
|
onChange?.(formik, [from, to], e)
|
2023-11-09 00:15:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const onChangeRawHandler = (e) => {
|
|
|
|
// raw user data can be incomplete while typing, so quietly bail on exceptions
|
|
|
|
try {
|
|
|
|
const dateStrings = e.target.value.split('-', 2)
|
|
|
|
const dates = dateStrings.map(s => new Date(s))
|
|
|
|
let [from, to] = dates
|
|
|
|
if (from) {
|
2024-01-20 02:38:35 +00:00
|
|
|
from = new Date(from.setHours(0, 0, 0, 0))
|
2023-11-14 16:23:44 +00:00
|
|
|
if (minDate) from = new Date(Math.max(from.getTime(), minDate.getTime()))
|
2023-11-09 00:15:36 +00:00
|
|
|
try {
|
2024-01-20 02:38:35 +00:00
|
|
|
if (to) {
|
|
|
|
to = new Date(to.setHours(23, 59, 59, 999))
|
|
|
|
if (maxDate) to = new Date(Math.min(to.getTime(), maxDate.getTime()))
|
|
|
|
}
|
2023-11-09 00:15:36 +00:00
|
|
|
|
|
|
|
// if end date isn't valid, set it to the start date
|
2024-01-20 02:38:35 +00:00
|
|
|
if (!(to instanceof Date && !isNaN(to)) || to < from) to = new Date(from.setHours(23, 59, 59, 999))
|
2023-11-09 00:15:36 +00:00
|
|
|
} catch {
|
2024-01-20 02:38:35 +00:00
|
|
|
to = new Date(from.setHours(23, 59, 59, 999))
|
2023-11-09 00:15:36 +00:00
|
|
|
}
|
|
|
|
innerOnChange([from, to], e)
|
|
|
|
}
|
|
|
|
} catch { }
|
|
|
|
}
|
2023-10-04 19:44:06 +00:00
|
|
|
|
|
|
|
return (
|
|
|
|
<ReactDatePicker
|
2023-11-09 00:15:36 +00:00
|
|
|
className={`form-control text-center ${className}`}
|
|
|
|
selectsRange
|
|
|
|
maxDate={new Date()}
|
|
|
|
minDate={new Date('2021-05-01')}
|
2023-10-04 19:44:06 +00:00
|
|
|
{...props}
|
2023-11-14 16:23:44 +00:00
|
|
|
selected={new Date(innerFrom)}
|
|
|
|
startDate={new Date(innerFrom)}
|
|
|
|
endDate={innerTo ? new Date(innerTo) : undefined}
|
2023-11-09 00:15:36 +00:00
|
|
|
dateFormat={dateFormat}
|
|
|
|
onChangeRaw={onChangeRawHandler}
|
|
|
|
onChange={innerOnChange}
|
2023-10-04 19:44:06 +00:00
|
|
|
/>
|
|
|
|
)
|
|
|
|
}
|
2024-02-08 18:33:13 +00:00
|
|
|
|
2024-02-21 18:18:43 +00:00
|
|
|
export function DateTimeInput ({ label, groupClassName, name, ...props }) {
|
|
|
|
const [, meta] = useField({ ...props, name })
|
|
|
|
return (
|
|
|
|
<FormGroup label={label} className={groupClassName}>
|
|
|
|
<div>
|
|
|
|
<DateTimePicker name={name} {...props} />
|
|
|
|
<BootstrapForm.Control.Feedback type='invalid' className='d-block'>
|
|
|
|
{meta.error}
|
|
|
|
</BootstrapForm.Control.Feedback>
|
|
|
|
</div>
|
|
|
|
</FormGroup>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
function DateTimePicker ({ name, className, ...props }) {
|
|
|
|
const [field, , helpers] = useField({ ...props, name })
|
|
|
|
return (
|
|
|
|
<ReactDatePicker
|
|
|
|
{...field}
|
|
|
|
{...props}
|
|
|
|
showTimeSelect
|
|
|
|
dateFormat='Pp'
|
|
|
|
className={`form-control ${className}`}
|
|
|
|
selected={(field.value && new Date(field.value)) || null}
|
|
|
|
value={(field.value && new Date(field.value)) || null}
|
|
|
|
onChange={(val) => {
|
|
|
|
helpers.setValue(val)
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2024-02-08 18:33:13 +00:00
|
|
|
function Client (Component) {
|
|
|
|
return ({ initialValue, ...props }) => {
|
|
|
|
// This component can be used for Formik fields
|
|
|
|
// where the initial value is not available on first render.
|
|
|
|
// Example: value is stored in localStorage which is fetched
|
|
|
|
// after first render using an useEffect hook.
|
|
|
|
const [,, helpers] = useField(props)
|
|
|
|
|
|
|
|
useEffect(() => {
|
2024-04-27 02:22:30 +00:00
|
|
|
initialValue && helpers.setValue(initialValue)
|
2024-02-08 18:33:13 +00:00
|
|
|
}, [initialValue])
|
|
|
|
|
|
|
|
return <Component {...props} />
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-21 22:28:57 +00:00
|
|
|
function PasswordHider ({ onClick, showPass }) {
|
|
|
|
return (
|
|
|
|
<InputGroup.Text
|
|
|
|
style={{ cursor: 'pointer' }}
|
|
|
|
onClick={onClick}
|
|
|
|
>
|
|
|
|
{!showPass
|
2024-04-23 14:46:27 +00:00
|
|
|
? <Eye
|
2024-10-04 20:00:13 +00:00
|
|
|
fill='var(--bs-body-color)' height={20} width={20}
|
2024-04-21 22:28:57 +00:00
|
|
|
/>
|
2024-04-23 14:46:27 +00:00
|
|
|
: <EyeClose
|
2024-10-04 20:00:13 +00:00
|
|
|
fill='var(--bs-body-color)' height={20} width={20}
|
2024-04-21 22:28:57 +00:00
|
|
|
/>}
|
|
|
|
</InputGroup.Text>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2024-10-04 20:00:13 +00:00
|
|
|
export function PasswordInput ({ newPass, ...props }) {
|
2024-04-21 22:28:57 +00:00
|
|
|
const [showPass, setShowPass] = useState(false)
|
|
|
|
|
|
|
|
return (
|
|
|
|
<ClientInput
|
|
|
|
{...props}
|
|
|
|
type={showPass ? 'text' : 'password'}
|
|
|
|
autoComplete={newPass ? 'new-password' : 'current-password'}
|
2024-10-04 20:00:13 +00:00
|
|
|
append={<PasswordHider showPass={showPass} onClick={() => setShowPass(!showPass)} />}
|
2024-04-21 22:28:57 +00:00
|
|
|
/>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2024-02-08 18:33:13 +00:00
|
|
|
export const ClientInput = Client(Input)
|
|
|
|
export const ClientCheckbox = Client(Checkbox)
|