refactor: useSync

This commit is contained in:
Ou 2024-10-17 15:33:33 +08:00
parent d2fcae1802
commit 3dc9c4b28c
8 changed files with 87 additions and 92 deletions

View File

@ -43,7 +43,6 @@ export default defineEventHandler(async (event) => {
}, },
}) })
console.log(userInfo)
const userID = String(userInfo.id) const userID = String(userInfo.id)
await userTable.addUser(userID, userInfo.notification_email || userInfo.email, "github") await userTable.addUser(userID, userInfo.notification_email || userInfo.email, "github")

View File

@ -68,12 +68,12 @@ export const originSources = {
}, },
"hupu": { "hupu": {
name: "虎扑", name: "虎扑",
active: false, disable: true,
home: "https://hupu.com", home: "https://hupu.com",
}, },
"tieba": { "tieba": {
name: "百度贴吧", name: "百度贴吧",
active: false, disable: true,
home: "https://tieba.baidu.com", home: "https://tieba.baidu.com",
}, },
"toutiao": { "toutiao": {
@ -91,13 +91,13 @@ export const originSources = {
"thepaper": { "thepaper": {
name: "澎湃新闻", name: "澎湃新闻",
interval: Time.Common, interval: Time.Common,
active: false, disable: true,
home: "https://www.thepaper.cn", home: "https://www.thepaper.cn",
}, },
"sputniknewscn": { "sputniknewscn": {
name: "卫星通讯社", name: "卫星通讯社",
color: "orange", color: "orange",
active: false, disable: true,
home: "https://sputniknews.cn", home: "https://sputniknews.cn",
}, },
"cankaoxiaoxi": { "cankaoxiaoxi": {
@ -116,7 +116,7 @@ function genSources() {
const parent = { const parent = {
name: source.name, name: source.name,
type: source.type, type: source.type,
active: source.active ?? true, disable: source.disable,
color: source.color ?? "red", color: source.color ?? "red",
interval: source.interval ?? Time.Default, interval: source.interval ?? Time.Default,
} }
@ -139,5 +139,5 @@ function genSources() {
} }
}) })
return typeSafeObjectFromEntries(_.filter(([_, v]) => v.active)) return typeSafeObjectFromEntries(_.filter(([_, v]) => !v.disable))
} }

View File

@ -8,10 +8,10 @@ type ConstSources = typeof originSources
type MainSourceID = keyof(ConstSources) type MainSourceID = keyof(ConstSources)
export type SourceID = { export type SourceID = {
[Key in MainSourceID]: ConstSources[Key] extends { active?: false } ? never : [Key in MainSourceID]: ConstSources[Key] extends { disable?: true } ? never :
ConstSources[Key] extends { sub?: infer SubSource } ? { ConstSources[Key] extends { sub?: infer SubSource } ? {
// @ts-expect-error >_< // @ts-expect-error >_<
[SubKey in keyof SubSource ]: SubSource[SubKey] extends { active?: false } ? never : `${Key}-${SubKey}` [SubKey in keyof SubSource ]: SubSource[SubKey] extends { disable?: true } ? never : `${Key}-${SubKey}`
}[keyof SubSource] | Key : Key; }[keyof SubSource] | Key : Key;
}[MainSourceID] }[MainSourceID]
@ -33,15 +33,15 @@ export interface OriginSource {
interval?: number interval?: number
type?: "hottest" | "realtime" type?: "hottest" | "realtime"
/** /**
* @default true * @default false
*/ */
active?: boolean disable?: boolean
home: string home: string
color?: Color color?: Color
sub?: Record<string, { sub?: Record<string, {
title: string title: string
type?: "hottest" | "realtime" type?: "hottest" | "realtime"
active?: boolean disable?: boolean
interval?: number interval?: number
}> }>
} }
@ -51,7 +51,7 @@ export interface Source {
title?: string title?: string
type?: "hottest" | "realtime" type?: "hottest" | "realtime"
color: Color color: Color
active: boolean disable?: boolean
interval: number interval: number
redirect?: SourceID redirect?: SourceID
} }

View File

