feat: add `NetworkStatusIndicator`

Signed-off-by: Innei <i@innei.in>
This commit is contained in:
Innei 2024-07-08 18:58:14 +08:00
parent 6fab907ed3
commit 6023993d3a
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
11 changed files with 158 additions and 17 deletions

View File

@ -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)
}
}

View File

@ -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"

View File

@ -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"

View File

@ -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,
)}
>

View File

@ -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
}

View File

@ -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(),

View File

@ -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,

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>