sigit.io/src/layouts/Main.tsx
enes 235e76be4e
All checks were successful
Release to Staging / build_and_release (push) Successful in 1m16s
fix: processing gift wraps and notifications (#193)
This change will potentially close multiple issues related to the gift-wrapped events processing (#168, #158). Further testing will be required to confirm before closing each.

The commented-out code causes the race condition during the processing of the gift wraps with sigits.

During the processing we perform checks to see if sigit is outdated. In cases where sigit includes multiple signers it's possible for a signer to receive multiple sigit updates at once (especially noticeable for 3rd, 4th signer). Due to async nature of processing we can have same sigit enter processing flow with different states. Since this code also updates user's app state, which includes uploads to the blossom server it takes time to upload local user state which causes both to check against the stale data and un-updated app state. This results in both sigits being "new" and both proceed to update user state and upload app data. We have no guarantees as in which event will update last, meaning that the final state we end up with could be already stale.

The issue is also complicated due to the fact that we mark the gift wraps as processed and it's impossible to update the state without creating a new gift wrap with correct state and processing it last to overwrite stale state.

This is temporary solution to stop broken sigit states until proper async implementation is ready.

Co-authored-by: b
Reviewed-on: #193
Reviewed-by: eugene <eugene@nostrdev.com>
Co-authored-by: enes <mulahasanovic@outlook.com>
Co-committed-by: enes <mulahasanovic@outlook.com>
2024-09-12 08:26:59 +00:00

184 lines
5.4 KiB
TypeScript

import { Event, kinds } from 'nostr-tools'
import { useEffect, useRef, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { Outlet } from 'react-router-dom'
import { AppBar } from '../components/AppBar/AppBar'
import { LoadingSpinner } from '../components/LoadingSpinner'
import { MetadataController, NostrController } from '../controllers'
import {
restoreState,
setAuthState,
setMetadataEvent,
updateUserAppData
} from '../store/actions'
import { LoginMethods } from '../store/auth/types'
import { State } from '../store/rootReducer'
import { Dispatch } from '../store/store'
import { setUserRobotImage } from '../store/userRobotImage/action'
import {
clearAuthToken,
clearState,
getRoboHashPicture,
getUsersAppData,
loadState,
saveNsecBunkerDelegatedKey,
subscribeForSigits
} from '../utils'
import { useAppSelector } from '../hooks'
import styles from './style.module.scss'
const UPDATE_INTERVAL_MS = 120000
export const MainLayout = () => {
const dispatch: Dispatch = useDispatch()
const [isLoading, setIsLoading] = useState(true)
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState(`Loading App`)
const authState = useSelector((state: State) => state.auth)
const usersAppData = useAppSelector((state) => state.userAppData)
// Ref to track if `subscribeForSigits` has been called
const hasSubscribed = useRef<number | null>(null)
useEffect(() => {
const metadataController = new MetadataController()
const logout = () => {
dispatch(
setAuthState({
keyPair: undefined,
loggedIn: false,
usersPubkey: undefined,
loginMethod: undefined,
nsecBunkerPubkey: undefined
})
)
dispatch(setMetadataEvent(metadataController.getEmptyMetadataEvent()))
// clear authToken saved in local storage
clearAuthToken()
clearState()
// update nsecBunker delegated key
const newDelegatedKey =
NostrController.getInstance().generateDelegatedKey()
saveNsecBunkerDelegatedKey(newDelegatedKey)
}
const restoredState = loadState()
if (restoredState) {
dispatch(restoreState(restoredState))
const { loggedIn, loginMethod, usersPubkey, nsecBunkerRelays } =
restoredState.auth
if (loggedIn) {
if (!loginMethod || !usersPubkey) return logout()
if (loginMethod === LoginMethods.nsecBunker) {
if (!nsecBunkerRelays) return logout()
const nostrController = NostrController.getInstance()
nostrController.nsecBunkerInit(nsecBunkerRelays).then(() => {
nostrController.createNsecBunkerSigner(usersPubkey)
})
}
const handleMetadataEvent = (event: Event) => {
dispatch(setMetadataEvent(event))
}
metadataController.on(usersPubkey, (kind: number, event: Event) => {
if (kind === kinds.Metadata) {
handleMetadataEvent(event)
}
})
metadataController.findMetadata(usersPubkey).then((metadataEvent) => {
if (metadataEvent) handleMetadataEvent(metadataEvent)
})
} else {
setIsLoading(false)
}
} else {
setIsLoading(false)
}
}, [dispatch])
useEffect(() => {
if (authState.loggedIn && usersAppData) {
const pubkey = authState.usersPubkey || authState.keyPair?.public
if (pubkey && !hasSubscribed.current) {
// Call `subscribeForSigits` only if it hasn't been called before
// #193 disabled websocket subscribtion, keep updating the sigits on UPDATE_INTERVAL_MS until #194 is done
// Set up the update sigit loop, use setTimeout to make sure times between updates are consistent
// (not affected by execution duration of subscribeForSigits call)
const loop = () => {
hasSubscribed.current = window.setTimeout(async () => {
await subscribeForSigits(pubkey)
loop()
}, UPDATE_INTERVAL_MS)
}
subscribeForSigits(pubkey)
loop()
// Mark `subscribeForSigits` as called
//hasSubscribed.current = true
}
}
return () => {
if (hasSubscribed.current) {
window.clearTimeout(hasSubscribed.current)
hasSubscribed.current = null
}
}
}, [authState, usersAppData])
/**
* When authState change user logged in / or app reloaded
* we set robohash avatar in the global state based on user npub
* so that avatar will be consistent across the app when kind 0 is empty
*/
useEffect(() => {
if (authState && authState.loggedIn) {
const pubkey = authState.usersPubkey || authState.keyPair?.public
if (pubkey) {
dispatch(setUserRobotImage(getRoboHashPicture(pubkey)))
}
setIsLoading(true)
setLoadingSpinnerDesc(`Loading SIGit history...`)
getUsersAppData()
.then((appData) => {
if (appData) {
dispatch(updateUserAppData(appData))
}
})
.finally(() => setIsLoading(false))
}
}, [authState, dispatch])
if (isLoading) return <LoadingSpinner desc={loadingSpinnerDesc} />
const isDev = import.meta.env.MODE === 'development'
return (
<>
<AppBar />
<main
className={styles.main}
// Add dev debug flag to show special dev css
// (visible marks border on verify)
{...(isDev && {
'data-dev': true
})}
>
<Outlet />
</main>
</>
)
}