feat: add `NetworkStatusIndicator`
Signed-off-by: Innei <i@innei.in>
This commit is contained in:
parent
6fab907ed3
commit
6023993d3a
|
|
@ -0,0 +1,33 @@
|
|||
import { createAtomHooks } from "@renderer/lib/jotai"
|
||||
import { atom } from "jotai"
|
||||
|
||||
export enum NetworkStatus {
|
||||
ONLINE,
|
||||
OFFLINE,
|
||||
}
|
||||
|
||||
export const [, , useNetworkStatus, , getNetworkStatus, setNetworkStatus] =
|
||||
createAtomHooks(
|
||||
atom(navigator.onLine ? NetworkStatus.ONLINE : NetworkStatus.OFFLINE),
|
||||
)
|
||||
|
||||
export const [, , useApiStatus, , getApiStatus, setApiStatus] = createAtomHooks(
|
||||
atom(NetworkStatus.ONLINE),
|
||||
)
|
||||
|
||||
export const subscribeNetworkStatus = () => {
|
||||
const handleOnline = () => setNetworkStatus(NetworkStatus.ONLINE)
|
||||
const handleOffline = () => setNetworkStatus(NetworkStatus.OFFLINE)
|
||||
|
||||
window.addEventListener("online", handleOnline)
|
||||
window.addEventListener("offline", handleOffline)
|
||||
|
||||
setNetworkStatus(
|
||||
navigator.onLine ? NetworkStatus.ONLINE : NetworkStatus.OFFLINE,
|
||||
)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("online", handleOnline)
|
||||
window.removeEventListener("offline", handleOffline)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { useInputComposition } from "@renderer/hooks/common/use-input-composition"
|
||||
import { useInputComposition } from "@renderer/hooks/common/useInputComposition"
|
||||
import { stopPropagation } from "@renderer/lib/dom"
|
||||
import { cn } from "@renderer/lib/utils"
|
||||
import type { DetailedHTMLProps, InputHTMLAttributes } from "react"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client"
|
||||
|
||||
import { useInputComposition } from "@renderer/hooks/common/use-input-composition"
|
||||
import { useInputComposition } from "@renderer/hooks/common/useInputComposition"
|
||||
import { stopPropagation } from "@renderer/lib/dom"
|
||||
import { cn } from "@renderer/lib/utils"
|
||||
import clsx from "clsx"
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export const RadioCard: FC<
|
|||
|
||||
"ring-0 ring-theme-accent/20 duration-200",
|
||||
|
||||
selected && "border-theme-accent bg-theme-accent text-white outline-none ring-2",
|
||||
selected && "border-theme-accent bg-theme-accent/5 font-medium outline-none ring-2",
|
||||
wrapperClassName,
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
import { useEffect, useState } from "react"
|
||||
|
||||
export const useIsOnline = () => {
|
||||
const [isOnline, setIsOnline] = useState(navigator.onLine)
|
||||
|
||||
useEffect(() => {
|
||||
const handleOnline = () => setIsOnline(true)
|
||||
const handleOffline = () => setIsOnline(false)
|
||||
|
||||
window.addEventListener("online", handleOnline)
|
||||
window.addEventListener("offline", handleOffline)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("online", handleOnline)
|
||||
window.removeEventListener("offline", handleOffline)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return isOnline
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import { registerGlobalContext } from "@shared/bridge"
|
|||
import { enableMapSet } from "immer"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import { subscribeNetworkStatus } from "./atoms/network"
|
||||
import { initializeSettings } from "./atoms/settings"
|
||||
import {
|
||||
getGeneralSettings,
|
||||
|
|
@ -31,6 +32,7 @@ export const initializeApp = async () => {
|
|||
appLog(`Initialize ${APP_NAME}...`)
|
||||
|
||||
const now = Date.now()
|
||||
subscribeNetworkStatus()
|
||||
|
||||
registerGlobalContext({
|
||||
showSetting: () => window.router.showSettings(),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { getCsrfToken } from "@hono/auth-js/react"
|
||||
import PKG from "@pkg"
|
||||
import { NetworkStatus, setApiStatus } from "@renderer/atoms/network"
|
||||
import { setLoginModalShow } from "@renderer/atoms/user"
|
||||
import type { AppType } from "@renderer/hono"
|
||||
import { hc } from "hono/client"
|
||||
|
|
@ -39,8 +40,19 @@ export const apiFetch = ofetch.create({
|
|||
options.headers = header
|
||||
}
|
||||
},
|
||||
onResponse() {
|
||||
setApiStatus(NetworkStatus.ONLINE)
|
||||
},
|
||||
onResponseError(context) {
|
||||
const { router } = window
|
||||
|
||||
// If api is down
|
||||
if ((!context.response || context.response.status === 0) && navigator.onLine) {
|
||||
setApiStatus(NetworkStatus.OFFLINE)
|
||||
} else {
|
||||
setApiStatus(NetworkStatus.ONLINE)
|
||||
}
|
||||
|
||||
if (context.response.status === 401) {
|
||||
// Or we can present LoginModal here.
|
||||
// router.navigate("/login")
|
||||
|
|
@ -59,7 +71,14 @@ export const apiFetch = ofetch.create({
|
|||
})
|
||||
|
||||
export const apiClient = hc<AppType>("", {
|
||||
fetch: async (input, options = {}) => apiFetch(input.toString(), options),
|
||||
fetch: async (input, options = {}) => apiFetch(input.toString(), options).catch(
|
||||
(err) => {
|
||||
if (err instanceof FetchError && !err.response) {
|
||||
setApiStatus(NetworkStatus.OFFLINE)
|
||||
}
|
||||
throw err
|
||||
},
|
||||
),
|
||||
headers() {
|
||||
return {
|
||||
"X-App-Version": PKG.version,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
import { NetworkStatus, useApiStatus, useNetworkStatus } from "@renderer/atoms/network"
|
||||
import { useGeneralSettingKey } from "@renderer/atoms/settings/general"
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@renderer/components/ui/tooltip"
|
||||
import { cn } from "@renderer/lib/utils"
|
||||
|
||||
export const NetworkStatusIndicator = () => {
|
||||
const networkStatus = useNetworkStatus()
|
||||
const apiStatus = useApiStatus()
|
||||
const canUseLocalData = useGeneralSettingKey("dataPersist")
|
||||
|
||||
if (
|
||||
networkStatus === NetworkStatus.ONLINE &&
|
||||
apiStatus === NetworkStatus.ONLINE
|
||||
) {
|
||||
return null
|
||||
}
|
||||
const canAccessAnyData =
|
||||
canUseLocalData ||
|
||||
(networkStatus === NetworkStatus.ONLINE &&
|
||||
apiStatus === NetworkStatus.ONLINE)
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className={cn(
|
||||
"fixed bottom-2 left-2 flex items-center gap-1 rounded-full border bg-background px-1.5 py-1",
|
||||
!canAccessAnyData &&
|
||||
"border-current bg-red-50 text-red-400 dark:bg-red-800 dark:text-red-500",
|
||||
)}
|
||||
>
|
||||
<i className="i-mgc-wifi-off-cute-re shrink-0 text-base" />
|
||||
<span className="shrink-0 text-xs font-semibold opacity-50">
|
||||
{networkStatus === NetworkStatus.OFFLINE ?
|
||||
canUseLocalData ?
|
||||
"Local Mode" :
|
||||
"Offline" :
|
||||
apiStatus === NetworkStatus.OFFLINE ?
|
||||
"API Error" :
|
||||
""}
|
||||
</span>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-[35ch]" align="start" side="top">
|
||||
{networkStatus === NetworkStatus.OFFLINE ?
|
||||
canUseLocalData ?
|
||||
"Currently in Local data mode due to current network connection failure." :
|
||||
"Your network environment is abnormal and you are unable to request a remote server." :
|
||||
apiStatus === NetworkStatus.OFFLINE ?
|
||||
"Your network connection is normal, there may be an error on the remote server and the API interface is temporarily unreachable." :
|
||||
""}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ import { isHotkeyPressed, useHotkeys } from "react-hotkeys-hook"
|
|||
import { Link } from "react-router-dom"
|
||||
|
||||
import { Vibrancy } from "../../components/ui/background"
|
||||
import { NetworkStatusIndicator } from "../app/NetworkStatusIndicator"
|
||||
import { FeedList } from "./list"
|
||||
|
||||
const lethargy = new Lethargy()
|
||||
|
|
@ -139,7 +140,10 @@ export function FeedColumn() {
|
|||
{APP_NAME}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2" onClick={stopPropagation}>
|
||||
<div
|
||||
className="relative flex items-center gap-2"
|
||||
onClick={stopPropagation}
|
||||
>
|
||||
<Link to="/discover" tabIndex={-1}>
|
||||
<ActionButton shortcut="Meta+T" tooltip="Add">
|
||||
<i className="i-mgc-add-cute-re size-5 text-theme-vibrancyFg" />
|
||||
|
|
@ -181,11 +185,11 @@ export function FeedColumn() {
|
|||
{views.map((item, index) => (
|
||||
<section
|
||||
key={item.name}
|
||||
className="min-h-full w-64 shrink-0 snap-center overflow-y-auto"
|
||||
className="min-h-full w-64 shrink-0 snap-center overflow-y-auto pb-8"
|
||||
>
|
||||
{active === index && (
|
||||
<FeedList
|
||||
className="flex size-full flex-col px-3 pb-6 text-sm"
|
||||
className="flex min-h-full w-full flex-col px-3 text-sm"
|
||||
view={index}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -194,8 +198,12 @@ export function FeedColumn() {
|
|||
</m.div>
|
||||
</div>
|
||||
{APP_VERSION?.[0] === "0" && (
|
||||
<div className="pointer-events-none absolute bottom-3 w-full text-center text-xs opacity-20">Early Access</div>
|
||||
<div className="pointer-events-none absolute bottom-3 w-full text-center text-xs opacity-20">
|
||||
Early Access
|
||||
</div>
|
||||
)}
|
||||
|
||||
<NetworkStatusIndicator />
|
||||
</Vibrancy>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,7 +115,9 @@ export const TipModalContent: FC<{
|
|||
<p className="leading-none">
|
||||
<span className="text-xs text-theme-foreground/80">No one has claimed this feed yet. The received Power will be securely held in the blockchain contract until it is claimed.</span>
|
||||
</p>
|
||||
<StyledButton variant="text" className="w-fit p-0" onClick={() => claimFeed()}>Claim this feed</StyledButton>
|
||||
<div className="text-center">
|
||||
<StyledButton variant="text" className="w-fit p-0" onClick={() => claimFeed()}>Claim this feed</StyledButton>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<Divider className="my-2" />
|
||||
|
|
@ -139,16 +141,18 @@ export const TipModalContent: FC<{
|
|||
</div>
|
||||
</RadioGroup>
|
||||
|
||||
<Divider className="my-2" />
|
||||
|
||||
{/* low balance notice */}
|
||||
{wrongNumberRange && (
|
||||
<div className="text-xs text-red-500">
|
||||
<span>
|
||||
Your balance is not enough to cover this tip. Please adjust the
|
||||
amount.
|
||||
</span>
|
||||
</div>
|
||||
<>
|
||||
<Divider className="my-2" />
|
||||
<div className="text-xs text-red-500">
|
||||
<span>
|
||||
Your balance is not enough to cover this tip. Please adjust the
|
||||
amount.
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue