Skip to content

Commit

Permalink
feat: setting sync (#273)
Browse files Browse the repository at this point in the history
* init

Signed-off-by: Innei <i@innei.in>

* feat: offline

Signed-off-by: Innei <i@innei.in>

* fix: update

Signed-off-by: Innei <i@innei.in>

* fix: chain

Signed-off-by: Innei <i@innei.in>

* update

Signed-off-by: Innei <i@innei.in>

* update

Signed-off-by: Innei <i@innei.in>

* update

Signed-off-by: Innei <i@innei.in>

* update

Signed-off-by: Innei <i@innei.in>

* feat: render social media with full text

---------

Signed-off-by: Innei <i@innei.in>
Co-authored-by: DIYgod <i@diygod.me>
  • Loading branch information
Innei and DIYgod authored Sep 5, 2024
1 parent 1531d2b commit f1a3481
Show file tree
Hide file tree
Showing 19 changed files with 1,111 additions and 507 deletions.
1,029 changes: 585 additions & 444 deletions src/hono.ts

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions src/renderer/src/atoms/settings/general.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,9 @@ export const subscribeShouldUseIndexedDB = (
) =>
jotaiStore.sub(__generalSettingAtom, () =>
callback(getGeneralSettings().dataPersist))

export const generalServerSyncWhiteListKeys: (keyof GeneralSettings)[] = [
"appLaunchOnStartup",
"dataPersist",
"sendAnonymousData",
]
27 changes: 25 additions & 2 deletions src/renderer/src/atoms/settings/helper.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useRefValue } from "@renderer/hooks/common"
import { EventBus } from "@renderer/lib/event-bus"
import { createAtomHooks } from "@renderer/lib/jotai"
import { getStorageNS } from "@renderer/lib/ns"
import type { SettingItem } from "@renderer/modules/settings/setting-builder"
Expand All @@ -7,6 +8,16 @@ import { useAtomValue } from "jotai"
import { atomWithStorage, selectAtom } from "jotai/utils"
import { useMemo } from "react"

declare module "@renderer/lib/event-bus" {
interface CustomEvent {
SETTING_CHANGE_EVENT: {
updated: number
payload: Record<string, any>
key: string
}
}
}

export const createSettingAtom = <T extends object>(
settingKey: string,
createDefaultSettings: () => T,
Expand All @@ -19,6 +30,7 @@ export const createSettingAtom = <T extends object>(
getOnInit: true,
},
)

const [, , useSettingValue, , getSettings, setSettings] =
createAtomHooks(atom)

Expand Down Expand Up @@ -53,9 +65,18 @@ export const createSettingAtom = <T extends object>(
key: K,
value: ReturnType<typeof getSettings>[K],
) => {
const updated = Date.now()
setSettings({
...getSettings(),
[key]: value,

updated,
})

EventBus.dispatch("SETTING_CHANGE_EVENT", {
payload: { [key]: value },
updated,
key: settingKey,
})
}

Expand Down Expand Up @@ -104,8 +125,10 @@ export const createDefineSettingItem =
description?: string | JSX.Element
onChange?: (value: T[K]) => void
hide?: boolean

} & Omit<SettingItem<any>, "onChange" | "description" | "label" | "hide" | "key">,
} & Omit<
SettingItem<any>,
"onChange" | "description" | "label" | "hide" | "key"
>,
): any => {
const { label, description, onChange, hide, ...rest } = options

Expand Down
8 changes: 8 additions & 0 deletions src/renderer/src/atoms/settings/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,12 @@ export const {
initializeDefaultSettings: initializeDefaultUISettings,
getSettings: getUISettings,
useSettingValue: useUISettingValue,
settingAtom: __uiSettingAtom,
} = createSettingAtom("ui", createDefaultSettings)

export const uiServerSyncWhiteListKeys: (keyof UISettings)[] = [
"uiFontFamily",
"readerFontFamily",
"opaqueSidebar",
"voice",
]
18 changes: 18 additions & 0 deletions src/renderer/src/components/icons/PhCloudCheck.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { SVGProps } from "react"

export function PhCloudCheck(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 256 256"
{...props}
>
<path
fill="currentColor"
d="M160 40a88.09 88.09 0 0 0-78.71 48.67A64 64 0 1 0 72 216h88a88 88 0 0 0 0-176m0 160H72a48 48 0 0 1 0-96c1.1 0 2.2 0 3.29.11A88 88 0 0 0 72 128a8 8 0 0 0 16 0a72 72 0 1 1 72 72m37.66-93.66a8 8 0 0 1 0 11.32l-48 48a8 8 0 0 1-11.32 0l-24-24a8 8 0 0 1 11.32-11.32L144 148.69l42.34-42.35a8 8 0 0 1 11.32 0"
/>
</svg>
)
}
7 changes: 7 additions & 0 deletions src/renderer/src/components/icons/PhCloudX.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { SVGProps } from "react"

