feat(notes): add try again popup for submit and validation
This commit is contained in:
parent
be9488a752
commit
53dd3cc193
@ -2,12 +2,19 @@ import { NDKSubscriptionCacheUsage } from '@nostr-dev-kit/ndk'
|
|||||||
import { FALLBACK_PROFILE_IMAGE } from '../../constants'
|
import { FALLBACK_PROFILE_IMAGE } from '../../constants'
|
||||||
import { useAppSelector, useLocalCache } from 'hooks'
|
import { useAppSelector, useLocalCache } from 'hooks'
|
||||||
import { useProfile } from 'hooks/useProfile'
|
import { useProfile } from 'hooks/useProfile'
|
||||||
import { Navigate, useNavigation, useSubmit } from 'react-router-dom'
|
import {
|
||||||
|
Navigate,
|
||||||
|
useActionData,
|
||||||
|
useNavigation,
|
||||||
|
useSubmit
|
||||||
|
} from 'react-router-dom'
|
||||||
import { appRoutes } from 'routes'
|
import { appRoutes } from 'routes'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { adjustTextareaHeight, NOTE_DRAFT_CACHE_KEY } from 'utils'
|
import { adjustTextareaHeight, NOTE_DRAFT_CACHE_KEY } from 'utils'
|
||||||
import { NotePreview } from './NotePreview'
|
import { NotePreview } from './NotePreview'
|
||||||
import { NoteSubmitForm } from 'types'
|
import { NoteSubmitActionResult, NoteSubmitForm } from 'types'
|
||||||
|
import { InputError } from 'components/Inputs/Error'
|
||||||
|
import { AlertPopup } from 'components/AlertPopup'
|
||||||
|
|
||||||
interface NoteSubmitProps {
|
interface NoteSubmitProps {
|
||||||
initialContent?: string | undefined
|
initialContent?: string | undefined
|
||||||
@ -27,10 +34,18 @@ export const NoteSubmit = ({
|
|||||||
const [content, setContent] = useState(initialContent ?? cache?.content ?? '')
|
const [content, setContent] = useState(initialContent ?? cache?.content ?? '')
|
||||||
const [nsfw, setNsfw] = useState(cache?.nsfw ?? false)
|
const [nsfw, setNsfw] = useState(cache?.nsfw ?? false)
|
||||||
const [showPreview, setShowPreview] = useState(!!initialContent)
|
const [showPreview, setShowPreview] = useState(!!initialContent)
|
||||||
const image = profile?.image || FALLBACK_PROFILE_IMAGE
|
const image = useMemo(
|
||||||
|
() => profile?.image || FALLBACK_PROFILE_IMAGE,
|
||||||
|
[profile?.image]
|
||||||
|
)
|
||||||
const ref = useRef<HTMLTextAreaElement>(null)
|
const ref = useRef<HTMLTextAreaElement>(null)
|
||||||
const submit = useSubmit()
|
const submit = useSubmit()
|
||||||
|
const actionData = useActionData() as NoteSubmitActionResult
|
||||||
|
const formErrors = useMemo(
|
||||||
|
() =>
|
||||||
|
actionData?.type === 'validation' ? actionData.formErrors : undefined,
|
||||||
|
[actionData]
|
||||||
|
)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ref.current && (!!initialContent || !!cache?.content)) {
|
if (ref.current && (!!initialContent || !!cache?.content)) {
|
||||||
adjustTextareaHeight(ref.current)
|
adjustTextareaHeight(ref.current)
|
||||||
@ -45,15 +60,19 @@ export const NoteSubmit = ({
|
|||||||
})
|
})
|
||||||
}, [content, nsfw, setCache])
|
}, [content, nsfw, setCache])
|
||||||
|
|
||||||
const handleContentChange = (
|
const [showTryAgainPopup, setShowTryAgainPopup] = useState<boolean>(false)
|
||||||
event: React.ChangeEvent<HTMLTextAreaElement>
|
useEffect(() => {
|
||||||
) => {
|
const isTimeout = actionData?.type === 'timeout'
|
||||||
setContent(event.currentTarget.value)
|
setShowTryAgainPopup(isTimeout)
|
||||||
adjustTextareaHeight(event.currentTarget)
|
if (isTimeout && actionData.action.intent === 'submit') {
|
||||||
|
setContent(actionData.action.data.content)
|
||||||
|
setNsfw(actionData.action.data.nsfw)
|
||||||
}
|
}
|
||||||
|
}, [actionData])
|
||||||
|
|
||||||
const handleFormSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
const handleFormSubmit = useCallback(
|
||||||
event.preventDefault()
|
async (event?: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event?.preventDefault()
|
||||||
const formSubmit = {
|
const formSubmit = {
|
||||||
intent: 'submit',
|
intent: 'submit',
|
||||||
data: {
|
data: {
|
||||||
@ -73,6 +92,30 @@ export const NoteSubmit = ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
typeof handleClose === 'function' && handleClose()
|
typeof handleClose === 'function' && handleClose()
|
||||||
|
},
|
||||||
|
[content, handleClose, nsfw, submit]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleTryAgainConfirm = useCallback(
|
||||||
|
(confirm: boolean) => {
|
||||||
|
setShowTryAgainPopup(false)
|
||||||
|
|
||||||
|
// Cancel if not confirmed
|
||||||
|
if (!confirm) return
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
setContent('')
|
||||||
|
setNsfw(false)
|
||||||
|
|
||||||
|
handleFormSubmit()
|
||||||
|
},
|
||||||
|
[handleFormSubmit]
|
||||||
|
)
|
||||||
|
const handleContentChange = (
|
||||||
|
event: React.ChangeEvent<HTMLTextAreaElement>
|
||||||
|
) => {
|
||||||
|
setContent(event.currentTarget.value)
|
||||||
|
adjustTextareaHeight(event.currentTarget)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePreviewToggle = () => {
|
const handlePreviewToggle = () => {
|
||||||
@ -151,13 +194,24 @@ export const NoteSubmit = ({
|
|||||||
className='btn btnMain'
|
className='btn btnMain'
|
||||||
type='submit'
|
type='submit'
|
||||||
style={{ padding: '5px 20px', borderRadius: '8px' }}
|
style={{ padding: '5px 20px', borderRadius: '8px' }}
|
||||||
disabled={navigation.state !== 'idle'}
|
disabled={navigation.state !== 'idle' || !content.length}
|
||||||
>
|
>
|
||||||
{navigation.state === 'submitting' ? 'Posting...' : 'Post'}
|
{navigation.state === 'submitting' ? 'Posting...' : 'Post'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{typeof formErrors?.content !== 'undefined' && (
|
||||||
|
<InputError message={formErrors?.content} />
|
||||||
|
)}
|
||||||
{showPreview && <NotePreview content={content} />}
|
{showPreview && <NotePreview content={content} />}
|
||||||
|
{showTryAgainPopup && (
|
||||||
|
<AlertPopup
|
||||||
|
handleConfirm={handleTryAgainConfirm}
|
||||||
|
handleClose={() => setShowTryAgainPopup(false)}
|
||||||
|
header={'Post'}
|
||||||
|
label={`Posting timed out. Do you want to try again?`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</>
|
</>
|
||||||
|
@ -129,6 +129,11 @@ export const FeedTabPosts = () => {
|
|||||||
return _notes
|
return _notes
|
||||||
}, [filterOptions.repost, notes, showing])
|
}, [filterOptions.repost, notes, showing])
|
||||||
|
|
||||||
|
const newNotes = useMemo(
|
||||||
|
() => discoveredNotes.filter((d) => !notes.some((n) => n.id === d.id)),
|
||||||
|
[discoveredNotes, notes]
|
||||||
|
)
|
||||||
|
|
||||||
if (!userPubkey) return null
|
if (!userPubkey) return null
|
||||||
|
|
||||||
const handleLoadMore = () => {
|
const handleLoadMore = () => {
|
||||||
@ -170,15 +175,15 @@ export const FeedTabPosts = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const discoveredCount = discoveredNotes.length
|
const discoveredCount = newNotes.length
|
||||||
const handleDiscoveredClick = () => {
|
const handleDiscoveredClick = () => {
|
||||||
// Combine newly discovred with the notes
|
// Combine newly discovred with the notes
|
||||||
// Skip events already in notes
|
// Skip events already in notes
|
||||||
setNotes((prev) => {
|
setNotes((prev) => {
|
||||||
return Array.from(new Set([...discoveredNotes, ...prev]))
|
return [...newNotes, ...prev]
|
||||||
})
|
})
|
||||||
// Increase showing by the discovered count
|
// Increase showing by the discovered count
|
||||||
setShowing((prev) => prev + discoveredNotes.length)
|
setShowing((prev) => prev + discoveredCount)
|
||||||
setDiscoveredNotes([])
|
setDiscoveredNotes([])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,13 +4,19 @@ import { ActionFunctionArgs, redirect } from 'react-router-dom'
|
|||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { getFeedNotePageRoute } from 'routes'
|
import { getFeedNotePageRoute } from 'routes'
|
||||||
import { store } from 'store'
|
import { store } from 'store'
|
||||||
import { NoteAction, NoteSubmitForm, NoteSubmitFormErrors } from 'types'
|
import {
|
||||||
|
NoteAction,
|
||||||
|
NoteSubmitForm,
|
||||||
|
NoteSubmitFormErrors,
|
||||||
|
TimeoutError
|
||||||
|
} from 'types'
|
||||||
import {
|
import {
|
||||||
log,
|
log,
|
||||||
LogType,
|
LogType,
|
||||||
NOTE_DRAFT_CACHE_KEY,
|
NOTE_DRAFT_CACHE_KEY,
|
||||||
now,
|
now,
|
||||||
removeLocalStorageItem
|
removeLocalStorageItem,
|
||||||
|
timeout
|
||||||
} from 'utils'
|
} from 'utils'
|
||||||
|
|
||||||
export const feedPostRouteAction =
|
export const feedPostRouteAction =
|
||||||
@ -38,8 +44,9 @@ export const feedPostRouteAction =
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let action: NoteAction | undefined
|
||||||
try {
|
try {
|
||||||
const action = (await request.json()) as NoteAction
|
action = (await request.json()) as NoteAction
|
||||||
switch (action.intent) {
|
switch (action.intent) {
|
||||||
case 'submit':
|
case 'submit':
|
||||||
return await handleActionSubmit(
|
return await handleActionSubmit(
|
||||||
@ -59,6 +66,14 @@ export const feedPostRouteAction =
|
|||||||
throw new Error('Unsupported feed action. Intent missing.')
|
throw new Error('Unsupported feed action. Intent missing.')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (action && error instanceof TimeoutError) {
|
||||||
|
log(true, LogType.Error, 'Failed to publish note. Try again initiated')
|
||||||
|
const result = {
|
||||||
|
type: 'timeout',
|
||||||
|
action
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
log(true, LogType.Error, 'Failed to publish note', error)
|
log(true, LogType.Error, 'Failed to publish note', error)
|
||||||
toast.error('Failed to publish note')
|
toast.error('Failed to publish note')
|
||||||
return null
|
return null
|
||||||
@ -82,7 +97,11 @@ async function handleActionSubmit(
|
|||||||
) {
|
) {
|
||||||
const formErrors = validateFormData(data)
|
const formErrors = validateFormData(data)
|
||||||
|
|
||||||
if (Object.keys(formErrors).length) return formErrors
|
if (Object.keys(formErrors).length)
|
||||||
|
return {
|
||||||
|
type: 'validation',
|
||||||
|
formErrors
|
||||||
|
}
|
||||||
|
|
||||||
const content = decodeURIComponent(data.content!)
|
const content = decodeURIComponent(data.content!)
|
||||||
const currentTimeStamp = now()
|
const currentTimeStamp = now()
|
||||||
@ -98,12 +117,14 @@ async function handleActionSubmit(
|
|||||||
pubkey
|
pubkey
|
||||||
})
|
})
|
||||||
|
|
||||||
try {
|
|
||||||
if (data.nsfw) ndkEvent.tags.push(['L', 'content-warning'])
|
if (data.nsfw) ndkEvent.tags.push(['L', 'content-warning'])
|
||||||
|
|
||||||
await ndkEvent.sign()
|
await ndkEvent.sign()
|
||||||
const note1 = ndkEvent.encode()
|
const note1 = ndkEvent.encode()
|
||||||
const publishedOnRelays = await ndkEvent.publish()
|
const publishedOnRelays = await Promise.race([
|
||||||
|
ndkEvent.publish(),
|
||||||
|
timeout(30000)
|
||||||
|
])
|
||||||
if (publishedOnRelays.size === 0) {
|
if (publishedOnRelays.size === 0) {
|
||||||
toast.error('Failed to publish note on any relay')
|
toast.error('Failed to publish note on any relay')
|
||||||
return null
|
return null
|
||||||
@ -112,11 +133,6 @@ async function handleActionSubmit(
|
|||||||
removeLocalStorageItem(NOTE_DRAFT_CACHE_KEY)
|
removeLocalStorageItem(NOTE_DRAFT_CACHE_KEY)
|
||||||
return redirect(getFeedNotePageRoute(note1))
|
return redirect(getFeedNotePageRoute(note1))
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
log(true, LogType.Error, 'Failed to publish note', error)
|
|
||||||
toast.error('Failed to publish note')
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
async function handleActionRepost(ndk: NDK, data: NostrEvent, note1: string) {
|
async function handleActionRepost(ndk: NDK, data: NostrEvent, note1: string) {
|
||||||
const ndkEvent = new NDKEvent(ndk, data)
|
const ndkEvent = new NDKEvent(ndk, data)
|
||||||
|
@ -17,3 +17,13 @@ export type NoteAction =
|
|||||||
note1: string
|
note1: string
|
||||||
data: NostrEvent
|
data: NostrEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NoteSubmitActionResult =
|
||||||
|
| {
|
||||||
|
type: 'timeout'
|
||||||
|
action: NoteAction
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'validation'
|
||||||
|
formErrors: NoteSubmitFormErrors
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user