feat: sync and update with all changes from fork (#720)

* feat: sync and update with all changes from fork
* refactor: extract clipboard copy logic into custom hook
* fix: correct spelling of DEFAULT_REFRESH_FREQUENCY_MS in Stamps and WalletBalance providers
* refactor(ui-tests): replace fixed sleeps with condition-based waits
* fix: handle null values for size and granteeCount in infoGroups
* fix(lint): add newline at end of file in useClipboardCopy hook
* fix(ui-tests): page.goto URL
* refactor: update import paths for useClipboardCopy

---------

Co-authored-by: Ferenc Sárai <sarai.ferenc@gmail.com>
This commit is contained in:
Bálint Ujvári
2026-03-02 11:34:39 +01:00
committed by GitHub
parent b0f00a624a
commit 519c411db0
303 changed files with 16609 additions and 29415 deletions
+120 -77
View File
@@ -13,8 +13,20 @@ import {
Topology,
WalletBalance,
} from '@ethersphere/bee-js'
import { createContext, ReactChild, ReactElement, useContext, useEffect, useState } from 'react'
import {
createContext,
ReactElement,
ReactNode,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import { useLatestBeeRelease } from '../hooks/apiHooks'
import { Context as SettingsContext } from './Settings'
const LAUNCH_GRACE_PERIOD = 15_000
@@ -92,8 +104,8 @@ const initialValues: ContextInterface = {
latestBeeRelease: null,
isLoading: true,
lastUpdate: null,
start: () => {}, // eslint-disable-line
stop: () => {}, // eslint-disable-line
start: () => {},
stop: () => {},
refresh: () => Promise.reject(),
}
@@ -101,7 +113,7 @@ export const Context = createContext<ContextInterface>(initialValues)
export const Consumer = Context.Consumer
interface Props {
children: ReactChild
children: ReactNode
}
function getStatus(
@@ -162,12 +174,9 @@ function determineOverallStatus(status: Status, startedAt: number): CheckState {
// This does not need to be exposed and works much better as variable than state variable which may trigger some unnecessary re-renders
let isRefreshing = false
interface Props {
children: ReactChild
}
export function Provider({ children }: Props): ReactElement {
const { beeApi } = useContext(SettingsContext)
const [beeVersion, setBeeVersion] = useState<string | null>(null)
const [apiHealth, setApiHealth] = useState<boolean>(false)
const [nodeAddresses, setNodeAddresses] = useState<NodeAddresses | null>(null)
@@ -182,42 +191,18 @@ export function Provider({ children }: Props): ReactElement {
const [settlements, setSettlements] = useState<AllSettlements | null>(null)
const [chainState, setChainState] = useState<ChainState | null>(null)
const [walletBalance, setWalletBalance] = useState<WalletBalance | null>(null)
const [startedAt] = useState(Date.now())
const [startedAt] = useState(() => Date.now())
const { latestBeeRelease } = useLatestBeeRelease()
const [error, setError] = useState<Error | null>(initialValues.error)
const [isLoading, setIsLoading] = useState<boolean>(initialValues.isLoading)
const [lastUpdate, setLastUpdate] = useState<number | null>(initialValues.lastUpdate)
const [frequency, setFrequency] = useState<number | null>(30000)
const [frequency, setFrequency] = useState<number | null>(REFRESH_WHEN_OK)
useEffect(() => {
setIsLoading(true)
const frequencyRef = useRef<number | null>(frequency)
setApiHealth(false)
if (beeApi !== null) refresh()
}, [beeApi]) // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
setIsLoading(true)
setNodeAddresses(null)
setNodeTopology(null)
setNodeInfo(null)
setPeers(null)
setChequebookAddress(null)
setChequebookBalance(null)
setPeerBalances(null)
setPeerCheques(null)
setSettlements(null)
setChainState(null)
if (beeApi !== null) {
refresh()
}
}, [beeApi]) // eslint-disable-line react-hooks/exhaustive-deps
const refresh = async () => {
const refresh = useCallback(async () => {
// Don't want to refresh when already refreshing
if (isRefreshing) {
return
@@ -325,23 +310,59 @@ export function Provider({ children }: Props): ReactElement {
setIsLoading(false)
isRefreshing = false
setLastUpdate(Date.now())
}
}, [beeApi])
const start = (freq = REFRESH_WHEN_OK) => {
refresh()
setFrequency(freq)
}
const stop = () => setFrequency(null)
const start = useCallback(
(freq = REFRESH_WHEN_OK) => {
refresh()
setFrequency(freq)
},
[refresh],
)
const stop = useCallback(() => setFrequency(null), [])
const status = getStatus(nodeInfo, apiHealth, topology, chequebookAddress, chequebookBalance, error, startedAt)
const status = useMemo(
() => getStatus(nodeInfo, apiHealth, topology, chequebookAddress, chequebookBalance, error, startedAt),
[nodeInfo, apiHealth, topology, chequebookAddress, chequebookBalance, error, startedAt],
)
useEffect(() => {
let newFrequency = REFRESH_WHEN_OK
const setStates = () => {
setIsLoading(true)
setApiHealth(false)
setNodeAddresses(null)
setNodeTopology(null)
setNodeInfo(null)
setPeers(null)
setChequebookAddress(null)
setChequebookBalance(null)
setPeerBalances(null)
setPeerCheques(null)
setSettlements(null)
setChainState(null)
if (status.all !== 'OK') newFrequency = REFRESH_WHEN_ERROR
if (beeApi !== null) {
refresh()
}
}
if (newFrequency !== frequency) setFrequency(newFrequency)
}, [status.all, frequency])
setStates()
}, [beeApi, refresh])
useEffect(() => {
frequencyRef.current = frequency
}, [frequency])
useEffect(() => {
const newFrequency = status.all !== CheckState.OK ? REFRESH_WHEN_ERROR : REFRESH_WHEN_OK
const setFrequencyState = () => {
if (newFrequency !== frequencyRef.current) {
setFrequency(newFrequency)
}
}
setFrequencyState()
}, [status.all])
// Start the update loop
useEffect(() => {
@@ -351,36 +372,58 @@ export function Provider({ children }: Props): ReactElement {
return () => clearInterval(interval)
}
}, [frequency, beeApi]) // eslint-disable-line react-hooks/exhaustive-deps
}, [frequency, beeApi, refresh])
return (
<Context.Provider
value={{
beeVersion,
status,
error,
apiHealth,
nodeAddresses,
nodeInfo,
topology,
chequebookAddress,
peers,
chequebookBalance,
stake,
peerBalances,
peerCheques,
settlements,
chainState,
walletBalance,
latestBeeRelease,
isLoading,
lastUpdate,
start,
stop,
refresh,
}}
>
{children}
</Context.Provider>
const contextValue = useMemo(
() => ({
beeVersion,
status,
error,
apiHealth,
nodeAddresses,
nodeInfo,
topology,
chequebookAddress,
peers,
chequebookBalance,
stake,
peerBalances,
peerCheques,
settlements,
chainState,
walletBalance,
latestBeeRelease,
isLoading,
lastUpdate,
start,
stop,
refresh,
}),
[
beeVersion,
status,
error,
apiHealth,
nodeAddresses,
nodeInfo,
topology,
chequebookAddress,
peers,
chequebookBalance,
stake,
peerBalances,
peerCheques,
settlements,
chainState,
walletBalance,
latestBeeRelease,
isLoading,
lastUpdate,
start,
stop,
refresh,
],
)
return <Context.Provider value={contextValue}>{children}</Context.Provider>
}
+11 -6
View File
@@ -1,6 +1,11 @@
import { createContext, ReactChild, ReactElement, useEffect, useState } from 'react'
import { createContext, ReactElement, ReactNode, useEffect, useState } from 'react'
export type IdentityType = 'V3' | 'PRIVATE_KEY'
import { LocalStorageKeys } from '../utils/localStorage'
export enum IdentityType {
V3 = 'V3',
PrivateKey = 'PRIVATE_KEY',
}
export interface Identity {
uuid: string
@@ -18,14 +23,14 @@ interface ContextInterface {
const initialValues: ContextInterface = {
identities: [],
setIdentities: () => {}, // eslint-disable-line
setIdentities: () => {},
}
export const Context = createContext<ContextInterface>(initialValues)
export const Consumer = Context.Consumer
interface Props {
children: ReactChild
children: ReactNode
}
export function Provider({ children }: Props): ReactElement {
@@ -33,11 +38,11 @@ export function Provider({ children }: Props): ReactElement {
useEffect(() => {
try {
setIdentities(JSON.parse(localStorage.getItem('feeds') || '[]'))
setIdentities(JSON.parse(localStorage.getItem(LocalStorageKeys.feeds) || '[]'))
} catch {
setIdentities([])
}
}, []) // eslint-disable-line react-hooks/exhaustive-deps
}, [setIdentities])
return <Context.Provider value={{ identities, setIdentities }}>{children}</Context.Provider>
}
+7 -6
View File
@@ -1,13 +1,14 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import { createContext, ReactElement, ReactNode, useEffect, useState } from 'react'
import { createContext, ReactChild, ReactElement, useState, useEffect } from 'react'
import { PREVIEW_DIMENSIONS } from '../constants'
import { getMetadata } from '../utils/file'
import { resize } from '../utils/image'
import { PREVIEW_DIMENSIONS } from '../constants'
export type UploadOrigin = { origin: 'UPLOAD' | 'FEED'; uuid?: string }
import { FileOrigin } from '@/pages/files/FileNavigation'
export const defaultUploadOrigin: UploadOrigin = { origin: 'UPLOAD' }
export type UploadOrigin = { origin: FileOrigin.Upload | FileOrigin.Feed; uuid?: string }
export const defaultUploadOrigin: UploadOrigin = { origin: FileOrigin.Upload }
interface ContextInterface {
files: FilePath[]
@@ -30,7 +31,7 @@ export const Context = createContext<ContextInterface>(initialValues)
export const Consumer = Context.Consumer
interface Props {
children: ReactChild
children: ReactNode
}
export function Provider({ children }: Props): ReactElement {
+89 -53
View File
@@ -1,12 +1,13 @@
import { createContext, useCallback, useContext, useState, ReactNode, useEffect } from 'react'
import { Bee, PostageBatch } from '@ethersphere/bee-js'
import type { FileInfo } from '@solarpunkltd/file-manager-lib'
import { FileManagerBase, FileManagerEvents } from '@solarpunkltd/file-manager-lib'
import { Context as SettingsContext } from './Settings'
import { DriveInfo } from '@solarpunkltd/file-manager-lib'
import { getSignerPk } from '../modules/filemanager/utils/common'
import { DriveInfo, FileManagerBase, FileManagerEvents } from '@solarpunkltd/file-manager-lib'
import { createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
import { FILE_MANAGER_EVENTS } from '../modules/filemanager/constants/common'
import { getUsableStamps } from '../modules/filemanager/utils/bee'
import { getSignerPk } from '../modules/filemanager/utils/common'
import { Context as SettingsContext } from './Settings'
interface ContextInterface {
fm: FileManagerBase | null
@@ -39,13 +40,15 @@ const initialValues: ContextInterface = {
initializationError: false,
showError: false,
shallReset: false,
setCurrentDrive: () => {}, // eslint-disable-line
setCurrentStamp: () => {}, // eslint-disable-line
resync: async () => {}, // eslint-disable-line
init: async () => null, // eslint-disable-line
setShowError: () => {}, // eslint-disable-line
syncDrives: async () => {}, // eslint-disable-line
refreshStamp: async () => undefined, // eslint-disable-line
setCurrentDrive: () => {},
setCurrentStamp: () => {},
resync: async () => {},
// eslint-disable-next-line require-await
init: async () => null,
setShowError: () => {},
syncDrives: async () => {},
// eslint-disable-next-line require-await
refreshStamp: async () => undefined,
}
export const Context = createContext<ContextInterface>(initialValues)
@@ -82,6 +85,8 @@ const findDrives = (
}
export function Provider({ children }: Props) {
const initInProgressRef = useRef(false)
const { apiUrl, beeApi } = useContext(SettingsContext)
const [fm, setFm] = useState<FileManagerBase | null>(null)
@@ -193,22 +198,30 @@ export function Provider({ children }: Props) {
}
}, [fm, syncDrives])
// no useCallback is needed because it caches the stamp
const refreshStamp = async (batchId: string): Promise<PostageBatch | undefined> => {
const usableStamps = await getUsableStamps(beeApi)
const refreshedStamp = usableStamps.find(s => s.batchID.toString() === batchId)
const refreshStamp = useCallback(
async (batchId: string): Promise<PostageBatch | undefined> => {
const usableStamps = await getUsableStamps(beeApi)
const refreshedStamp = usableStamps.find(s => s.batchID.toString() === batchId)
if (currentStamp && currentStamp.batchID.toString() === batchId && refreshedStamp) {
setCurrentStamp(refreshedStamp)
}
setCurrentStamp(prev => {
if (prev && prev.batchID.toString() === batchId && refreshedStamp) {
return refreshedStamp
}
return refreshedStamp
}
return prev
})
return refreshedStamp
},
[beeApi],
)
const init = useCallback(async (): Promise<FileManagerBase | null> => {
const pk = getSignerPk()
if (!apiUrl || !pk) return null
if (!apiUrl || !pk || initInProgressRef.current) return null
initInProgressRef.current = true
setFm(null)
setFiles([])
@@ -235,7 +248,6 @@ export function Provider({ children }: Props) {
}
setFm(manager)
syncDrives(manager)
syncFiles(manager)
}
}
@@ -290,8 +302,10 @@ export function Provider({ children }: Props) {
await manager.initialize()
return manager
} catch (error) {
} catch {
return null
} finally {
initInProgressRef.current = false
}
}, [apiUrl, syncDrives, syncFiles])
@@ -305,18 +319,17 @@ export function Provider({ children }: Props) {
const refreshedDrive = manager.driveList.find(d => d.id.toString() === prevDriveId)
setCurrentDrive(refreshedDrive)
const isValidCurrentStamp = (await getUsableStamps(beeApi)).find(
s => s.batchID.toString() === prevStamp?.batchID.toString(),
)
const uStamps: PostageBatch[] = await getUsableStamps(beeApi)
const isValidCurrentStamp = uStamps.find(s => s.batchID.toString() === prevStamp?.batchID.toString())
setCurrentStamp(isValidCurrentStamp)
}
}, [currentDrive?.id, currentStamp, init, setCurrentDrive, setCurrentStamp, beeApi])
}, [currentDrive?.id, currentStamp, init, beeApi])
useEffect(() => {
const pk = getSignerPk()
if (!pk || fm) return
if (!pk || fm || initInProgressRef.current) return
const initFromLocalState = async () => {
await init()
@@ -325,29 +338,52 @@ export function Provider({ children }: Props) {
initFromLocalState()
}, [fm, init])
return (
<Context.Provider
value={{
fm,
files,
currentDrive,
currentStamp,
drives,
expiredDrives,
adminDrive,
initializationError,
showError,
shallReset,
setCurrentDrive,
setCurrentStamp,
resync,
init,
setShowError,
syncDrives: syncDrivesPublic,
refreshStamp,
}}
>
{children}
</Context.Provider>
useEffect(() => {
if (fm && drives.length === 0 && !adminDrive) {
syncDrives(fm)
}
}, [fm, drives.length, adminDrive, syncDrives])
const contextValue = useMemo(
() => ({
fm,
files,
currentDrive,
currentStamp,
drives,
expiredDrives,
adminDrive,
initializationError,
showError,
shallReset,
setCurrentDrive,
setCurrentStamp,
resync,
init,
setShowError,
syncDrives: syncDrivesPublic,
refreshStamp,
}),
[
fm,
files,
currentDrive,
currentStamp,
drives,
expiredDrives,
adminDrive,
initializationError,
showError,
shallReset,
setCurrentDrive,
setCurrentStamp,
resync,
init,
setShowError,
syncDrivesPublic,
refreshStamp,
],
)
return <Context.Provider value={contextValue}>{children}</Context.Provider>
}
+10 -17
View File
@@ -1,4 +1,4 @@
import { createContext, ReactChild, ReactElement, useState, useEffect } from 'react'
import { createContext, ReactElement, ReactNode, useState } from 'react'
// These need to be numeric values as they are used as indexes in the TabsContainer
export enum Platforms {
@@ -21,14 +21,14 @@ interface ContextInterface {
const initialValues: ContextInterface = {
platform: SupportedPlatforms.macOS,
setPlatform: () => {}, // eslint-disable-line
setPlatform: () => {},
}
export const Context = createContext<ContextInterface>(initialValues)
export const Consumer = Context.Consumer
interface Props {
children: ReactChild
children: ReactNode
}
function isSupportedPlatform(platform: unknown): platform is SupportedPlatforms {
@@ -37,20 +37,16 @@ function isSupportedPlatform(platform: unknown): platform is SupportedPlatforms
function getOS(): Platforms | null {
const userAgent = window.navigator.userAgent
const platform = window.navigator.platform
const macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K']
const windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE']
const iosPlatforms = ['iPhone', 'iPad', 'iPod']
if (macosPlatforms.includes(platform)) return Platforms.macOS
if (/Macintosh|MacIntel|MacPPC|Mac68K/.test(userAgent)) return Platforms.macOS
if (iosPlatforms.includes(platform)) return Platforms.iOS
if (/iPhone|iPad|iPod/.test(userAgent)) return Platforms.iOS
if (windowsPlatforms.includes(platform)) return Platforms.Windows
if (/Win32|Win64|Windows|WinCE/.test(userAgent)) return Platforms.Windows
if (/Android/.test(userAgent)) return Platforms.Android
if (/Linux/.test(platform)) return Platforms.Linux
if (/Linux/.test(userAgent)) return Platforms.Linux
return null
}
@@ -92,14 +88,11 @@ export const cacheClearUrls = {
}
export function Provider({ children }: Props): ReactElement {
const [platform, setPlatform] = useState<SupportedPlatforms>(SupportedPlatforms.Linux)
// This is in useEffect as it really just needs to run once and not on each re-render
useEffect(() => {
const [platform, setPlatform] = useState<SupportedPlatforms>(() => {
const os = getOS()
setPlatform(isSupportedPlatform(os) ? os : SupportedPlatforms.Linux)
}, [])
return isSupportedPlatform(os) ? os : SupportedPlatforms.Linux
})
return <Context.Provider value={{ platform, setPlatform }}>{children}</Context.Provider>
}
+67 -54
View File
@@ -1,12 +1,11 @@
import { Bee } from '@ethersphere/bee-js'
import { providers } from 'ethers'
import { ReactElement, ReactNode, createContext, useEffect, useState } from 'react'
import { JsonRpcProvider } from 'ethers'
import { createContext, ReactElement, ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
import { DEFAULT_BEE_API_HOST, DEFAULT_RPC_URL } from '../constants'
import { useGetBeeConfig } from '../hooks/apiHooks'
const LocalStorageKeys = {
providerUrl: 'json-rpc-provider',
}
import { newGnosisProvider } from '../utils/chain'
import { LocalStorageKeys } from '../utils/localStorage'
interface ContextInterface {
apiUrl: string
@@ -16,7 +15,7 @@ interface ContextInterface {
isDesktop: boolean
desktopUrl: string
rpcProviderUrl: string
rpcProvider: providers.JsonRpcProvider | null
rpcProvider: JsonRpcProvider | null
cors: string | null
dataDir: string | null
ensResolver: string | null
@@ -29,12 +28,12 @@ interface ContextInterface {
const initialValues: ContextInterface = {
beeApi: null,
apiUrl: DEFAULT_BEE_API_HOST,
setApiUrl: () => {}, // eslint-disable-line
setApiUrl: () => {},
lockedApiSettings: false,
isDesktop: false,
desktopApiKey: '',
desktopUrl: window.location.origin,
setAndPersistJsonRpcProvider: async () => {}, // eslint-disable-line
setAndPersistJsonRpcProvider: async () => {},
rpcProviderUrl: '',
rpcProvider: null,
cors: null,
@@ -66,12 +65,13 @@ export function Provider({ children, ...propsSettings }: Props): ReactElement {
localStorage.getItem(LocalStorageKeys.providerUrl) || propsSettings.defaultRpcUrl || DEFAULT_RPC_URL
const [apiUrl, setApiUrl] = useState<string>(
localStorage.getItem('api_host') ?? propsSettings.beeApiUrl ?? initialValues.apiUrl,
localStorage.getItem(LocalStorageKeys.apiHost) ?? propsSettings.beeApiUrl ?? initialValues.apiUrl,
)
const [beeApi, setBeeApi] = useState<Bee | null>(null)
const [desktopApiKey, setDesktopApiKey] = useState<string>(initialValues.desktopApiKey)
const [rpcProviderUrl, setRpcProviderUrl] = useState(propsProviderUrl)
const [rpcProvider, setRpcProvider] = useState(new providers.JsonRpcProvider(propsProviderUrl))
const [rpcProvider, setRpcProvider] = useState(newGnosisProvider(propsProviderUrl))
const { config, isLoading, error } = useGetBeeConfig(desktopUrl)
useEffect(() => {
@@ -79,56 +79,80 @@ export function Provider({ children, ...propsSettings }: Props): ReactElement {
const newApiKey = urlSearchParams.get('v')
if (newApiKey) {
localStorage.setItem('apiKey', newApiKey)
localStorage.setItem(LocalStorageKeys.apiKey, newApiKey)
window.location.search = ''
setDesktopApiKey(newApiKey)
}
}, [])
useEffect(() => {
const url = makeHttpUrl(localStorage.getItem('api_host') ?? config?.['api-addr'] ?? apiUrl)
try {
setBeeApi(new Bee(url))
} catch (e) {
setBeeApi(null)
const url = makeHttpUrl(localStorage.getItem(LocalStorageKeys.apiHost) ?? config?.['api-addr'] ?? apiUrl)
const setBeeApiState = () => {
try {
setBeeApi(new Bee(url))
} catch {
setBeeApi(null)
}
}
setBeeApiState()
}, [config, apiUrl])
const updateApiUrl = (url: string) => {
const updateApiUrl = useCallback((url: string) => {
const userProvidedUrl = makeHttpUrl(url)
try {
setBeeApi(new Bee(userProvidedUrl))
localStorage.setItem('api_host', userProvidedUrl)
localStorage.setItem(LocalStorageKeys.apiHost, userProvidedUrl)
setApiUrl(userProvidedUrl)
} catch (e) {
} catch (_) {
setBeeApi(null)
}
}
}, [])
return (
<Context.Provider
value={{
apiUrl,
beeApi,
setApiUrl: updateApiUrl,
lockedApiSettings: Boolean(propsSettings.lockedApiSettings),
desktopApiKey,
isDesktop,
desktopUrl,
rpcProvider,
rpcProviderUrl,
cors: config?.['cors-allowed-origins'] ?? null,
dataDir: config?.['data-dir'] ?? null,
ensResolver: config?.['resolver-options'] ?? null,
setAndPersistJsonRpcProvider: setAndPersistJsonRpcProviderClosure(setRpcProviderUrl, setRpcProvider),
isLoading,
error,
}}
>
{children}
</Context.Provider>
const setAndPersistJsonRpcProvider = useCallback((providerUrl: string) => {
localStorage.setItem(LocalStorageKeys.providerUrl, providerUrl)
setRpcProviderUrl(providerUrl)
setRpcProvider(newGnosisProvider(providerUrl))
}, [])
const contextValue = useMemo(
() => ({
apiUrl,
beeApi,
setApiUrl: updateApiUrl,
lockedApiSettings: Boolean(propsSettings.lockedApiSettings),
desktopApiKey,
isDesktop,
desktopUrl,
rpcProvider,
rpcProviderUrl,
cors: config?.['cors-allowed-origins'] ?? null,
dataDir: config?.['data-dir'] ?? null,
ensResolver: config?.['resolver-options'] ?? null,
setAndPersistJsonRpcProvider,
isLoading,
error,
}),
[
apiUrl,
beeApi,
updateApiUrl,
propsSettings.lockedApiSettings,
desktopApiKey,
isDesktop,
desktopUrl,
rpcProvider,
rpcProviderUrl,
config,
setAndPersistJsonRpcProvider,
isLoading,
error,
],
)
return <Context.Provider value={contextValue}>{children}</Context.Provider>
}
function makeHttpUrl(string: string): string {
@@ -138,14 +162,3 @@ function makeHttpUrl(string: string): string {
return string
}
function setAndPersistJsonRpcProviderClosure(
setProviderUrl: (url: string) => void,
setProvider: (prov: providers.JsonRpcProvider) => void,
) {
return (providerUrl: string) => {
localStorage.setItem(LocalStorageKeys.providerUrl, providerUrl)
setProviderUrl(providerUrl)
setProvider(new providers.JsonRpcProvider(providerUrl))
}
}
+23 -16
View File
@@ -1,7 +1,10 @@
import { PostageBatch } from '@ethersphere/bee-js'
import { createContext, ReactChild, ReactElement, useContext, useEffect, useState } from 'react'
import { createContext, ReactElement, ReactNode, useCallback, useContext, useEffect, useRef, useState } from 'react'
import { Context as SettingsContext } from './Settings'
const DEFAULT_REFRESH_REQUENCY_MS = 30_000
export interface EnrichedPostageBatch extends PostageBatch {
usage: number
usageText: string
@@ -22,8 +25,8 @@ const initialValues: ContextInterface = {
error: null,
isLoading: false,
lastUpdate: null,
start: () => {}, // eslint-disable-line
stop: () => {}, // eslint-disable-line
start: () => {},
stop: () => {},
refresh: () => Promise.reject(),
}
@@ -31,7 +34,7 @@ export const Context = createContext<ContextInterface>(initialValues)
export const Consumer = Context.Consumer
interface Props {
children: ReactChild
children: ReactNode
}
export function enrichStamp(postageBatch: PostageBatch): EnrichedPostageBatch {
@@ -55,18 +58,20 @@ export function Provider({ children }: Props): ReactElement {
const [lastUpdate, setLastUpdate] = useState<number | null>(initialValues.lastUpdate)
const [frequency, setFrequency] = useState<number | null>(null)
const refresh = async () => {
if (isLoading) {
return
}
const isLoadingRef = useRef<boolean>(isLoading)
if (!beeApi) {
useEffect(() => {
isLoadingRef.current = isLoading
}, [isLoading])
const refresh = useCallback(async () => {
if (isLoadingRef.current || !beeApi) {
return
}
try {
setIsLoading(true)
const stamps = await beeApi.getAllPostageBatch()
const stamps = await beeApi.getPostageBatches()
setStamps(stamps.map(enrichStamp))
setLastUpdate(Date.now())
@@ -76,22 +81,24 @@ export function Provider({ children }: Props): ReactElement {
} finally {
setIsLoading(false)
}
}
}, [beeApi])
const start = (freq = 30000) => setFrequency(freq)
const start = (freq = DEFAULT_REFRESH_REQUENCY_MS) => setFrequency(freq)
const stop = () => setFrequency(null)
// Start the update loop
useEffect(() => {
refresh()
if (beeApi) {
refresh()
}
}, [beeApi, refresh])
// Start autorefresh only if the frequency is set
useEffect(() => {
if (frequency) {
const interval = setInterval(refresh, frequency)
return () => clearInterval(interval)
}
}, [frequency]) // eslint-disable-line react-hooks/exhaustive-deps
}, [frequency, refresh])
return (
<Context.Provider value={{ stamps, error, isLoading, lastUpdate, start, stop, refresh }}>
+13 -13
View File
@@ -1,12 +1,9 @@
import { Wallet } from 'ethers'
import { createContext, ReactElement, useContext, useEffect, useState } from 'react'
import { Context as SettingsContext } from './Settings'
import { createContext, ReactElement, useCallback, useContext, useEffect, useState } from 'react'
const LocalStorageKeys = {
depositWallet: 'deposit-wallet',
giftWallets: 'gift-wallets',
invitation: 'invitation',
}
import { LocalStorageKeys } from '../utils/localStorage'
import { Context as SettingsContext } from './Settings'
interface ContextInterface {
giftWallets: Wallet[]
@@ -15,7 +12,7 @@ interface ContextInterface {
const initialValues: ContextInterface = {
giftWallets: [],
addGiftWallet: () => {}, // eslint-disable-line
addGiftWallet: () => {},
}
export const Context = createContext<ContextInterface>(initialValues)
@@ -39,11 +36,14 @@ export function Provider({ children }: Props): ReactElement {
}
}, [rpcProvider])
function addGiftWallet(wallet: Wallet) {
const newArray = [...giftWallets, wallet]
localStorage.setItem(LocalStorageKeys.giftWallets, JSON.stringify(newArray.map(x => x.privateKey)))
setGiftWallets(newArray)
}
const addGiftWallet = useCallback((wallet: Wallet) => {
setGiftWallets(prev => {
const newArray = [...prev, wallet]
localStorage.setItem(LocalStorageKeys.giftWallets, JSON.stringify(newArray.map(x => x.privateKey)))
return newArray
})
}, [])
return (
<Context.Provider
+50 -16
View File
@@ -1,5 +1,17 @@
import { createContext, ReactChild, ReactElement, useContext, useEffect, useState } from 'react'
import {
createContext,
ReactElement,
ReactNode,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import { WalletAddress } from '../utils/wallet'
import { Context as BeeContext } from './Bee'
import { Context as SettingsContext } from './Settings'
@@ -18,8 +30,8 @@ const initialValues: ContextInterface = {
error: null,
isLoading: false,
lastUpdate: null,
start: () => {}, // eslint-disable-line
stop: () => {}, // eslint-disable-line
start: () => {},
stop: () => {},
refresh: () => Promise.reject(),
}
@@ -27,18 +39,31 @@ export const Context = createContext<ContextInterface>(initialValues)
export const Consumer = Context.Consumer
interface Props {
children: ReactChild
children: ReactNode
}
const DEFAULT_REFRESH_REQUENCY_MS = 30_000
export function Provider({ children }: Props): ReactElement {
const { rpcProvider } = useContext(SettingsContext)
const { nodeAddresses } = useContext(BeeContext)
const [balance, setBalance] = useState<WalletAddress | null>(initialValues.balance)
const [error, setError] = useState<Error | null>(initialValues.error)
const [isLoading, setIsLoading] = useState<boolean>(initialValues.isLoading)
const balanceRef = useRef<WalletAddress | null>(balance)
const isLoadingRef = useRef<boolean>(isLoading)
const [lastUpdate, setLastUpdate] = useState<number | null>(initialValues.lastUpdate)
const [frequency, setFrequency] = useState<number | null>(null)
useEffect(() => {
balanceRef.current = balance
}, [balance])
useEffect(() => {
isLoadingRef.current = isLoading
}, [isLoading])
useEffect(() => {
if (nodeAddresses?.ethereum && rpcProvider) {
WalletAddress.make(nodeAddresses.ethereum.toHex(), rpcProvider).then(setBalance)
@@ -47,26 +72,26 @@ export function Provider({ children }: Props): ReactElement {
}
}, [nodeAddresses, rpcProvider])
const refresh = async () => {
const refresh = useCallback(async () => {
// Don't want to refresh when already refreshing
if (isLoading) return
if (isLoadingRef.current) return
if (!balance) return
if (!balanceRef.current) return
try {
setIsLoading(true)
setBalance(await balance.refresh())
setBalance(await balanceRef.current.refresh())
setLastUpdate(Date.now())
} catch (e) {
setError(e as Error)
} finally {
setIsLoading(false)
}
}
}, [])
const start = (freq = 30000) => setFrequency(freq)
const stop = () => setFrequency(null)
const start = useCallback((freq = DEFAULT_REFRESH_REQUENCY_MS) => setFrequency(freq), [])
const stop = useCallback(() => setFrequency(null), [])
// Start the update loop
useEffect(() => {
@@ -78,11 +103,20 @@ export function Provider({ children }: Props): ReactElement {
return () => clearInterval(interval)
}
}, [frequency]) // eslint-disable-line react-hooks/exhaustive-deps
}, [frequency, refresh])
return (
<Context.Provider value={{ balance, error, isLoading, lastUpdate, start, stop, refresh }}>
{children}
</Context.Provider>
const contextValue = useMemo(
() => ({
balance,
error,
isLoading,
lastUpdate,
start,
stop,
refresh,
}),
[balance, error, isLoading, lastUpdate, start, stop, refresh],
)
return <Context.Provider value={contextValue}>{children}</Context.Provider>
}