export function PhCloudX(props: SVGProps<SVGSVGElement>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256" {...props}><path fill="currentColor" d="M160 40a88.09 88.09 0 0 0-78.71 48.67A64 64 0 1 0 72 216h88a88 88 0 0 0 0-176m0 160H72a48 48 0 0 1 0-96c1.1 0 2.2 0 3.29.11A88 88 0 0 0 72 128a8 8 0 0 0 16 0a72 72 0 1 1 72 72m29.66-82.34L171.31 136l18.35 18.34a8 8 0 0 1-11.32 11.32L160 147.31l-18.34 18.35a8 8 0 0 1-11.32-11.32L148.69 136l-18.35-18.34a8 8 0 0 1 11.32-11.32L160 124.69l18.34-18.35a8 8 0 0 1 11.32 11.32" /></svg>
)
}
48 changes: 11 additions & 37 deletions src/renderer/src/initialize/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@ import { repository } from "@pkg"
import { getUISettings } from "@renderer/atoms/settings/ui"
import { isElectronBuild } from "@renderer/constants"
import { browserDB } from "@renderer/database"
import { getStorageNS } from "@renderer/lib/ns"
import { ElectronCloseEvent, ElectronShowEvent } from "@renderer/providers/invalidate-query-provider"
import { settingSyncQueue } from "@renderer/modules/settings/helper/sync-queue"
import {
ElectronCloseEvent,
ElectronShowEvent,
} from "@renderer/providers/invalidate-query-provider"
import { CleanerService } from "@renderer/services/cleaner"
import { registerGlobalContext } from "@shared/bridge"
import dayjs from "dayjs"
import duration from "dayjs/plugin/duration"
import localizedFormat from "dayjs/plugin/localizedFormat"
import relativeTime from "dayjs/plugin/relativeTime"
import { enableMapSet } from "immer"
import { createElement } from "react"
import { toast } from "sonner"

import { subscribeNetworkStatus } from "../atoms/network"
Expand All @@ -27,8 +29,8 @@ import {
hydrateSettings,
setHydrated,
} from "./hydrate"
import { doMigration } from "./migrates"
import { initPostHog } from "./posthog"
import { pushAfterReadyCallback } from "./queue"
import { initSentry } from "./sentry"