@ -4,6 +4,7 @@ import type { PrimitiveAtom } from "jotai"
import { atom } from "jotai" import { atom } from "jotai"
import type { PrimitiveMetadata } from "@shared/types" import type { PrimitiveMetadata } from "@shared/types"
import { verifyPrimitiveMetadata } from "@shared/verify" import { verifyPrimitiveMetadata } from "@shared/verify"
import { sources } from "@shared/sources"
import type { Update } from "./types" import type { Update } from "./types"
function createPrimitiveMetadataAtom( function createPrimitiveMetadataAtom(
@ -17,40 +18,42 @@ function createPrimitiveMetadataAtom(
if (item) { if (item) {
const stored = JSON.parse(item) as PrimitiveMetadata const stored = JSON.parse(item) as PrimitiveMetadata
verifyPrimitiveMetadata(stored) verifyPrimitiveMetadata(stored)
return preprocess(stored) return preprocess({
...stored,
action: "init",
})
} }
} catch { } } catch { }
return initialValue return initialValue
} }
const baseAtom = atom(getInitialValue()) const baseAtom = atom(getInitialValue())
const derivedAtom = atom( const derivedAtom = atom(get => get(baseAtom), (get, set, update: Update<PrimitiveMetadata>) => {
get => get(baseAtom),
(get, set, update: Update<PrimitiveMetadata>) => {
const nextValue = update instanceof Function ? update(get(baseAtom)) : update const nextValue = update instanceof Function ? update(get(baseAtom)) : update
if (nextValue.updatedTime > get(baseAtom).updatedTime) {
set(baseAtom, nextValue) set(baseAtom, nextValue)
localStorage.setItem(key, JSON.stringify(nextValue)) localStorage.setItem(key, JSON.stringify(nextValue))
}, }
) })
return derivedAtom return derivedAtom
} }
const initialMetadata = typeSafeObjectFromEntries(typeSafeObjectEntries(metadata).map(([id, val]) => [id, val.sources])) const initialMetadata = typeSafeObjectFromEntries(typeSafeObjectEntries(metadata).map(([id, val]) => [id, val.sources]))
export function preprocessMetadata(target: PrimitiveMetadata, action: PrimitiveMetadata["action"] = "init") { export function preprocessMetadata(target: PrimitiveMetadata) {
return { return {
data: { data: {
...initialMetadata, ...initialMetadata,
...typeSafeObjectFromEntries( ...typeSafeObjectFromEntries(
typeSafeObjectEntries(target.data) typeSafeObjectEntries(target.data)
.filter(([id]) => initialMetadata[id]) .filter(([id]) => initialMetadata[id])
.map(([id, sources]) => { .map(([id, s]) => {
if (id === "focus") return [id, sources] if (id === "focus") return [id, s.filter(k => sources[k])]
const oldS = sources.filter(k => initialMetadata[id].includes(k)) const oldS = s.filter(k => initialMetadata[id].includes(k))
const newS = initialMetadata[id].filter(k => !oldS.includes(k)) const newS = initialMetadata[id].filter(k => !oldS.includes(k))
return [id, [...oldS, ...newS]] return [id, [...oldS, ...newS]]
}), }),
), ),
}, },
action, action: target.action,
updatedTime: target.updatedTime, updatedTime: target.updatedTime,
} as PrimitiveMetadata } as PrimitiveMetadata
} }

View File

@ -13,7 +13,7 @@ export function Column({ id }: { id: ColumnID }) {
useEffect(() => { useEffect(() => {
setCurrentColumnID(id) setCurrentColumnID(id)
}, [id, setCurrentColumnID]) }, [id, setCurrentColumnID])
useTitle(`NewsNow ${metadata[id].name}`) useTitle(`NewsNow | ${metadata[id].name}`)
return ( return (
<> <>
<div className="w-full flex justify-center"> <div className="w-full flex justify-center">

View File

@ -46,13 +46,13 @@ export function OverlayScrollbar({ children, options, events, defer, ...props }:
export function GlobalOverlayScrollbar({ children, ...props }: PropsWithChildren<HTMLProps<HTMLDivElement>>) { export function GlobalOverlayScrollbar({ children, ...props }: PropsWithChildren<HTMLProps<HTMLDivElement>>) {
const ref = useRef<HTMLDivElement>(null) const ref = useRef<HTMLDivElement>(null)
const lastScroll = useRef(0) const lastTrigger = useRef(0)
const timer = useRef<any>() const timer = useRef<any>()
const setGoToTop = useSetAtom(goToTopAtom) const setGoToTop = useSetAtom(goToTopAtom)
const onScroll = useCallback((e: Event) => { const onScroll = useCallback((e: Event) => {
const now = Date.now() const now = Date.now()
if (now - lastScroll.current > 50) { if (now - lastTrigger.current > 50) {
lastScroll.current = now lastTrigger.current = now
clearTimeout(timer.current) clearTimeout(timer.current)
timer.current = setTimeout( timer.current = setTimeout(
() => { () => {

View File

@ -1,18 +0,0 @@
import { useEffect, useRef, useState } from "react"
export function useSticky() {
const ref = useRef<HTMLDivElement>(null)
const [isSticky, setIsSticky] = useState(false)
useEffect(() => {
const observer = new IntersectionObserver(
([event]) => setIsSticky(event.intersectionRatio < 1),
{ threshold: [1], rootMargin: "-1px 0px 0px 0px" },
)
observer.observe(ref.current!)
return () => observer.disconnect()
}, [])
return { ref, isSticky }
}

View File

@ -1,16 +1,14 @@
import type { PrimitiveMetadata } from "@shared/types" import type { PrimitiveMetadata } from "@shared/types"
import { useAtom } from "jotai" import { useAtom } from "jotai"
import { ofetch } from "ofetch" import { ofetch } from "ofetch"
import { useCallback, useEffect } from "react" import { useEffect } from "react"
import { useDebounce } from "react-use"
import { preprocessMetadata, primitiveMetadataAtom } from "~/atoms" import { preprocessMetadata, primitiveMetadataAtom } from "~/atoms"
export function useSync() { export async function uploadMetadata(metadata: PrimitiveMetadata) {
const [primitiveMetadata, setPrimitiveMetadata] = useAtom(primitiveMetadataAtom)
const uploadMetadata = useCallback(async () => {
if (!__ENABLE_LOGIN__) return if (!__ENABLE_LOGIN__) return
const jwt = localStorage.getItem("user_jwt") const jwt = localStorage.getItem("user_jwt")
if (!jwt) return if (!jwt) return
if (primitiveMetadata.action !== "manual") return
try { try {
await ofetch("/api/me/sync", { await ofetch("/api/me/sync", {
method: "POST", method: "POST",
@ -18,16 +16,16 @@ export function useSync() {
Authorization: `Bearer ${jwt}`, Authorization: `Bearer ${jwt}`,
}, },
body: { body: {
data: primitiveMetadata.data, data: metadata.data,
updatedTime: primitiveMetadata.updatedTime, updatedTime: metadata.updatedTime,
}, },
}) })
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} }
}, [primitiveMetadata]) }
const downloadMetadata = useCallback(async () => { export async function downloadMetadata(): Promise<PrimitiveMetadata | undefined> {
if (!__ENABLE_LOGIN__) return if (!__ENABLE_LOGIN__) return
const jwt = localStorage.getItem("user_jwt") const jwt = localStorage.getItem("user_jwt")
if (!jwt) return if (!jwt) return
@ -37,21 +35,34 @@ export function useSync() {
Authorization: `Bearer ${jwt}`, Authorization: `Bearer ${jwt}`,
}, },
}) as PrimitiveMetadata }) as PrimitiveMetadata
if (data && updatedTime > primitiveMetadata.updatedTime) {
// 不用同步 action 字段 // 不用同步 action 字段
setPrimitiveMetadata(preprocessMetadata({ action: "sync", data, updatedTime }, "sync")) if (data) {
return {
action: "sync",
data,
updatedTime,
}
} }
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} }
// 只需要在初始化时执行一次 }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setPrimitiveMetadata]) export function useSync() {
const [primitiveMetadata, setPrimitiveMetadata] = useAtom(primitiveMetadataAtom)
useEffect(() => {
downloadMetadata() useDebounce(async () => {
}, [setPrimitiveMetadata, downloadMetadata]) if (primitiveMetadata.action === "manual") {
useEffect(() => { uploadMetadata(primitiveMetadata)
uploadMetadata() }
}, [primitiveMetadata, uploadMetadata]) }, 10000, [primitiveMetadata])
useEffect(() => {
const fn = async () => {
const metadata = await downloadMetadata()
if (metadata) {
setPrimitiveMetadata(preprocessMetadata(metadata))
}
}
fn()
}, [setPrimitiveMetadata])
} }