const cleanup = subscribeShouldUseIndexedDB((value) => {
Expand All @@ -48,8 +50,6 @@ declare global {
}
}

const appVersionKey = getStorageNS("app_version")

export const initializeApp = async () => {
appLog(`${APP_NAME}: Next generation information browser`, repository.url)
appLog(`Initialize ${APP_NAME}...`)
Expand All @@ -62,37 +62,7 @@ export const initializeApp = async () => {
"electron" :
"web"

const lastVersion = localStorage.getItem(appVersionKey)

if (lastVersion && lastVersion !== APP_VERSION) {
appLog(`Upgrade from ${lastVersion} to ${APP_VERSION}`)

pushAfterReadyCallback(() => {
setTimeout(() => {
toast.success(
// `App is upgraded to ${APP_VERSION}, enjoy the new features! 🎉`,
createElement("div", {
children: [
"App is upgraded to ",
createElement(
"a",
{
href: `${repository.url}/releases/tag/${APP_VERSION}`,
target: "_blank",
className: "underline",
},
createElement("strong", {
children: APP_VERSION,
}),
),
", enjoy the new features! 🎉",
],
}),
)
}, 1000)
})
}
localStorage.setItem(appVersionKey, APP_VERSION)
doMigration()

// Initialize dayjs
dayjs.extend(duration)
Expand Down Expand Up @@ -122,6 +92,10 @@ export const initializeApp = async () => {
})

hydrateSettings()

settingSyncQueue.init()
settingSyncQueue.syncLocal()

// should after hydrateSettings
const { dataPersist: enabledDataPersist, sendAnonymousData } =
getGeneralSettings()
Expand Down
43 changes: 43 additions & 0 deletions src/renderer/src/initialize/migrates/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { repository } from "@pkg"
import { appLog } from "@renderer/lib/log"
import { getStorageNS } from "@renderer/lib/ns"
import { createElement } from "react"
import { toast } from "sonner"

import { waitAppReady } from "../queue"

const appVersionKey = getStorageNS("app_version")

export const doMigration = () => {
const lastVersion = localStorage.getItem(appVersionKey)

if (lastVersion && lastVersion !== APP_VERSION) {
appLog(`Upgrade from ${lastVersion} to ${APP_VERSION}`)

waitAppReady(() => {
toast.success(
// `App is upgraded to ${APP_VERSION}, enjoy the new features! 🎉`,
createElement("div", {
children: [
"App is upgraded to ",
createElement(
"a",
{
href: `${repository.url}/releases/tag/${APP_VERSION}`,
target: "_blank",
className: "underline",
},
createElement("strong", {
children: APP_VERSION,
}),
),
", enjoy the new features! 🎉",
],
}),
)
}, 1000)

// NOTE: Add migration logic here
}
localStorage.setItem(appVersionKey, APP_VERSION)
}
4 changes: 2 additions & 2 deletions src/renderer/src/initialize/queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { appIsReady } from "@renderer/atoms/app"

const afterReadyCallbackQueue = [] as Array<() => void>

export const pushAfterReadyCallback = (callback: () => void) => {
export const waitAppReady = (callback: () => void, delay = 0) => {
if (appIsReady()) {
callback()
delay ? callback() : setTimeout(callback, delay)
} else {
afterReadyCallbackQueue.push(callback)
}
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/src/lib/defineQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export type DefinedQuery<TQueryKey extends QueryKey, TData> = Readonly<{
invalidateRoot: () => void

refetch: () => Promise<TData | undefined>
prefetch: () => Promise<void>
prefetch: () => Promise<TData | undefined>

setData: <Data = TData>(
updater: (draft: Draft<Data>) => ValidRecipeReturnType<Draft<Data>>
Expand Down Expand Up @@ -98,6 +98,7 @@ export function defineQuery<
queryKey: key,
queryFn: fn,
})
return queryClient.getQueryData<TData>(key)
},
cancel: async (keyExtactor) => {
const queryKey =
Expand Down
35 changes: 35 additions & 0 deletions src/renderer/src/lib/event-bus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface CustomEvent {}
export interface EventBusMap extends CustomEvent {}

class EventBusEvent extends Event {
static type = "EventBusEvent"
constructor(public _type: string, public data: any) {
super(EventBusEvent.type)
}
}
class EventBusStatic {
dispatch<T extends keyof EventBusMap>(event: T, data: EventBusMap[T]) {
window.dispatchEvent(new EventBusEvent(event, data))
}

subscribe<T extends keyof EventBusMap>(
event: T,
callback: (data: EventBusMap[T]) => void,
) {
const handler = (e: any) => {
if (e instanceof EventBusEvent && e._type === event) {
callback(e.data)
}
}
window.addEventListener(EventBusEvent.type, handler)

return this.unsubscribe.bind(this, event, handler)
}

unsubscribe(event: string, handler: (e: any) => void) {
window.removeEventListener(EventBusEvent.type, handler)
}
}

export const EventBus = new EventBusStatic()
4 changes: 3 additions & 1 deletion src/renderer/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ export function formatXml(xml: string, indent = 4) {
}

export const sleep = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms))
new Promise<void>((resolve) => setTimeout(resolve, ms))

export const capitalizeFirstLetter = (string: string) =>
string.charAt(0).toUpperCase() + string.slice(1)
Expand Down Expand Up @@ -247,3 +247,5 @@ export const getUrlIcon = (url: string, fallback?: boolean | undefined) => {

return ret
}

export const isEmptyObject = (obj: Record<string, any>) => Object.keys(obj).length === 0
2 changes: 1 addition & 1 deletion src/renderer/src/modules/feed-column/corner-player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const CornerPlayer = () => {
{show && entry && feed && (
<m.div
key="corner-player"
className="group relative z-10 !mb-0 w-full pr-px"
className="group relative z-10 !my-0 w-full pr-px"
initial={{ y: 50, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 50, opacity: 0 }}
Expand Down
49 changes: 49 additions & 0 deletions src/renderer/src/modules/settings/helper/SyncIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { PhCloudCheck } from "@renderer/components/icons/PhCloudCheck"
import { PhCloudX } from "@renderer/components/icons/PhCloudX"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@renderer/components/ui/tooltip"
import { useAuthQuery, useIsOnline } from "@renderer/hooks/common"
import { settings } from "@renderer/queries/settings"
import { useEffect, useRef } from "react"

import { settingSyncQueue } from "./sync-queue"

export const SyncIndicator = () => {
const { data: remoteSettings, isLoading } = useAuthQuery(settings.get(), {})

const isOnline = useIsOnline()
const onceRef = useRef(false)
useEffect(() => {
if (!isLoading && remoteSettings && !onceRef.current) {
const hasSetting = JSON.stringify(remoteSettings.settings) !== "{}"
onceRef.current = true
if (hasSetting) {
return
}
// Replace local to remote
settingSyncQueue.replaceRemote()
}
}, [remoteSettings, isLoading])

return (
<Tooltip>
<TooltipTrigger asChild>
<div className="center absolute right-2 size-5">
{isOnline ? (
<PhCloudCheck className="size-4" />
) : (
<PhCloudX className="size-4" />
)}
</div>
</TooltipTrigger>
<TooltipContent>
<div className="text-center text-xs">
{isOnline ? "Synced with server" : "Offline"}
</div>
</TooltipContent>
</Tooltip>
)
}
Loading

0 comments on commit f1a3481

Please sign in to comment.