Merge branch 'main' into dev
This commit is contained in:
commit
df80682b2c
|
|
@ -0,0 +1,33 @@
|
|||
# What's New in v0.6.0
|
||||
|
||||
## Shiny New Things
|
||||
|
||||
- Import and export your Actions (394d00f)
|
||||
- Add a bio, website, and social links to your profile (507a525)
|
||||
- Upload a profile picture
|
||||
- Use video duration as an Action condition
|
||||
|
||||
## Improvements
|
||||
|
||||
- A snazzy new look for your personal profile
|
||||
- Redesigned the Actions page (1ace5ea)
|
||||
- Redesigned the RSSHub page (f9aca60)
|
||||
- Added length limits to certain profile fields
|
||||
- Simplified default commands in the entry tool (85122fb)
|
||||
- Enhanced UI labels and descriptions for clarity (2ed9f70)
|
||||
- Gradually rolling out an experimental unified local database for mobile and desktop (#3897 #3902)
|
||||
- Polished image-preview styling (cf72753)
|
||||
- Refined toast notifications (73f8011)
|
||||
|
||||
## No Longer Broken
|
||||
|
||||
- More reliable automatic recovery after database-migration failures (c2e0c3d)
|
||||
- Fixed unread counts not clearing in the macOS Docker build (70255af)
|
||||
- Fixed old entries showing during initial load (24ae065)
|
||||
- Fixed handling of links starting with `.` (de8eac8)
|
||||
- Fixed text-to-speech not working (82952b0)
|
||||
- Fixed star/unstar status not syncing across devices (fbd0b3)
|
||||
|
||||
## Thanks
|
||||
|
||||
Special thanks to volunteer contributors @kovsu @huanfe1 @cscnk52 @Olexandr88 @0-o0 @kingsword09 @ericyzhu for their valuable contributions
|
||||
|
|
@ -1,29 +1,11 @@
|
|||
# What's new in vNEXT_VERSION
|
||||
|
||||
## ⚠️ Important
|
||||
|
||||
We’ve made some updates to our `user` database to help keep things running smoothly and securely. Some fields—like your name, email, profile image link, handle, bio, and website—now have maximum character limits:
|
||||
|
||||
- Name: up to 64 characters
|
||||
- Email: up to 64 characters
|
||||
- Profile image link: up to 256 characters
|
||||
- Handle: up to 36 characters
|
||||
- Bio: up to 256 characters
|
||||
- Website: up to 256 characters
|
||||
|
||||
If you previously entered information that’s longer than these limits, it will be automatically shortened to fit. Other fields, like your email verification status, two-factor authentication, and social links, are not affected by these changes.
|
||||
|
||||
## Shiny new things
|
||||
|
||||
- 🎉 Say hello to a snazzy new look for your personal profile! We've sprinkled in some cool social attribute settings to spice things up. But hold onto your hats—this is just the appetizer for our grand social vision! 🚀 Stay tuned for more! 😎
|
||||
- We have redesigned the pages for Discover, RSSHub, and Actions. These pages are now simpler and easier to use, with a more modern UI.
|
||||
|
||||
## Improvements
|
||||
|
||||
## No longer broken
|
||||
|
||||
🎉 Ta-da! We’ve squashed a bunch of pesky bugs you awesome folks in the community pointed out—high fives all around! 🙌 But if something’s still acting wonky, don’t be shy—holler at us with an issue report, pretty please! 😜 Let’s keep the good vibes rolling! 🚀
|
||||
|
||||
## Thanks
|
||||
|
||||
Special thanks to volunteer contributors @ for their valuable contributions
|
||||
|
|
|
|||
|
|
@ -5,5 +5,6 @@ declare global {
|
|||
electron?: ElectronAPI
|
||||
api?: { canWindowBlur: boolean }
|
||||
platform: NodeJS.Platform
|
||||
mas: boolean
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ if (process.contextIsolated) {
|
|||
contextBridge.exposeInMainWorld("electron", electronAPI)
|
||||
contextBridge.exposeInMainWorld("api", api)
|
||||
contextBridge.exposeInMainWorld("platform", process.platform)
|
||||
contextBridge.exposeInMainWorld("mas", process.mas)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
|
@ -49,6 +50,8 @@ if (process.contextIsolated) {
|
|||
window.api = api
|
||||
// @ts-ignore (define in dts)
|
||||
window.platform = process.platform
|
||||
// @ts-ignore (define in dts)
|
||||
window.mas = process.mas
|
||||
|
||||
Object.defineProperty(window.navigator, "clipboard", {
|
||||
get: () => {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ declare global {
|
|||
electron?: ElectronAPI
|
||||
api?: { canWindowBlur: boolean }
|
||||
platform: NodeJS.Platform
|
||||
mas: boolean
|
||||
}
|
||||
export const APP_NAME = "Folo"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,5 @@ export const [, , useServerConfigs, , getServerConfigs, setServerConfigs] = crea
|
|||
|
||||
export const useIsInMASReview = () => {
|
||||
const serverConfigs = useServerConfigs()
|
||||
return (
|
||||
typeof process !== "undefined" &&
|
||||
process.mas &&
|
||||
serverConfigs?.MAS_IN_REVIEW_VERSION === PKG.version
|
||||
)
|
||||
return window.mas && serverConfigs?.MAS_IN_REVIEW_VERSION === PKG.version
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,12 +57,12 @@ export const useDeleteSubscription = ({ onSuccess }: { onSuccess?: () => void }
|
|||
toast.dismiss(toastId)
|
||||
}
|
||||
|
||||
const toastId = toast("", {
|
||||
const toastId = toast.warning("", {
|
||||
duration: 3000,
|
||||
description: <UnfollowInfo title={feed.title!} undo={undo} />,
|
||||
action: {
|
||||
label: (
|
||||
<span className="flex items-center gap-1">
|
||||
<span className={"flex items-center gap-1 px-1"}>
|
||||
{t("words.undo")}
|
||||
<Kbd className="border-border inline-flex items-center border bg-transparent text-white">
|
||||
$mod+Z
|
||||
|
|
@ -95,13 +95,15 @@ const UnfollowInfo = ({ title, undo }: { title: string; undo: () => any }) => {
|
|||
preventDefault: true,
|
||||
})
|
||||
return (
|
||||
<Trans
|
||||
ns="app"
|
||||
i18nKey="notify.unfollow_feed"
|
||||
components={{
|
||||
FeedItem: <i className="mr-px font-semibold">{title}</i>,
|
||||
}}
|
||||
/>
|
||||
<span className="text-text font-medium">
|
||||
<Trans
|
||||
ns="app"
|
||||
i18nKey="notify.unfollow_feed"
|
||||
components={{
|
||||
FeedItem: <i className="mr-px font-semibold">{title}</i>,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import { env } from "@follow/shared/env.desktop"
|
||||
import type { AuthSession } from "@follow/shared/hono"
|
||||
import { setOpenPanelTracker, setPostHogTracker, tracker } from "@follow/tracker"
|
||||
import posthog from "posthog-js"
|
||||
import { setFirebaseTracker, setOpenPanelTracker, tracker } from "@follow/tracker"
|
||||
|
||||
import { QUERY_PERSIST_KEY } from "~/constants/app"
|
||||
|
||||
import { ga4 } from "./ga4"
|
||||
import { op } from "./op"
|
||||
|
||||
export const initAnalytics = async () => {
|
||||
|
|
@ -15,14 +14,9 @@ export const initAnalytics = async () => {
|
|||
language: navigator.language,
|
||||
})
|
||||
|
||||
setFirebaseTracker(ga4)
|
||||
|
||||
setOpenPanelTracker(op)
|
||||
setPostHogTracker(
|
||||
posthog.init(env.VITE_POSTHOG_KEY, {
|
||||
api_host: env.VITE_POSTHOG_HOST,
|
||||
person_profiles: "always",
|
||||
defaults: "2025-05-24",
|
||||
}),
|
||||
)
|
||||
|
||||
let session: AuthSession | undefined
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
import { v4 as uuidv4 } from "uuid"
|
||||
|
||||
import { apiClient } from "~/lib/api-fetch"
|
||||
|
||||
class Analytics4 {
|
||||
private clientID: string
|
||||
private sessionID: string
|
||||
private userID: string | null = null
|
||||
private userProperties: Record<string, { value: unknown }> | null = null
|
||||
|
||||
constructor(clientID: string = uuidv4(), sessionID = uuidv4()) {
|
||||
this.clientID = clientID
|
||||
this.sessionID = sessionID
|
||||
}
|
||||
|
||||
async setUserId(id: string) {
|
||||
this.userID = id
|
||||
}
|
||||
|
||||
async setUserProperties(upValue?: Record<string, unknown>) {
|
||||
const userProperties = Object.entries(upValue || {}).reduce((acc, [key, value]) => {
|
||||
acc[key] = {
|
||||
value,
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
this.userProperties = userProperties
|
||||
}
|
||||
|
||||
async logEvent(eventName: string, params?: Record<string, unknown>): Promise<any> {
|
||||
delete params?.__code
|
||||
delete params?.__eventName
|
||||
|
||||
const payload = {
|
||||
client_id: this.clientID,
|
||||
user_id: this.userID,
|
||||
events: [
|
||||
{
|
||||
name: eventName,
|
||||
params: {
|
||||
session_id: this.sessionID,
|
||||
engagement_time_msec: 1000,
|
||||
...params,
|
||||
},
|
||||
},
|
||||
],
|
||||
user_properties: this.userProperties,
|
||||
}
|
||||
|
||||
return apiClient.data.g.$post({
|
||||
json: payload,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const ga4 = new Analytics4()
|
||||
|
|
@ -61,6 +61,7 @@ export const apiFetch = ofetch.create({
|
|||
{
|
||||
closeButton: true,
|
||||
duration: 10e4,
|
||||
|
||||
classNames: {
|
||||
content: tw`w-full`,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,7 +3,11 @@ import "@follow/components/tailwind"
|
|||
import "./styles/main.css"
|
||||
|
||||
import { IN_ELECTRON, WEB_BUILD } from "@follow/shared/constants"
|
||||
import { apiClientSimpleContext, authClientSimpleContext } from "@follow/store/context"
|
||||
import {
|
||||
apiClientSimpleContext,
|
||||
authClientSimpleContext,
|
||||
queryClientSimpleContext,
|
||||
} from "@follow/store/context"
|
||||
import { getOS } from "@follow/utils/utils"
|
||||
import * as React from "react"
|
||||
import ReactDOM from "react-dom/client"
|
||||
|
|
@ -16,10 +20,12 @@ import { setAppIsReady } from "./atoms/app"
|
|||
import { ElECTRON_CUSTOM_TITLEBAR_HEIGHT } from "./constants"
|
||||
import { initializeApp } from "./initialize"
|
||||
import { registerAppGlobalShortcuts } from "./initialize/global-shortcuts"
|
||||
import { queryClient } from "./lib/query-client"
|
||||
import { router } from "./router"
|
||||
|
||||
apiClientSimpleContext.provide(apiClient)
|
||||
authClientSimpleContext.provide(authClient)
|
||||
queryClientSimpleContext.provide(queryClient)
|
||||
|
||||
initializeApp().finally(() => {
|
||||
import("./push-notification").then(({ registerWebPushNotifications }) => {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { toastStyles } from "@follow/components/ui/toast/styles.js"
|
||||
import { stopPropagation } from "@follow/utils/dom"
|
||||
import { useCallback } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
|
@ -13,10 +14,8 @@ export const NeedActivationToast = (props: { dimiss: () => void }) => {
|
|||
<div>{t("activation.description")}</div>
|
||||
|
||||
<button
|
||||
className="bg-accent shrink-0 text-white"
|
||||
className={toastStyles.actionButton}
|
||||
type="button"
|
||||
data-button="true"
|
||||
data-action="true"
|
||||
onPointerDown={stopPropagation}
|
||||
onClick={useCallback(() => {
|
||||
presentActivationModal()
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { ScrollArea } from "@follow/components/ui/scroll-area/index.js"
|
|||
import { Switch } from "@follow/components/ui/switch/index.jsx"
|
||||
import { FeedViewType } from "@follow/constants"
|
||||
import type { EntryModelSimple, FeedAnalyticsModel, FeedModel } from "@follow/models/types"
|
||||
import { invalidateEntriesQuery } from "@follow/store/entry/hooks"
|
||||
import { useFeedByIdOrUrl } from "@follow/store/feed/hooks"
|
||||
import { useCategories, useSubscriptionByFeedId } from "@follow/store/subscription/hooks"
|
||||
import { subscriptionSyncService } from "@follow/store/subscription/store"
|
||||
|
|
@ -36,7 +37,6 @@ import { getRouteParams } from "~/hooks/biz/useRouteParams"
|
|||
import { useI18n } from "~/hooks/common"
|
||||
import { apiClient } from "~/lib/api-fetch"
|
||||
import { toastFetchError } from "~/lib/error-parser"
|
||||
import { entries as entriesQuery } from "~/queries/entries"
|
||||
import { feed as feedQuery, useFeedQuery } from "~/queries/feed"
|
||||
|
||||
import { ViewSelectorRadioGroup } from "../shared/ViewSelectorRadioGroup"
|
||||
|
|
@ -250,13 +250,7 @@ const FeedInnerForm = ({
|
|||
},
|
||||
onSuccess: (data, variables) => {
|
||||
if (getGeneralSettings().hidePrivateSubscriptionsInTimeline) {
|
||||
entriesQuery
|
||||
.entries({
|
||||
feedId: "all",
|
||||
view: Number(variables.view),
|
||||
excludePrivate: true,
|
||||
})
|
||||
.invalidate({ exact: true })
|
||||
invalidateEntriesQuery({ views: [Number(variables.view)] })
|
||||
}
|
||||
|
||||
if ("unread" in data) {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { LoadingCircle } from "@follow/components/ui/loading/index.jsx"
|
|||
import { Switch } from "@follow/components/ui/switch/index.jsx"
|
||||
import { FeedViewType } from "@follow/constants"
|
||||
import type { ListAnalyticsModel } from "@follow/models/types"
|
||||
import { invalidateEntriesQuery } from "@follow/store/entry/hooks"
|
||||
import { useListById, usePrefetchListById } from "@follow/store/list/hooks"
|
||||
import type { ListModel } from "@follow/store/list/types"
|
||||
import { useSubscriptionByFeedId } from "@follow/store/subscription/hooks"
|
||||
|
|
@ -34,7 +35,6 @@ import { useI18n } from "~/hooks/common"
|
|||
import { apiClient } from "~/lib/api-fetch"
|
||||
import { getFetchErrorMessage, toastFetchError } from "~/lib/error-parser"
|
||||
import { getNewIssueUrl } from "~/lib/issues"
|
||||
import { entries as entriesQuery } from "~/queries/entries"
|
||||
|
||||
import { useTOTPModalWrapper } from "../profile/hooks"
|
||||
import { ViewSelectorRadioGroup } from "../shared/ViewSelectorRadioGroup"
|
||||
|
|
@ -224,13 +224,7 @@ const ListInnerForm = ({
|
|||
},
|
||||
onSuccess: (data, variables) => {
|
||||
if (getGeneralSettings().hidePrivateSubscriptionsInTimeline) {
|
||||
entriesQuery
|
||||
.entries({
|
||||
feedId: "all",
|
||||
view: Number(variables.view),
|
||||
excludePrivate: true,
|
||||
})
|
||||
.invalidate({ exact: true })
|
||||
invalidateEntriesQuery({ views: [Number(variables.view)] })
|
||||
}
|
||||
|
||||
if ("unread" in data) {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import { useEventCallback } from "usehooks-ts"
|
|||
|
||||
import { useActionLanguage, useGeneralSettingKey } from "~/atoms/settings/general"
|
||||
import { MediaContainerWidthProvider } from "~/components/ui/media/MediaContainerWidthProvider"
|
||||
import type { StoreImageType } from "~/store/image"
|
||||
import { imageActions } from "~/store/image"
|
||||
|
||||
import { getMasonryColumnValue, setMasonryColumnValue, useMasonryColumnValue } from "../atoms"
|
||||
|
|
@ -54,6 +55,7 @@ export const PictureMasonry: FC<MasonryProps> = (props) => {
|
|||
const deferIsInitLayout = useDeferredValue(isInitLayout)
|
||||
const restoreDimensions = useEventCallback(async () => {
|
||||
const images = [] as string[]
|
||||
|
||||
data.forEach((entryId) => {
|
||||
const entry = getEntry(entryId)
|
||||
if (!entry) return
|
||||
|
|
@ -68,7 +70,30 @@ export const PictureMasonry: FC<MasonryProps> = (props) => {
|
|||
setIsInitDim(true)
|
||||
})
|
||||
})
|
||||
}, [])
|
||||
}, [restoreDimensions])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const images: StoreImageType[] = []
|
||||
data.forEach((entryId) => {
|
||||
const entry = getEntry(entryId)
|
||||
if (!entry) return
|
||||
|
||||
if (!entry.media) return
|
||||
for (const media of entry.media) {
|
||||
if (!media.height || !media.width) continue
|
||||
|
||||
images.push({
|
||||
src: media.url,
|
||||
width: media.width,
|
||||
height: media.height,
|
||||
ratio: media.width / media.height,
|
||||
})
|
||||
}
|
||||
})
|
||||
if (images.length > 0) {
|
||||
imageActions.saveImages(images)
|
||||
}
|
||||
}, [JSON.stringify(data)])
|
||||
|
||||
const customizeColumn = useMasonryColumnValue()
|
||||
const { containerRef, currentColumn, currentItemWidth, calcItemWidth } = useMasonryColumn(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { views } from "@follow/constants"
|
||||
import { useCollectionEntryList } from "@follow/store/collection/hooks"
|
||||
import {
|
||||
useEntriesQuery,
|
||||
useEntryIdsByFeedId,
|
||||
useEntryIdsByFeedIds,
|
||||
useEntryIdsByInboxId,
|
||||
|
|
@ -21,7 +22,7 @@ import { useGeneralSettingKey } from "~/atoms/settings/general"
|
|||
import { ROUTE_FEED_PENDING } from "~/constants/app"
|
||||
import { useRouteParams } from "~/hooks/biz/useRouteParams"
|
||||
import { useAuthQuery } from "~/hooks/common"
|
||||
import { entries, useEntries } from "~/queries/entries"
|
||||
import { entries } from "~/queries/entries"
|
||||
|
||||
import { useIsPreviewFeed } from "./useIsPreviewFeed"
|
||||
|
||||
|
|
@ -45,8 +46,10 @@ const useRemoteEntries = (): UseEntriesReturn => {
|
|||
inboxId,
|
||||
listId,
|
||||
view,
|
||||
...(unreadOnly === true && !isPreview && { read: false }),
|
||||
...(hidePrivateSubscriptionsInTimeline === true && { excludePrivate: true }),
|
||||
...(unreadOnly === true && !isPreview && { unreadOnly: true }),
|
||||
...(hidePrivateSubscriptionsInTimeline === true && {
|
||||
hidePrivateSubscriptionsInTimeline: true,
|
||||
}),
|
||||
}
|
||||
|
||||
if (feedId && listId && isBizId(feedId)) {
|
||||
|
|
@ -64,7 +67,7 @@ const useRemoteEntries = (): UseEntriesReturn => {
|
|||
view,
|
||||
hidePrivateSubscriptionsInTimeline,
|
||||
])
|
||||
const query = useEntries(entriesOptions)
|
||||
const query = useEntriesQuery(entriesOptions)
|
||||
|
||||
const [fetchedTime, setFetchedTime] = useState<number>()
|
||||
useEffect(() => {
|
||||
|
|
@ -155,15 +158,16 @@ const useLocalEntries = (): UseEntriesReturn => {
|
|||
const allEntries = useEntryStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
const ids = showEntriesByView
|
||||
? (entryIdsByView ?? [])
|
||||
: (getEntryIdsFromMultiplePlace(
|
||||
entryIdsByCollections,
|
||||
entryIdsByFeedId,
|
||||
entryIdsByCategory,
|
||||
entryIdsByListId,
|
||||
entryIdsByInboxId,
|
||||
) ?? [])
|
||||
const ids = isCollection
|
||||
? entryIdsByCollections
|
||||
: showEntriesByView
|
||||
? (entryIdsByView ?? [])
|
||||
: (getEntryIdsFromMultiplePlace(
|
||||
entryIdsByFeedId,
|
||||
entryIdsByCategory,
|
||||
entryIdsByListId,
|
||||
entryIdsByInboxId,
|
||||
) ?? [])
|
||||
|
||||
return ids
|
||||
.map((id) => {
|
||||
|
|
|
|||
|
|
@ -103,12 +103,13 @@ export const GridItemFooter = ({
|
|||
<div className="flex items-center gap-1 truncate text-[13px]">
|
||||
<FeedIcon
|
||||
fallback
|
||||
className="mr-0.5 flex"
|
||||
noMargin
|
||||
className="flex"
|
||||
feed={feeds!}
|
||||
entry={entry?.iconEntry}
|
||||
size={18}
|
||||
/>
|
||||
<span className={cn("min-w-0 truncate", descriptionClassName)}>
|
||||
<span className={cn("min-w-0 truncate pl-1", descriptionClassName)}>
|
||||
<FeedTitle feed={feeds} />
|
||||
</span>
|
||||
<span className={cn("text-zinc-500", timeClassName)}>·</span>
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ import { useNavigateEntry } from "~/hooks/biz/useNavigateEntry"
|
|||
import { getRouteParams, useRouteParamsSelector } from "~/hooks/biz/useRouteParams"
|
||||
import { useContextMenu } from "~/hooks/common/useContextMenu"
|
||||
import { createErrorToaster } from "~/lib/error-parser"
|
||||
import { invalidateEntriesQuery } from "~/queries/entries"
|
||||
import { getPreferredTitle } from "~/store/feed/hooks"
|
||||
|
||||
import { useModalStack } from "../../components/ui/modal/stacked/hooks"
|
||||
|
|
@ -162,12 +161,6 @@ function FeedCategoryImpl({ data: ids, view, categoryOpenStateData }: FeedCatego
|
|||
newView: nextView,
|
||||
})
|
||||
},
|
||||
|
||||
onSuccess(_data, variables) {
|
||||
invalidateEntriesQuery({
|
||||
views: [view, variables],
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const [isCategoryEditing, setIsCategoryEditing] = useState(false)
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ const AppNotificationContainer: FC = () => {
|
|||
const toaster = () => {
|
||||
toast.success("", {
|
||||
description: (
|
||||
<div>
|
||||
<div className="text-text font-medium">
|
||||
App is upgraded to{" "}
|
||||
<a
|
||||
href={`${repository.url}/releases/tag/v${APP_VERSION}`}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,27 @@ import { HotkeyScope } from "~/constants"
|
|||
import { getRouteParams } from "~/hooks/biz/useRouteParams"
|
||||
import { COMMAND_ID } from "~/modules/command/commands/id"
|
||||
|
||||
const checkIsFocusInIframe = (): boolean => {
|
||||
const { activeElement } = document
|
||||
if (!activeElement) return false
|
||||
|
||||
// Check if active element is an iframe or webview
|
||||
if (activeElement.tagName === "IFRAME" || activeElement.tagName === "WEBVIEW") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if active element is inside an iframe or webview
|
||||
let parent = activeElement.parentElement
|
||||
while (parent) {
|
||||
if (parent.tagName === "IFRAME" || parent.tagName === "WEBVIEW") {
|
||||
return true
|
||||
}
|
||||
parent = parent.parentElement
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
const selector = (s: EnhanceSet<string>) => s.size === 0
|
||||
export const FocusableGuardProvider = () => {
|
||||
const hasNoFocusable = useGlobalFocusableScopeSelector(selector)
|
||||
|
|
@ -21,6 +42,10 @@ export const FocusableGuardProvider = () => {
|
|||
|
||||
useEffect(() => {
|
||||
const timer: NodeJS.Timeout = setTimeout(() => {
|
||||
if (checkIsFocusInIframe()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (hasNoFocusable) {
|
||||
if (hasModalRef.current) {
|
||||
setGlobalFocusableScope(HotkeyScope.Modal, "append")
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { env } from "@follow/shared/env.desktop"
|
||||
import { initializeApp } from "firebase/app"
|
||||
import { getApp } from "firebase/app"
|
||||
import { getMessaging, getToken } from "firebase/messaging"
|
||||
|
||||
import { setAppMessagingToken } from "./atoms/app"
|
||||
|
|
@ -31,7 +31,7 @@ export async function registerWebPushNotifications() {
|
|||
|
||||
await navigator.serviceWorker.ready
|
||||
|
||||
const app = initializeApp(firebaseConfig)
|
||||
const app = getApp()
|
||||
const messaging = getMessaging(app)
|
||||
|
||||
const permission = await Notification.requestPermission()
|
||||
|
|
|
|||
|
|
@ -1,61 +1,9 @@
|
|||
import type { FeedViewType } from "@follow/constants"
|
||||
import { useFeedUnreadIsDirty } from "@follow/store/atoms/feed"
|
||||
import { entrySyncServices } from "@follow/store/entry/store"
|
||||
|
||||
import { useAuthInfiniteQuery, useAuthQuery } from "~/hooks/common"
|
||||
import { useAuthQuery } from "~/hooks/common"
|
||||
import { apiClient } from "~/lib/api-fetch"
|
||||
import { defineQuery } from "~/lib/defineQuery"
|
||||
import { queryClient } from "~/lib/query-client"
|
||||
import { getEntriesParams } from "~/lib/utils"
|
||||
|
||||
export function invalidateEntriesQuery({ views }: { views: FeedViewType[] }) {
|
||||
return queryClient.invalidateQueries({
|
||||
predicate: (query) => {
|
||||
const { queryKey } = query
|
||||
if (Array.isArray(queryKey) && queryKey[0] === "entries") {
|
||||
const view = queryKey[2]
|
||||
return views.includes(view as FeedViewType)
|
||||
}
|
||||
return false
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const entries = {
|
||||
entries: ({
|
||||
feedId,
|
||||
inboxId,
|
||||
listId,
|
||||
view,
|
||||
read,
|
||||
excludePrivate,
|
||||
limit,
|
||||
}: {
|
||||
feedId?: string
|
||||
inboxId?: string
|
||||
listId?: string
|
||||
view?: number
|
||||
read?: boolean
|
||||
excludePrivate?: boolean
|
||||
limit?: number
|
||||
}) =>
|
||||
defineQuery(
|
||||
["entries", inboxId || listId || feedId, view, read, excludePrivate, limit],
|
||||
async ({ pageParam }) =>
|
||||
entrySyncServices.fetchEntries({
|
||||
feedId,
|
||||
inboxId,
|
||||
listId,
|
||||
view,
|
||||
read,
|
||||
excludePrivate,
|
||||
limit,
|
||||
pageParam: pageParam as string,
|
||||
}),
|
||||
{
|
||||
rootKey: ["entries", inboxId || listId || feedId],
|
||||
},
|
||||
),
|
||||
preview: (id: string) =>
|
||||
defineQuery(
|
||||
["entries-preview", id],
|
||||
|
|
@ -123,45 +71,6 @@ export const entries = {
|
|||
),
|
||||
}
|
||||
|
||||
const defaultStaleTime = 10 * (60 * 1000) // 10 minutes
|
||||
|
||||
export const useEntries = ({
|
||||
feedId,
|
||||
inboxId,
|
||||
listId,
|
||||
view,
|
||||
read,
|
||||
excludePrivate,
|
||||
}: {
|
||||
feedId?: string
|
||||
inboxId?: string
|
||||
listId?: string
|
||||
view?: number
|
||||
read?: boolean
|
||||
excludePrivate?: boolean
|
||||
}) => {
|
||||
const fetchUnread = read === false
|
||||
const feedUnreadDirty = useFeedUnreadIsDirty((feedId as string) || "")
|
||||
|
||||
return useAuthInfiniteQuery(
|
||||
entries.entries({ feedId, inboxId, listId, view, read, excludePrivate }),
|
||||
{
|
||||
enabled: feedId !== undefined || inboxId !== undefined || listId !== undefined,
|
||||
getNextPageParam: (lastPage) => lastPage.data?.at(-1)?.entries.publishedAt,
|
||||
initialPageParam: undefined,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
// DON'T refetch when the router is pop to previous page
|
||||
refetchOnMount: fetchUnread && feedUnreadDirty && !history.isPop ? "always" : false,
|
||||
|
||||
staleTime:
|
||||
// Force refetch unread entries when feed is dirty
|
||||
// HACK: disable refetch when the router is pop to previous page
|
||||
history.isPop ? Infinity : fetchUnread && feedUnreadDirty ? 0 : defaultStaleTime,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export const useEntriesPreview = ({ id }: { id?: string }) =>
|
||||
useAuthQuery(entries.preview(id!), {
|
||||
enabled: !!id,
|
||||
|
|
|
|||
|
|
@ -12,8 +12,6 @@ import { apiClient } from "~/lib/api-fetch"
|
|||
import { defineQuery } from "~/lib/defineQuery"
|
||||
import { toastFetchError } from "~/lib/error-parser"
|
||||
|
||||
import { entries } from "./entries"
|
||||
|
||||
type FeedQueryParams = { id?: string; url?: string }
|
||||
|
||||
export const feed = {
|
||||
|
|
@ -76,15 +74,6 @@ export const useRefreshFeedMutation = (feedId?: string) =>
|
|||
useMutation({
|
||||
mutationKey: ["refreshFeed", feedId],
|
||||
mutationFn: () => apiClient.feeds.refresh.$get({ query: { id: feedId! } }),
|
||||
|
||||
onSuccess() {
|
||||
if (!feedId) return
|
||||
entries
|
||||
.entries({
|
||||
feedId: feedId!,
|
||||
})
|
||||
.invalidateRoot()
|
||||
},
|
||||
async onError(err) {
|
||||
toastFetchError(err)
|
||||
},
|
||||
|
|
@ -99,8 +88,7 @@ export const useResetFeed = () => {
|
|||
toastIDRef.current = toast.loading(t("sidebar.feed_actions.resetting_feed"))
|
||||
await apiClient.feeds.reset.$get({ query: { id: feedId } })
|
||||
},
|
||||
onSuccess: (_, feedId) => {
|
||||
entries.entries({ feedId }).invalidateRoot()
|
||||
onSuccess: () => {
|
||||
toast.success(
|
||||
t("sidebar.feed_actions.reset_feed_success"),
|
||||
toastIDRef.current ? { id: toastIDRef.current } : undefined,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "Folo",
|
||||
"type": "module",
|
||||
"version": "0.5.0",
|
||||
"version": "0.6.0",
|
||||
"private": true,
|
||||
"description": "Follow everything in one place",
|
||||
"author": "Folo Team",
|
||||
|
|
@ -94,5 +94,5 @@
|
|||
"vite-tsconfig-paths": "5.1.4"
|
||||
},
|
||||
"productName": "Folo",
|
||||
"mainHash": "c4275ef4fd948d72b930953bd4d4b3a45bb37a20eeab354f78b86b6eca1a19d6"
|
||||
"mainHash": "a57052b632dc4602db20bd054171a736fca97cdd302764b413ed95838c7067eb"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { isTaggedFunctionCallOf } from "ast-kit"
|
||||
import type { Transformer } from "unplugin-ast/dist/types-DGZH3jc3.js"
|
||||
import type { Transformer } from "unplugin-ast"
|
||||
import { RemoveWrapperFunction } from "unplugin-ast/transformers"
|
||||
import AST from "unplugin-ast/vite"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import "./global.css"
|
||||
|
||||
import { apiClientSimpleContext, authClientSimpleContext } from "@follow/store/context"
|
||||
import {
|
||||
apiClientSimpleContext,
|
||||
authClientSimpleContext,
|
||||
queryClientSimpleContext,
|
||||
} from "@follow/store/context"
|
||||
import { registerRootComponent } from "expo"
|
||||
import { Image } from "expo-image"
|
||||
import { LinearGradient } from "expo-linear-gradient"
|
||||
|
|
@ -20,6 +24,7 @@ import { TabBarPortal } from "./lib/navigation/bottom-tab/TabBarPortal"
|
|||
import { TabRoot } from "./lib/navigation/bottom-tab/TabRoot"
|
||||
import { TabScreen } from "./lib/navigation/bottom-tab/TabScreen"
|
||||
import { RootStackNavigation } from "./lib/navigation/StackNavigation"
|
||||
import { queryClient } from "./lib/query-client"
|
||||
import { RootProviders } from "./providers"
|
||||
import { IndexTabScreen } from "./screens/(stack)/(tabs)"
|
||||
import { DiscoverTabScreen } from "./screens/(stack)/(tabs)/discover"
|
||||
|
|
@ -33,6 +38,7 @@ global.APP_NAME = "Folo"
|
|||
global.ELECTRON = false
|
||||
apiClientSimpleContext.provide(apiClient)
|
||||
authClientSimpleContext.provide(authClient)
|
||||
queryClientSimpleContext.provide(queryClient)
|
||||
|
||||
enableFreeze(true)
|
||||
;[Image, LinearGradient].forEach((Component) => {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,5 @@
|
|||
import { FeedViewType } from "@follow/constants"
|
||||
import {
|
||||
getInvalidateEntriesQueryPredicate,
|
||||
useEntriesQuery,
|
||||
useEntryIdsByFeedId,
|
||||
} from "@follow/store/entry/hooks"
|
||||
import { useEntriesQuery, useEntryIdsByFeedId } from "@follow/store/entry/hooks"
|
||||
import { getFeedById } from "@follow/store/feed/getter"
|
||||
import { getSubscriptionById } from "@follow/store/subscription/getter"
|
||||
import { getSubscriptionCategory } from "@follow/store/subscription/hooks"
|
||||
|
|
@ -24,7 +20,6 @@ import { PlatformActivityIndicator } from "@/src/components/ui/loading/PlatformA
|
|||
import { views } from "@/src/constants/views"
|
||||
import { useNavigation } from "@/src/lib/navigation/hooks"
|
||||
import type { Navigation } from "@/src/lib/navigation/Navigation"
|
||||
import { queryClient } from "@/src/lib/query-client"
|
||||
import { toast } from "@/src/lib/toast"
|
||||
import { FollowScreen } from "@/src/screens/(modal)/FollowScreen"
|
||||
import { FeedScreen } from "@/src/screens/(stack)/feeds/[feedId]/FeedScreen"
|
||||
|
|
@ -315,17 +310,11 @@ export const SubscriptionFeedCategoryContextMenu = ({
|
|||
key={`SubContent/${view.name}`}
|
||||
value={isSelected}
|
||||
onSelect={() => {
|
||||
subscriptionSyncService
|
||||
.changeCategoryView({
|
||||
category,
|
||||
currentView,
|
||||
newView: view.view,
|
||||
})
|
||||
.then(() => {
|
||||
queryClient.invalidateQueries({
|
||||
predicate: getInvalidateEntriesQueryPredicate([view.view, currentView]),
|
||||
})
|
||||
})
|
||||
subscriptionSyncService.changeCategoryView({
|
||||
category,
|
||||
currentView,
|
||||
newView: view.view,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<ContextMenu.ItemTitle>{t(view.name, { ns: "common" })}</ContextMenu.ItemTitle>
|
||||
|
|
|
|||
|
|
@ -234,15 +234,16 @@ function useLocalEntries(props?: UseEntriesProps): UseEntriesReturn {
|
|||
const allEntries = useEntryStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
const ids = showEntriesByView
|
||||
? (entryIdsByView ?? [])
|
||||
: (getEntryIdsFromMultiplePlace(
|
||||
entryIdsByCollections,
|
||||
entryIdsByFeedId,
|
||||
entryIdsByCategory,
|
||||
entryIdsByListId,
|
||||
entryIdsByInboxId,
|
||||
) ?? [])
|
||||
const ids = isCollection
|
||||
? entryIdsByCollections
|
||||
: showEntriesByView
|
||||
? (entryIdsByView ?? [])
|
||||
: (getEntryIdsFromMultiplePlace(
|
||||
entryIdsByFeedId,
|
||||
entryIdsByCategory,
|
||||
entryIdsByListId,
|
||||
entryIdsByInboxId,
|
||||
) ?? [])
|
||||
|
||||
return ids
|
||||
.map((id) => {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
const { createApp } = require("../dist/server/index.js")
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line antfu/no-import-dist
|
||||
import { createApp } from "../dist/server/index.js"
|
||||
|
||||
// export const config = {
|
||||
// runtime: "nodejs", // this is a pre-requisite
|
||||
// }
|
||||
|
||||
module.exports = async function handler(req: any, res: any) {
|
||||
export default async function handler(req: any, res: any) {
|
||||
const app = await createApp()
|
||||
await app.ready()
|
||||
app.server.emit("request", req, res)
|
||||
|
|
|
|||
|
|
@ -1,30 +1,82 @@
|
|||
import { Header } from "@client/components/layout/header"
|
||||
import { openInFollowApp } from "@client/lib/helper"
|
||||
import { jotaiStore } from "@client/lib/store"
|
||||
import { RootProviders } from "@client/providers/root-providers"
|
||||
import { MemoedDangerousHTMLStyle } from "@follow/components/common/MemoedDangerousHTMLStyle.jsx"
|
||||
import { PoweredByFooter } from "@follow/components/common/PoweredByFooter.jsx"
|
||||
import { Button } from "@follow/components/ui/button/index.jsx"
|
||||
import { useSyncThemeWebApp, useTitle } from "@follow/hooks"
|
||||
import { Provider } from "jotai"
|
||||
import { m as motion } from "motion/react"
|
||||
import { m, useAnimationControls } from "motion/react"
|
||||
import { Fragment, useEffect, useState } from "react"
|
||||
|
||||
const NotFoundContent = () => {
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
const [glitchText, setGlitchText] = useState("404")
|
||||
const [isGlitching, setIsGlitching] = useState(false)
|
||||
|
||||
// Animation controls
|
||||
const iconControls = useAnimationControls()
|
||||
const messageControls = useAnimationControls()
|
||||
const titleControls = useAnimationControls()
|
||||
const descriptionControls = useAnimationControls()
|
||||
const buttonsControls = useAnimationControls()
|
||||
const helpControls = useAnimationControls()
|
||||
|
||||
useTitle("404 - Page Not Found")
|
||||
useSyncThemeWebApp()
|
||||
|
||||
useEffect(() => {
|
||||
setIsVisible(true)
|
||||
}, [])
|
||||
// Start all animations in parallel with their respective delays
|
||||
iconControls.start({
|
||||
scale: 1,
|
||||
rotate: 0,
|
||||
transition: {
|
||||
duration: 0.8,
|
||||
type: "spring",
|
||||
stiffness: 100,
|
||||
delay: 0.2,
|
||||
},
|
||||
})
|
||||
|
||||
messageControls.start({
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { duration: 0.6, delay: 0.4 },
|
||||
})
|
||||
|
||||
titleControls.start({
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
transition: { duration: 0.5, delay: 0.6 },
|
||||
})
|
||||
|
||||
descriptionControls.start({
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
transition: { duration: 0.5, delay: 0.8 },
|
||||
})
|
||||
|
||||
buttonsControls.start({
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
transition: { duration: 0.5, delay: 1 },
|
||||
})
|
||||
|
||||
helpControls.start({
|
||||
opacity: 1,
|
||||
transition: { duration: 0.5, delay: 1.2 },
|
||||
})
|
||||
}, [
|
||||
iconControls,
|
||||
messageControls,
|
||||
titleControls,
|
||||
descriptionControls,
|
||||
buttonsControls,
|
||||
helpControls,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isGlitching) return
|
||||
|
||||
const glitchTexts = ["404", "40₄", "4Ø4", "404", "4◯4", "4○4", "4𝟘4"]
|
||||
const glitchTexts = ["404", "40₄", "4Ø4", "404", "4◯4", "4○4", "4𝟘4"]
|
||||
|
||||
const glitchInterval = setInterval(() => {
|
||||
const randomText = glitchTexts[Math.floor(Math.random() * glitchTexts.length)]
|
||||
|
|
@ -95,21 +147,15 @@ const NotFoundContent = () => {
|
|||
}`}
|
||||
</MemoedDangerousHTMLStyle>
|
||||
<Header />
|
||||
<main className="relative mx-auto flex w-full max-w-[var(--container-max-width)] flex-1 flex-col items-center justify-center pt-20">
|
||||
<main className="relative mx-auto my-8 flex w-full max-w-[var(--container-max-width)] flex-1 flex-col items-center justify-center pt-20">
|
||||
<Fragment>
|
||||
{/* 404 Icon with animations */}
|
||||
<motion.div
|
||||
<m.div
|
||||
className="mb-8 flex items-center justify-center"
|
||||
initial={{ scale: 0, rotate: -180 }}
|
||||
animate={{ scale: isVisible ? 1 : 0, rotate: isVisible ? 0 : -180 }}
|
||||
transition={{
|
||||
duration: 0.8,
|
||||
type: "spring",
|
||||
stiffness: 100,
|
||||
delay: 0.2,
|
||||
}}
|
||||
animate={iconControls}
|
||||
>
|
||||
<motion.div
|
||||
<m.div
|
||||
className="float-animation pulse-glow-animation flex size-32 cursor-pointer items-center justify-center rounded-full bg-zinc-100 dark:bg-zinc-800"
|
||||
whileHover={{
|
||||
scale: 1.1,
|
||||
|
|
@ -131,7 +177,7 @@ const NotFoundContent = () => {
|
|||
setIsGlitching(false)
|
||||
}}
|
||||
>
|
||||
<motion.span
|
||||
<m.span
|
||||
className={`select-none text-4xl font-bold text-zinc-400 dark:text-zinc-600 ${isGlitching ? "glitch-animation" : ""}`}
|
||||
key={glitchText}
|
||||
initial={{ opacity: 0 }}
|
||||
|
|
@ -139,51 +185,47 @@ const NotFoundContent = () => {
|
|||
transition={{ duration: 0.1 }}
|
||||
>
|
||||
{glitchText}
|
||||
</motion.span>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</m.span>
|
||||
</m.div>
|
||||
</m.div>
|
||||
{/* Error Message with stagger animation */}
|
||||
<motion.div
|
||||
<m.div
|
||||
className="mb-8 flex flex-col items-center text-center"
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
animate={{ opacity: isVisible ? 1 : 0, y: isVisible ? 0 : 50 }}
|
||||
transition={{ duration: 0.6, delay: 0.4 }}
|
||||
animate={messageControls}
|
||||
>
|
||||
<motion.h1
|
||||
<m.h1
|
||||
className="mb-4 text-3xl font-bold text-zinc-900 dark:text-zinc-100"
|
||||
initial={{ opacity: 0, x: -30 }}
|
||||
animate={{ opacity: isVisible ? 1 : 0, x: isVisible ? 0 : -30 }}
|
||||
transition={{ duration: 0.5, delay: 0.6 }}
|
||||
animate={titleControls}
|
||||
>
|
||||
Page Not Found
|
||||
</motion.h1>
|
||||
<motion.p
|
||||
className="max-w-md text-lg text-zinc-500 dark:text-zinc-400"
|
||||
</m.h1>
|
||||
<m.p
|
||||
className="max-w-md text-base text-zinc-500 dark:text-zinc-400"
|
||||
initial={{ opacity: 0, x: 30 }}
|
||||
animate={{ opacity: isVisible ? 1 : 0, x: isVisible ? 0 : 30 }}
|
||||
transition={{ duration: 0.5, delay: 0.8 }}
|
||||
animate={descriptionControls}
|
||||
>
|
||||
Sorry, the page you are looking for doesn't exist or has been moved. Please check the
|
||||
URL or return to the homepage to continue browsing.
|
||||
</motion.p>
|
||||
</motion.div>
|
||||
</m.p>
|
||||
</m.div>
|
||||
{/* Action Buttons with hover effects */}
|
||||
<motion.div
|
||||
<m.div
|
||||
className="flex flex-col items-center gap-4 sm:flex-row"
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: isVisible ? 1 : 0, scale: isVisible ? 1 : 0.8 }}
|
||||
transition={{ duration: 0.5, delay: 1 }}
|
||||
animate={buttonsControls}
|
||||
>
|
||||
<motion.div whileHover={{ scale: 1.05, y: -2 }} whileTap={{ scale: 0.95 }}>
|
||||
<m.div whileHover={{ scale: 1.05, y: -2 }} whileTap={{ scale: 0.95 }}>
|
||||
<Button
|
||||
onClick={handleGoHome}
|
||||
buttonClassName="px-6 py-2 transition-all duration-200"
|
||||
>
|
||||
Go Home
|
||||
</Button>
|
||||
</motion.div>
|
||||
</m.div>
|
||||
|
||||
<motion.div whileHover={{ scale: 1.05, y: -2 }} whileTap={{ scale: 0.95 }}>
|
||||
<m.div whileHover={{ scale: 1.05, y: -2 }} whileTap={{ scale: 0.95 }}>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleOpenInApp}
|
||||
|
|
@ -191,18 +233,13 @@ const NotFoundContent = () => {
|
|||
>
|
||||
Open {APP_NAME}
|
||||
</Button>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</m.div>
|
||||
</m.div>
|
||||
{/* Additional Help with fade in */}
|
||||
<motion.div
|
||||
className="mt-12 text-center"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: isVisible ? 1 : 0 }}
|
||||
transition={{ duration: 0.5, delay: 1.2 }}
|
||||
>
|
||||
<m.div className="mt-12 text-center" initial={{ opacity: 0 }} animate={helpControls}>
|
||||
<p className="text-sm text-zinc-400 dark:text-zinc-500">
|
||||
If you believe this is an error, please submit a issue on{" "}
|
||||
<motion.a
|
||||
<m.a
|
||||
className="text-accent transition-colors duration-200"
|
||||
href="https://github.com/rssnext/folo/issues"
|
||||
target="_blank"
|
||||
|
|
@ -211,13 +248,13 @@ const NotFoundContent = () => {
|
|||
style={{ display: "inline-block" }}
|
||||
>
|
||||
GitHub
|
||||
</motion.a>
|
||||
</m.a>
|
||||
</p>
|
||||
</motion.div>
|
||||
</m.div>
|
||||
{/* Floating particles effect */}
|
||||
<div className="pointer-events-none absolute inset-0 overflow-hidden">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<motion.div
|
||||
<m.div
|
||||
key={i}
|
||||
className="absolute size-1 rounded-full bg-zinc-300 opacity-30 dark:bg-zinc-600"
|
||||
initial={{
|
||||
|
|
@ -245,8 +282,8 @@ const NotFoundContent = () => {
|
|||
|
||||
export const NotFound = () => {
|
||||
return (
|
||||
<Provider store={jotaiStore}>
|
||||
<RootProviders>
|
||||
<NotFoundContent />
|
||||
</Provider>
|
||||
</RootProviders>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
export const defineGlobalConstants = () => {
|
||||
Object.assign(globalThis, {
|
||||
APP_NAME: "Folo",
|
||||
})
|
||||
|
||||
try {
|
||||
void __DEV__
|
||||
} catch {
|
||||
Object.assign(globalThis, {
|
||||
APP_NAME: "Folo",
|
||||
__DEV__: process.env.NODE_ENV === "development",
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import "./global"
|
||||
import "./src/lib/load-env"
|
||||
|
||||
import os from "node:os"
|
||||
|
||||
import middie from "@fastify/middie"
|
||||
import { fastifyRequestContext } from "@fastify/request-context"
|
||||
import { env } from "@follow/shared/env.ssr"
|
||||
|
|
@ -8,16 +11,12 @@ import Fastify from "fastify"
|
|||
import { nanoid } from "nanoid"
|
||||
import { FetchError } from "ofetch"
|
||||
|
||||
import { isDev } from "~/lib/env"
|
||||
import { MetaError } from "~/meta-handler"
|
||||
import { staticRoute } from "~/router/static"
|
||||
|
||||
import { defineGlobalConstants } from "./global"
|
||||
import { globalRoute } from "./src/router/global"
|
||||
import { ogRoute } from "./src/router/og"
|
||||
|
||||
defineGlobalConstants()
|
||||
|
||||
const isVercel = process.env.VERCEL === "1"
|
||||
|
||||
declare module "@fastify/request-context" {
|
||||
|
|
@ -64,7 +63,7 @@ export const createApp = async () => {
|
|||
const finalHost = forwardedHost || host
|
||||
|
||||
const upstreamEnv = finalHost?.includes("dev") ? "dev" : "prod"
|
||||
if (!isDev) req.requestContext.set("upstreamEnv", upstreamEnv)
|
||||
if (!__DEV__) req.requestContext.set("upstreamEnv", upstreamEnv)
|
||||
if (upstreamEnv === "prod") {
|
||||
req.requestContext.set("upstreamOrigin", env.VITE_WEB_PROD_URL || env.VITE_WEB_URL)
|
||||
} else {
|
||||
|
|
@ -74,8 +73,8 @@ export const createApp = async () => {
|
|||
done()
|
||||
})
|
||||
|
||||
if (isDev) {
|
||||
const devVite = require("./src/lib/dev-vite")
|
||||
if (__DEV__) {
|
||||
const devVite = await import("./src/lib/dev-vite")
|
||||
await devVite.registerDevViteServer(app)
|
||||
}
|
||||
|
||||
|
|
@ -97,11 +96,11 @@ if (!isVercel) {
|
|||
}
|
||||
|
||||
function getIPAddress() {
|
||||
const interfaces = require("node:os").networkInterfaces()
|
||||
const interfaces = os.networkInterfaces()
|
||||
for (const devName in interfaces) {
|
||||
const iface = interfaces[devName]
|
||||
|
||||
for (const alias of iface) {
|
||||
for (const alias of iface || []) {
|
||||
if (alias.family === "IPv4" && alias.address !== "127.0.0.1" && !alias.internal)
|
||||
return alias.address
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"name": "@follow/ssr",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production vite build && tsx scripts/prepare-vercel-build.ts && tsdown && tsx scripts/cleanup-vercel-build.ts",
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
import { rmSync, writeFileSync } from "node:fs"
|
||||
import { resolve } from "node:path"
|
||||
import { rmSync } from "node:fs"
|
||||
import { dirname, resolve } from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
rmSync(resolve(__dirname, "../.generated"), { recursive: true, force: true })
|
||||
// restore env file
|
||||
|
||||
writeFileSync(
|
||||
resolve(__dirname, "../src/lib/env.ts"),
|
||||
`export const isDev = process.env.NODE_ENV === "development"\n`,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import { mkdirSync } from "node:fs"
|
||||
import fs from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import path, { dirname } from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
mkdirSync(path.join(__dirname, "../.generated"), { recursive: true })
|
||||
|
||||
|
|
@ -12,21 +15,8 @@ async function generateIndexHtmlData() {
|
|||
)
|
||||
}
|
||||
|
||||
async function replaceEnvFile() {
|
||||
const envFile = await fs.readFile(path.join(__dirname, "../src/lib/env.ts"), "utf-8")
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(__dirname, "../src/lib/env.ts"),
|
||||
// For tree shaking
|
||||
envFile.replace(
|
||||
`export const isDev = process.env.NODE_ENV === "development"`,
|
||||
`export const isDev = ${process.env.NODE_ENV === "development"}`,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await Promise.all([generateIndexHtmlData(), replaceEnvFile()])
|
||||
await generateIndexHtmlData()
|
||||
}
|
||||
|
||||
main()
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { hc } from "hono/client"
|
|||
import { ofetch } from "ofetch"
|
||||
|
||||
import PKG from "../../../desktop/package.json"
|
||||
import { isDev } from "./env"
|
||||
|
||||
const getBaseURL = () => {
|
||||
const req = requestContext.get("req")!
|
||||
|
|
@ -34,7 +33,7 @@ export const createApiFetch = () => {
|
|||
credentials: "include",
|
||||
retry: false,
|
||||
onRequest(context) {
|
||||
if (isDev) console.info(`request: ${context.request}`)
|
||||
if (__DEV__) console.info(`request: ${context.request}`)
|
||||
|
||||
context.options.headers.set("User-Agent", `Folo External Server Api Client/${PKG.version}`)
|
||||
},
|
||||
|
|
@ -57,7 +56,7 @@ export const createApiClient = () => {
|
|||
headers() {
|
||||
return {
|
||||
"X-App-Version": PKG.version,
|
||||
"X-App-Dev": isDev ? "1" : "0",
|
||||
"X-App-Dev": __DEV__ ? "1" : "0",
|
||||
"User-Agent": `Folo External Server Api Client/${PKG.version}`,
|
||||
Cookie: authSessionToken ? `__Secure-better-auth.session_token=${authSessionToken}` : "",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { resolve } from "node:path"
|
||||
import { dirname, resolve } from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
|
||||
import type { FastifyInstance } from "fastify"
|
||||
import type { ViteDevServer } from "vite"
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const root = resolve(__dirname, "../..")
|
||||
|
||||
let globalVite: ViteDevServer
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
export const isDev = process.env.NODE_ENV === "development"
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
import fs from "node:fs"
|
||||
import { createRequire } from "node:module"
|
||||
import path, { resolve } from "node:path"
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
|
||||
const weights = [
|
||||
{
|
||||
name: "Thin",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { readFileSync } from "node:fs"
|
||||
import path, { resolve } from "node:path"
|
||||
import path, { dirname, resolve } from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
|
||||
import { env } from "@follow/shared/env.ssr"
|
||||
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"
|
||||
|
|
@ -8,7 +9,6 @@ import { parseHTML } from "linkedom"
|
|||
import { FetchError } from "ofetch"
|
||||
import xss from "xss"
|
||||
|
||||
import { isDev } from "~/lib/env"
|
||||
import { NotFoundError } from "~/lib/not-found"
|
||||
import { buildSeoMetaTags } from "~/lib/seo"
|
||||
|
||||
|
|
@ -17,10 +17,11 @@ import { injectMetaHandler, MetaError } from "../meta-handler"
|
|||
const devHandler = (app: FastifyInstance) => {
|
||||
app.get("*", async (req, reply) => {
|
||||
const url = req.originalUrl
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
const root = resolve(__dirname, "../..")
|
||||
|
||||
const vite = require("../lib/dev-vite").getViteServer()
|
||||
const vite = await import("../lib/dev-vite").then((m) => m.getViteServer())
|
||||
try {
|
||||
let template = readFileSync(path.resolve(root, vite.config.root, "index.html"), "utf-8")
|
||||
template = await vite.transformIndexHtml(url, template)
|
||||
|
|
@ -30,14 +31,15 @@ const devHandler = (app: FastifyInstance) => {
|
|||
reply.type("text/html")
|
||||
reply.send(document.toString())
|
||||
} catch (e) {
|
||||
vite.ssrFixStacktrace(e)
|
||||
vite.ssrFixStacktrace(e as Error)
|
||||
reply.code(500).send(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
const prodHandler = (app: FastifyInstance) => {
|
||||
app.get("*", async (req, reply) => {
|
||||
const template = require("../../.generated/index.template").default
|
||||
// @ts-expect-error
|
||||
const template = await import("../../.generated/index.template").then((m) => m.default)
|
||||
const { document } = parseHTML(template)
|
||||
await safeInjectMetaToTemplate(document, req, reply)
|
||||
|
||||
|
|
@ -91,7 +93,7 @@ injectEnv({"VITE_API_URL":"${apiUrl}","VITE_EXTERNAL_API_URL":"${apiUrl}","VITE_
|
|||
)
|
||||
})
|
||||
}
|
||||
export const globalRoute = isDev ? devHandler : prodHandler
|
||||
export const globalRoute = __DEV__ ? devHandler : prodHandler
|
||||
|
||||
async function safeInjectMetaToTemplate(
|
||||
document: Document,
|
||||
|
|
|
|||
|
|
@ -69,11 +69,7 @@ export const OGCanvas = ({ children, seed }: { children: React.ReactNode; seed:
|
|||
{/* Follow Logo */}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
|
||||
<FollowIcon />
|
||||
<span
|
||||
style={{ fontSize: 28, fontWeight: "bold", color: "white", letterSpacing: "0.05em" }}
|
||||
>
|
||||
Folo
|
||||
</span>
|
||||
<LogoText />
|
||||
</div>
|
||||
|
||||
{/* AI RSS */}
|
||||
|
|
@ -132,6 +128,35 @@ function FollowIcon() {
|
|||
)
|
||||
}
|
||||
|
||||
function LogoText() {
|
||||
return (
|
||||
<svg
|
||||
// width="115"
|
||||
height="30"
|
||||
viewBox="0 0 115 50"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M97.4813 49.2552C87.5432 49.2552 80.678 42.0631 80.678 31.9288C80.678 21.6637 87.4778 14.4062 97.4813 14.4062C107.55 14.4062 114.35 21.6637 114.35 31.9288C114.35 42.0631 107.485 49.2552 97.4813 49.2552ZM97.4813 42.1939C102.843 42.1939 106.112 37.944 106.112 31.8634C106.112 25.7174 102.843 21.4676 97.4813 21.4676C92.1199 21.4676 88.9816 25.7174 88.9816 31.8634C88.9816 37.944 92.1199 42.1939 97.4813 42.1939Z"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
d="M72.7289 48.6667C70.1136 48.6667 68.6752 46.9668 68.6752 44.0899V5.38342C68.6752 2.50658 70.1136 0.806641 72.7289 0.806641C75.3442 0.806641 76.7826 2.50658 76.7826 5.38342V44.0899C76.7826 46.9668 75.4096 48.6667 72.7289 48.6667Z"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
d="M47.8314 49.2552C37.8932 49.2552 31.0281 42.0631 31.0281 31.9288C31.0281 21.6637 37.8279 14.4062 47.8314 14.4062C57.9003 14.4062 64.7001 21.6637 64.7001 31.9288C64.7001 42.0631 57.8349 49.2552 47.8314 49.2552ZM47.8314 42.1939C53.1928 42.1939 56.4619 37.944 56.4619 31.8634C56.4619 25.7174 53.1928 21.4676 47.8314 21.4676C42.47 21.4676 39.3317 25.7174 39.3317 31.8634C39.3317 37.944 42.47 42.1939 47.8314 42.1939Z"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
d="M5.10235 48.6669C2.42166 48.6669 0.852478 46.967 0.852478 43.9594V7.21438C0.852478 4.20678 2.55243 2.50684 5.62541 2.50684H26.1555C28.967 2.50684 30.5362 3.87987 30.5362 6.23365C30.5362 8.58742 28.967 9.89507 26.1555 9.89507H9.35221V20.9447H24.7825C27.594 20.9447 29.1631 22.187 29.1631 24.5408C29.1631 26.9599 27.594 28.2022 24.7825 28.2022H9.35221V43.9594C9.35221 46.967 7.84842 48.6669 5.10235 48.6669Z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export async function getImageBase64(image: string | null | undefined) {
|
||||
if (!image) {
|
||||
return null
|
||||
|
|
|
|||
|
|
@ -9,12 +9,14 @@ export default defineConfig({
|
|||
outDir: "dist/server",
|
||||
|
||||
clean: true,
|
||||
format: ["cjs"],
|
||||
format: ["esm"],
|
||||
external: ["lightningcss", "vite"],
|
||||
treeshake: true,
|
||||
|
||||
define: {
|
||||
__DEV__: JSON.stringify(process.env.NODE_ENV === "development"),
|
||||
},
|
||||
|
||||
hooks(hooks) {
|
||||
hooks.hook("build:done", async () => {
|
||||
if (process.env.VERCEL !== "1") return
|
||||
|
|
@ -24,10 +26,11 @@ export default defineConfig({
|
|||
|
||||
try {
|
||||
const insertCode = `try {
|
||||
require.resolve("@fontsource/sn-pro")
|
||||
require.resolve('kose-font')
|
||||
require.resolve('kose-font/fonts/KosefontP-JP.ttf')
|
||||
require.resolve('kose-font/fonts/Kosefont-JP.ttf')
|
||||
const noop = () => {}
|
||||
import("@fontsource/sn-pro").then(noop)
|
||||
import('kose-font').then(noop)
|
||||
import('kose-font/fonts/KosefontP-JP.ttf').then(noop)
|
||||
import('kose-font/fonts/Kosefont-JP.ttf').then(noop)
|
||||
${(() => {
|
||||
const require = createRequire(import.meta.url)
|
||||
const fontDepsPath = require.resolve("@fontsource/sn-pro")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { resolve } from "node:path"
|
||||
import { dirname, resolve } from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
|
||||
import react from "@vitejs/plugin-react"
|
||||
import { codeInspectorPlugin } from "code-inspector-plugin"
|
||||
|
|
@ -13,6 +14,8 @@ const routeBuilderPluginV2 = await tsImport(
|
|||
import.meta.url,
|
||||
).then((m) => m.default)
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@
|
|||
"login.confirm_password.label": "Confirm Password",
|
||||
"login.continueWith": "Continue with {{provider}}",
|
||||
"login.email": "Email",
|
||||
"login.enter_token": "If you aren't redirected automatically, copy the token below and paste it in the \"Enter authorization token to continue\" form on the Desktop App.",
|
||||
"login.enter_token": "If you aren't redirected automatically, copy the token below and paste it in the \"Enter authorization token to continue\" form on the Desktop App (v0.5.0+).",
|
||||
"login.errors.unknown": "Errors Unknown",
|
||||
"login.forget_password.description": "Enter the email address associated with your account and we'll send you an email about how to reset your password.",
|
||||
"login.forget_password.email_invalid": "Invalid email",
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@
|
|||
"login.confirm_password.label": "确认密码",
|
||||
"login.continueWith": "使用 {{provider}} 登录",
|
||||
"login.email": "邮件地址",
|
||||
"login.enter_token": "如果未自动重定向,请复制下方令牌并粘贴到桌面应用的“输入授权令牌以继续”表单中。",
|
||||
"login.enter_token": "如果未自动重定向,请复制下方令牌并粘贴到桌面应用(v0.5.0 以上)的“输入授权令牌以继续”表单中。",
|
||||
"login.errors.unknown": "未知错误",
|
||||
"login.forget_password.description": "请输入与你的帐户关联的邮件地址,我们将向你发送一封关于如何重置密码的邮件。",
|
||||
"login.forget_password.email_invalid": "无效的邮箱地址",
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import { LazyMotion, MotionConfig } from "motion/react"
|
||||
import { domMax, LazyMotion, MotionConfig } from "motion/react"
|
||||
|
||||
const loadFeatures = () => import("../framer-lazy-feature").then((res) => res.default)
|
||||
export const MotionProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<LazyMotion features={loadFeatures} strict key="framer">
|
||||
<LazyMotion features={domMax} strict key="framer">
|
||||
<MotionConfig
|
||||
transition={{
|
||||
type: "tween",
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
export { domMax as default } from "motion/react"
|
||||
|
|
@ -2,6 +2,7 @@ import { useIsDark } from "@follow/hooks"
|
|||
import { Toaster as Sonner } from "sonner"
|
||||
|
||||
import { ZIndexProvider } from "../z-index"
|
||||
import { toastStyles } from "./styles"
|
||||
|
||||
type ToasterProps = React.ComponentProps<typeof Sonner>
|
||||
const TOAST_Z_INDEX = 999999999
|
||||
|
|
@ -16,70 +17,7 @@ export const Toaster = ({ ...props }: ToasterProps) => {
|
|||
gap={12}
|
||||
toastOptions={{
|
||||
unstyled: true,
|
||||
classNames: {
|
||||
toast: tw`
|
||||
group relative flex w-full items-center justify-between gap-3 rounded-2xl p-4 shadow-lg
|
||||
backdrop-blur-xl border border-border/50
|
||||
bg-material-ultra-thick
|
||||
transition-all duration-300 ease-out
|
||||
hover:scale-[1.02] hover:shadow-xl
|
||||
data-[type=success]:border-green/30 data-[type=success]:bg-green/5
|
||||
data-[type=error]:border-red/30 data-[type=error]:bg-red/5
|
||||
data-[type=warning]:border-orange/30 data-[type=warning]:bg-orange/5
|
||||
data-[type=info]:border-blue/30 data-[type=info]:bg-blue/5
|
||||
data-[type=loading]:border-gray/30 data-[type=loading]:bg-gray/5
|
||||
max-w-md min-w-[320px]
|
||||
font-theme
|
||||
`,
|
||||
title: tw`
|
||||
text-sm font-medium text-text
|
||||
leading-tight
|
||||
`,
|
||||
description: tw`
|
||||
text-xs text-text-secondary
|
||||
leading-relaxed mt-1
|
||||
`,
|
||||
content: tw`
|
||||
flex-1 min-w-0
|
||||
`,
|
||||
icon: tw`
|
||||
flex-shrink-0 mt-0.5 size-5
|
||||
[li[data-type="success"]_&]:text-green
|
||||
[li[data-type="error"]_&]:text-red
|
||||
[li[data-type="warning"]_&]:text-orange
|
||||
[li[data-type="info"]_&]:text-blue
|
||||
[li[data-type="loading"]_&]:text-gray
|
||||
`,
|
||||
actionButton: tw`
|
||||
px-2.5 py-1 text-xs font-medium rounded-md
|
||||
transition-all duration-200
|
||||
focus:outline-none focus:shadow-lg bg-accent
|
||||
group-data-[type=success]:bg-green group-data-[type=success]:text-white group-data-[type=success]:hover:bg-green/90 group-data-[type=success]:focus:shadow-green/50
|
||||
group-data-[type=error]:bg-red group-data-[type=error]:text-white group-data-[type=error]:hover:bg-red/90 group-data-[type=error]:focus:shadow-red/50
|
||||
group-data-[type=warning]:bg-orange group-data-[type=warning]:text-white group-data-[type=warning]:hover:bg-orange/90 group-data-[type=warning]:focus:shadow-orange/50
|
||||
group-data-[type=info]:bg-blue group-data-[type=info]:text-white group-data-[type=info]:hover:bg-blue/90 group-data-[type=info]:focus:shadow-blue/50
|
||||
group-data-[type=loading]:bg-gray group-data-[type=loading]:text-white group-data-[type=loading]:hover:bg-gray/90 group-data-[type=loading]:focus:shadow-gray/50
|
||||
hover:shadow-md active:scale-95
|
||||
`,
|
||||
cancelButton: tw`
|
||||
px-2.5 py-1 text-xs font-medium rounded-md
|
||||
bg-fill-secondary text-text-secondary
|
||||
hover:bg-fill-tertiary hover:text-text
|
||||
transition-colors duration-200
|
||||
focus:outline-none focus:ring-2 focus:ring-fill/50 focus:ring-offset-1
|
||||
`,
|
||||
closeButton: tw`
|
||||
absolute top-2 right-2 w-6 h-6 rounded-full
|
||||
flex items-center justify-center
|
||||
bg-fill text-text-tertiary
|
||||
hover:bg-fill-secondary hover:text-text-secondary
|
||||
active:bg-fill-tertiary active:text-text
|
||||
transition-all duration-200
|
||||
opacity-0 group-hover:opacity-100
|
||||
focus:outline-none focus:ring-2 focus:ring-accent/50
|
||||
focus:opacity-100
|
||||
`,
|
||||
},
|
||||
classNames: toastStyles,
|
||||
}}
|
||||
icons={{
|
||||
success: <i className="i-mgc-check-circle-cute-re" />,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
export const toastStyles = {
|
||||
toast: tw`
|
||||
group relative flex w-full items-center justify-between gap-3 rounded-2xl p-4 shadow-lg
|
||||
backdrop-blur-background border border-border/50
|
||||
bg-material-ultra-thick duration-300 ease-out
|
||||
data-[type=success]:border-green/30 data-[type=success]:bg-green/20
|
||||
data-[type=error]:border-red/30 data-[type=error]:bg-red/20
|
||||
data-[type=warning]:border-orange/30 data-[type=warning]:bg-orange/20
|
||||
data-[type=info]:border-blue/30 data-[type=info]:bg-blue/20
|
||||
data-[type=loading]:border-gray/30 data-[type=loading]:bg-gray/20
|
||||
max-w-md min-w-[320px]
|
||||
font-theme
|
||||
`,
|
||||
title: tw`
|
||||
text-sm font-medium text-text
|
||||
leading-tight
|
||||
`,
|
||||
description: tw`
|
||||
text-xs text-text-secondary
|
||||
leading-relaxed mt-1
|
||||
`,
|
||||
content: tw`
|
||||
flex-1 min-w-0
|
||||
`,
|
||||
icon: tw`
|
||||
flex-shrink-0 mt-0.5 size-5
|
||||
[li[data-type="success"]_&]:text-green
|
||||
[li[data-type="error"]_&]:text-red
|
||||
[li[data-type="warning"]_&]:text-orange
|
||||
[li[data-type="info"]_&]:text-blue
|
||||
[li[data-type="loading"]_&]:text-gray
|
||||
`,
|
||||
actionButton: tw`
|
||||
shrink-0
|
||||
h-6
|
||||
px-2.5 text-xs font-medium rounded-md
|
||||
transition-all duration-200
|
||||
focus:outline-none focus:shadow-lg bg-accent
|
||||
group-data-[type=success]:bg-green group-data-[type=success]:text-white group-data-[type=success]:hover:bg-green/90 group-data-[type=success]:focus:shadow-green/50
|
||||
group-data-[type=error]:bg-red group-data-[type=error]:text-white group-data-[type=error]:hover:bg-red/90 group-data-[type=error]:focus:shadow-red/50
|
||||
group-data-[type=warning]:bg-orange group-data-[type=warning]:text-white group-data-[type=warning]:hover:bg-orange/90 group-data-[type=warning]:focus:shadow-orange/50
|
||||
group-data-[type=info]:bg-blue group-data-[type=info]:text-white group-data-[type=info]:hover:bg-blue/90 group-data-[type=info]:focus:shadow-blue/50
|
||||
group-data-[type=loading]:bg-gray group-data-[type=loading]:text-white group-data-[type=loading]:hover:bg-gray/90 group-data-[type=loading]:focus:shadow-gray/50
|
||||
hover:shadow-md active:scale-95
|
||||
`,
|
||||
cancelButton: tw`
|
||||
h-6
|
||||
px-2.5 text-xs font-medium rounded-md
|
||||
bg-fill-secondary text-text-secondary
|
||||
hover:bg-fill-tertiary hover:text-text
|
||||
transition-colors duration-200
|
||||
focus:outline-none focus:ring-2 focus:ring-fill/50 focus:ring-offset-1
|
||||
`,
|
||||
closeButton: tw`
|
||||
absolute -top-2 -right-2 w-6 h-6 rounded-full
|
||||
flex items-center justify-center
|
||||
text-text
|
||||
border border-border
|
||||
backdrop-blur-background
|
||||
bg-material-ultra-thick
|
||||
transition-all duration-200
|
||||
opacity-0 group-hover:opacity-100
|
||||
focus:outline-none focus:ring-2 focus:ring-accent/50
|
||||
focus:opacity-100
|
||||
`,
|
||||
}
|
||||
|
|
@ -35,7 +35,7 @@ const TooltipContent = ({
|
|||
<m.div
|
||||
initial={{ opacity: 0.82, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={Spring.presets.snappy}
|
||||
transition={Spring.snappy(0.1)}
|
||||
>
|
||||
{/* https://github.com/radix-ui/primitives/discussions/868 */}
|
||||
<TooltipPrimitive.Arrow className="z-50 fill-white [clip-path:inset(0_-10px_-10px_-10px)] dark:fill-neutral-950 dark:drop-shadow-[0_0_1px_theme(colors.white/0.5)]" />
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"name": "@follow/constants",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"exports": {
|
||||
|
|
|
|||
|
|
@ -11,9 +11,13 @@ class CollectionServiceStatic implements Resetable {
|
|||
await db.delete(collectionsTable).execute()
|
||||
}
|
||||
|
||||
async upsertMany(collections: CollectionSchema[]) {
|
||||
async upsertMany(collections: CollectionSchema[], options?: { reset?: boolean }) {
|
||||
if (collections.length === 0) return
|
||||
|
||||
if (options?.reset) {
|
||||
await db.delete(collectionsTable).execute()
|
||||
}
|
||||
|
||||
await db
|
||||
.insert(collectionsTable)
|
||||
.values(collections)
|
||||
|
|
@ -27,6 +31,11 @@ class CollectionServiceStatic implements Resetable {
|
|||
await db.delete(collectionsTable).where(eq(collectionsTable.entryId, entryId))
|
||||
}
|
||||
|
||||
async deleteMany(entryId: string[]) {
|
||||
if (entryId.length === 0) return
|
||||
await db.delete(collectionsTable).where(inArray(collectionsTable.entryId, entryId))
|
||||
}
|
||||
|
||||
getCollectionMany(entryId: string[]) {
|
||||
return db.query.collectionsTable.findMany({ where: inArray(collectionsTable.entryId, entryId) })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20928,7 +20928,18 @@ declare const _routes: hono_hono_base.HonoBase<Env, ({
|
|||
status: 200;
|
||||
};
|
||||
};
|
||||
}, "/trending">, "/">;
|
||||
}, "/trending"> | hono_types.MergeSchemaPath<{
|
||||
"/g": {
|
||||
$post: {
|
||||
input: {
|
||||
json: any;
|
||||
};
|
||||
output: Response;
|
||||
outputFormat: "json";
|
||||
status: hono_utils_http_status.StatusCode;
|
||||
};
|
||||
};
|
||||
}, "/data">, "/">;
|
||||
type AppType = typeof _routes;
|
||||
|
||||
export { type ActionItem, type ActionsModel, type AirdropActivity, type AppType, type AttachmentsModel, type AuthSession, type AuthUser, CommonEntryFields, type ConditionItem, type DetailModel, type EntriesModel, type ExtraModel, type FeedModel, type ListModel, type MediaModel, type MessagingData, MessagingType, type SettingsModel, type UrlReadsModel, account, achievements, achievementsOpenAPISchema, actions, actionsItemOpenAPISchema, actionsOpenAPISchema, actionsRelations, activityEnum, airdrops, airdropsOpenAPISchema, attachmentsZodSchema, authPlugins, boosts, captcha, collections, collectionsOpenAPISchema, collectionsRelations, detailModelSchema, entries, entriesOpenAPISchema, entriesRelations, extraZodSchema, feedAnalytics, feedAnalyticsOpenAPISchema, feedAnalyticsRelations, feedPowerTokens, feedPowerTokensOpenAPISchema, feedPowerTokensRelations, feeds, feedsOpenAPISchema, feedsRelations, inboxHandleSchema, inboxes, inboxesEntries, inboxesEntriesInsertOpenAPISchema, type inboxesEntriesModel, inboxesEntriesOpenAPISchema, inboxesEntriesRelations, inboxesOpenAPISchema, inboxesRelations, invitations, invitationsOpenAPISchema, invitationsRelations, languageSchema, levels, levelsOpenAPISchema, levelsRelations, listAnalytics, listAnalyticsOpenAPISchema, listAnalyticsRelations, lists, listsOpenAPISchema, listsRelations, listsSubscriptions, listsSubscriptionsOpenAPISchema, listsSubscriptionsRelations, lower, mediaZodSchema, messaging, messagingOpenAPISchema, messagingRelations, readabilities, rsshub, rsshubAnalytics, rsshubAnalyticsOpenAPISchema, rsshubOpenAPISchema, rsshubPurchase, rsshubUsage, rsshubUsageOpenAPISchema, rsshubUsageRelations, session, settings, subscriptions, subscriptionsOpenAPISchema, subscriptionsRelations, timeline, timelineOpenAPISchema, timelineRelations, transactionType, transactions, transactionsOpenAPISchema, transactionsRelations, trendingFeeds, trendingFeedsOpenAPISchema, trendingFeedsRelations, twoFactor, uploads, urlReads, urlReadsOpenAPISchema, user, users, usersOpenApiSchema, usersRelations, verification, wallets, walletsOpenAPISchema, walletsRelations };
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ import { CollectionService } from "@follow/database/services/collection"
|
|||
|
||||
import { apiClient } from "../context"
|
||||
import { getEntry } from "../entry/getter"
|
||||
import type { Hydratable } from "../internal/base"
|
||||
import { invalidateEntriesQuery } from "../entry/hooks"
|
||||
import type { Hydratable, Resetable } from "../internal/base"
|
||||
import { createTransaction, createZustandStore } from "../internal/helper"
|
||||
|
||||
interface CollectionState {
|
||||
|
|
@ -52,6 +53,8 @@ class CollectionSyncService {
|
|||
})
|
||||
|
||||
await tx.run()
|
||||
|
||||
invalidateEntriesQuery({ collection: true })
|
||||
}
|
||||
|
||||
async unstarEntry(entryId: string) {
|
||||
|
|
@ -75,20 +78,24 @@ class CollectionSyncService {
|
|||
})
|
||||
|
||||
await tx.run()
|
||||
|
||||
invalidateEntriesQuery({ collection: true })
|
||||
}
|
||||
}
|
||||
|
||||
class CollectionActions implements Hydratable {
|
||||
class CollectionActions implements Hydratable, Resetable {
|
||||
async hydrate() {
|
||||
const collections = await CollectionService.getCollectionAll()
|
||||
collectionActions.upsertManyInSession(collections)
|
||||
}
|
||||
|
||||
upsertManyInSession(collections: CollectionSchema[]) {
|
||||
upsertManyInSession(collections: CollectionSchema[], options?: { reset?: boolean }) {
|
||||
const state = get()
|
||||
const nextCollections: CollectionState["collections"] = {
|
||||
...state.collections,
|
||||
}
|
||||
const nextCollections: CollectionState["collections"] = options?.reset
|
||||
? {}
|
||||
: {
|
||||
...state.collections,
|
||||
}
|
||||
collections.forEach((collection) => {
|
||||
if (!collection.entryId) return
|
||||
nextCollections[collection.entryId] = collection
|
||||
|
|
@ -99,42 +106,63 @@ class CollectionActions implements Hydratable {
|
|||
})
|
||||
}
|
||||
|
||||
async upsertMany(collections: CollectionSchema[]) {
|
||||
async upsertMany(collections: CollectionSchema[], options?: { reset?: boolean }) {
|
||||
const tx = createTransaction()
|
||||
tx.store(() => {
|
||||
this.upsertManyInSession(collections)
|
||||
this.upsertManyInSession(collections, options)
|
||||
})
|
||||
tx.persist(() => {
|
||||
return CollectionService.upsertMany(collections)
|
||||
return CollectionService.upsertMany(collections, options)
|
||||
})
|
||||
await tx.run()
|
||||
}
|
||||
|
||||
async deleteInSession(entryId: string) {
|
||||
deleteInSession(entryId: string | string[]) {
|
||||
const normalizedEntryId = Array.isArray(entryId) ? entryId : [entryId]
|
||||
|
||||
const state = useCollectionStore.getState()
|
||||
const nextCollections: CollectionState["collections"] = {
|
||||
...state.collections,
|
||||
}
|
||||
delete nextCollections[entryId]
|
||||
|
||||
normalizedEntryId.forEach((id) => {
|
||||
delete nextCollections[id]
|
||||
})
|
||||
set({
|
||||
...state,
|
||||
collections: nextCollections,
|
||||
})
|
||||
}
|
||||
|
||||
async delete(entryId: string) {
|
||||
async delete(entryId: string | string[]) {
|
||||
const entryIdsInCollection = new Set(Object.keys(get().collections))
|
||||
const normalizedEntryId = (Array.isArray(entryId) ? entryId : [entryId]).filter((id) =>
|
||||
entryIdsInCollection.has(id),
|
||||
)
|
||||
|
||||
if (normalizedEntryId.length === 0) return
|
||||
|
||||
const tx = createTransaction()
|
||||
tx.store(() => {
|
||||
this.deleteInSession(entryId)
|
||||
})
|
||||
tx.persist(() => {
|
||||
return CollectionService.delete(entryId)
|
||||
return CollectionService.deleteMany(normalizedEntryId)
|
||||
})
|
||||
tx.run()
|
||||
}
|
||||
|
||||
reset() {
|
||||
set(defaultState)
|
||||
async reset() {
|
||||
const tx = createTransaction()
|
||||
tx.store(() => {
|
||||
set(defaultState)
|
||||
})
|
||||
|
||||
tx.persist(() => {
|
||||
return CollectionService.reset()
|
||||
})
|
||||
|
||||
await tx.run()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { AuthClient } from "@follow/shared/auth"
|
||||
import type { QueryClient } from "@tanstack/react-query"
|
||||
|
||||
import type { APIClient } from "./types"
|
||||
|
||||
|
|
@ -27,5 +28,7 @@ function createSimpleContext<T>() {
|
|||
|
||||
export const apiClientSimpleContext = createSimpleContext<APIClient>()
|
||||
export const authClientSimpleContext = createSimpleContext<AuthClient>()
|
||||
export const queryClientSimpleContext = createSimpleContext<QueryClient>()
|
||||
export const apiClient = apiClientSimpleContext.consumer
|
||||
export const authClient = authClientSimpleContext.consumer
|
||||
export const queryClient = queryClientSimpleContext.consumer
|
||||
|
|
|
|||
|
|
@ -1,26 +1,48 @@
|
|||
import type { FeedViewType } from "@follow/constants"
|
||||
import type { Query } from "@tanstack/react-query"
|
||||
import { useInfiniteQuery, useQuery } from "@tanstack/react-query"
|
||||
import { useCallback } from "react"
|
||||
|
||||
import { useFeedUnreadIsDirty } from "../atoms/feed"
|
||||
import { FEED_COLLECTION_LIST } from "../constants/app"
|
||||
import { queryClient } from "../context"
|
||||
import { getSubscriptionByEntryId } from "../subscription/getter"
|
||||
import { getEntry } from "./getter"
|
||||
import { entrySyncServices, useEntryStore } from "./store"
|
||||
import type { EntryModel, FetchEntriesProps, FetchEntriesPropsSettings } from "./types"
|
||||
|
||||
export const getInvalidateEntriesQueryPredicate = (views: FeedViewType[]) => {
|
||||
return (query: Query) => {
|
||||
const { queryKey } = query
|
||||
if (Array.isArray(queryKey) && queryKey[0] === "entries") {
|
||||
const view = queryKey[4]
|
||||
return views.includes(view as FeedViewType)
|
||||
}
|
||||
return false
|
||||
}
|
||||
export const invalidateEntriesQuery = ({
|
||||
views,
|
||||
collection,
|
||||
}: {
|
||||
views?: FeedViewType[]
|
||||
collection?: true
|
||||
}) => {
|
||||
return queryClient().invalidateQueries({
|
||||
predicate: (query) => {
|
||||
const { queryKey } = query
|
||||
if (Array.isArray(queryKey) && queryKey[0] === "entries") {
|
||||
const feedId = queryKey[1]
|
||||
const view = queryKey[4]
|
||||
|
||||
const isCollection = queryKey[7]
|
||||
if (views) {
|
||||
return views.includes(view as FeedViewType)
|
||||
}
|
||||
|
||||
if (collection) {
|
||||
return isCollection === true || feedId === FEED_COLLECTION_LIST
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const defaultStaleTime = 10 * (60 * 1000) // 10 minutes
|
||||
|
||||
export const useEntriesQuery = (
|
||||
props?: Omit<FetchEntriesProps, "pageParam" | "read"> & FetchEntriesPropsSettings,
|
||||
props?: Omit<FetchEntriesProps, "pageParam" | "read" | "excludePrivate"> &
|
||||
FetchEntriesPropsSettings,
|
||||
) => {
|
||||
const {
|
||||
feedId,
|
||||
|
|
@ -29,10 +51,17 @@ export const useEntriesQuery = (
|
|||
view,
|
||||
limit,
|
||||
feedIdList,
|
||||
isCollection,
|
||||
unreadOnly,
|
||||
hidePrivateSubscriptionsInTimeline,
|
||||
} = props || {}
|
||||
|
||||
const fetchUnread = unreadOnly
|
||||
const feedUnreadDirty = useFeedUnreadIsDirty((feedId as string) || "")
|
||||
|
||||
const isPop =
|
||||
"history" in globalThis && "isPop" in globalThis.history && !!globalThis.history.isPop
|
||||
|
||||
return useInfiniteQuery({
|
||||
queryKey: [
|
||||
"entries",
|
||||
|
|
@ -40,9 +69,10 @@ export const useEntriesQuery = (
|
|||
inboxId,
|
||||
listId,
|
||||
view,
|
||||
unreadOnly,
|
||||
limit,
|
||||
feedIdList,
|
||||
isCollection,
|
||||
unreadOnly,
|
||||
hidePrivateSubscriptionsInTimeline,
|
||||
],
|
||||
queryFn: ({ pageParam }) =>
|
||||
|
|
@ -52,11 +82,18 @@ export const useEntriesQuery = (
|
|||
read: unreadOnly ? false : undefined,
|
||||
excludePrivate: hidePrivateSubscriptionsInTimeline,
|
||||
}),
|
||||
staleTime: 3 * 60 * 1000,
|
||||
|
||||
getNextPageParam: (lastPage) => lastPage.data?.at(-1)?.entries.publishedAt,
|
||||
initialPageParam: undefined as undefined | string,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
// DON'T refetch when the router is pop to previous page
|
||||
refetchOnMount: fetchUnread && feedUnreadDirty && !isPop ? "always" : false,
|
||||
|
||||
staleTime:
|
||||
// Force refetch unread entries when feed is dirty
|
||||
// HACK: disable refetch when the router is pop to previous page
|
||||
isPop ? Infinity : fetchUnread && feedUnreadDirty ? 0 : defaultStaleTime,
|
||||
enabled: !!props,
|
||||
})
|
||||
}
|
||||
|
|
@ -104,7 +141,7 @@ function sortEntryIdsByPublishDate(a: string, b: string) {
|
|||
return entryB.publishedAt.getTime() - entryA.publishedAt.getTime()
|
||||
}
|
||||
|
||||
export const useEntryIdsByView = (view: FeedViewType, excludePrivate: boolean) => {
|
||||
export const useEntryIdsByView = (view: FeedViewType, excludePrivate: boolean | undefined) => {
|
||||
return useEntryStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
|
|
|
|||
|
|
@ -489,12 +489,12 @@ class EntrySyncServices {
|
|||
|
||||
await entryActions.upsertMany(entries)
|
||||
|
||||
if (isCollection && res.data) {
|
||||
if (view === undefined) {
|
||||
console.error("view is required for collection")
|
||||
}
|
||||
const collections = honoMorph.toCollections(res.data, view ?? 0)
|
||||
await collectionActions.upsertMany(collections)
|
||||
if (typeof view === "number") {
|
||||
const { collections, entryIdsNotInCollections } = honoMorph.toCollections(res.data, view)
|
||||
await collectionActions.upsertMany(collections, {
|
||||
reset: params.isCollection && !pageParam,
|
||||
})
|
||||
await collectionActions.delete(entryIdsNotInCollections)
|
||||
}
|
||||
|
||||
const dataFeeds = res.data?.map((e) => e.feeds).filter((f) => f.type === "feed")
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ export type FetchEntriesProps = {
|
|||
}
|
||||
|
||||
export type FetchEntriesPropsSettings = {
|
||||
hidePrivateSubscriptionsInTimeline: boolean
|
||||
unreadOnly: boolean
|
||||
hidePrivateSubscriptionsInTimeline?: boolean
|
||||
unreadOnly?: boolean
|
||||
}
|
||||
|
||||
export type UseEntriesProps = {
|
||||
|
|
|
|||
|
|
@ -146,23 +146,33 @@ class Morph {
|
|||
}
|
||||
|
||||
toCollections(
|
||||
data: HonoApiClient.Entry_Post | HonoApiClient.Entry_Inbox_Post,
|
||||
data: HonoApiClient.Entry_Post | HonoApiClient.Entry_Inbox_Post | undefined,
|
||||
view: FeedViewType,
|
||||
): CollectionModel[] {
|
||||
if (!data) return [] satisfies CollectionModel[]
|
||||
return data
|
||||
.map((item) => {
|
||||
if (!item.collections) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
createdAt: item.collections.createdAt,
|
||||
entryId: item.entries.id,
|
||||
feedId: item.feeds.id,
|
||||
view,
|
||||
} satisfies CollectionModel
|
||||
): {
|
||||
collections: CollectionModel[]
|
||||
entryIdsNotInCollections: string[]
|
||||
} {
|
||||
if (!data) return { collections: [], entryIdsNotInCollections: [] }
|
||||
|
||||
const collections: CollectionModel[] = []
|
||||
const entryIdsNotInCollections: string[] = []
|
||||
for (const item of data) {
|
||||
if (!item.collections) {
|
||||
entryIdsNotInCollections.push(item.entries.id)
|
||||
continue
|
||||
}
|
||||
collections.push({
|
||||
createdAt: item.collections.createdAt,
|
||||
entryId: item.entries.id,
|
||||
feedId: item.feeds.id,
|
||||
view,
|
||||
})
|
||||
.filter((i) => i !== null)
|
||||
}
|
||||
|
||||
return {
|
||||
collections,
|
||||
entryIdsNotInCollections,
|
||||
}
|
||||
}
|
||||
|
||||
toEntry(data?: HonoApiClient.Entry_Get | HonoApiClient.Entry_Inbox_Get): EntryModel | null {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { tracker } from "@follow/tracker"
|
|||
import { omit } from "es-toolkit"
|
||||
|
||||
import { apiClient } from "../context"
|
||||
import { invalidateEntriesQuery } from "../entry/hooks"
|
||||
import { getFeedById } from "../feed/getter"
|
||||
import { feedActions } from "../feed/store"
|
||||
import { inboxActions } from "../inbox/store"
|
||||
|
|
@ -585,6 +586,10 @@ class SubscriptionSyncService {
|
|||
feedIds: folderFeedIds,
|
||||
view: newView,
|
||||
})
|
||||
|
||||
invalidateEntriesQuery({
|
||||
views: [currentView, newView],
|
||||
})
|
||||
}
|
||||
|
||||
async renameCategory({
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
diff --git a/esm/main/ipc.js b/esm/main/ipc.js
|
||||
index 8edfaf4660734f9120a6d5f5f806688c25a3a026..a692b1c16470b0dbed7c53e1c9aeff8a456f1c5a 100644
|
||||
index b5d4478b20cac94e1e1e54900d616286aa9f7733..3639b802c2f4012e943ed2f3d0454fbf5eeda3e3 100644
|
||||
--- a/esm/main/ipc.js
|
||||
+++ b/esm/main/ipc.js
|
||||
@@ -111,14 +111,6 @@ function configureProtocol(client, options) {
|
||||
@@ -134,14 +134,6 @@ function configureProtocol(client, options) {
|
||||
if (app.isReady()) {
|
||||
throw new Error("Sentry SDK should be initialized before the Electron app 'ready' event is fired");
|
||||
}
|
||||
|
|
@ -18,10 +18,10 @@ index 8edfaf4660734f9120a6d5f5f806688c25a3a026..a692b1c16470b0dbed7c53e1c9aeff8a
|
|||
app
|
||||
.whenReady()
|
||||
diff --git a/main/ipc.js b/main/ipc.js
|
||||
index 01fc75bdf031b62195504cc0bf7055ebbf15b641..0519242ba5f391f9f618229044948feefeae1de8 100644
|
||||
index 6866654fb79361ec90cc9b175a624d80c1e614b4..74494413854aba2acf777d641e356d0200b5a6d1 100644
|
||||
--- a/main/ipc.js
|
||||
+++ b/main/ipc.js
|
||||
@@ -111,14 +111,6 @@ function configureProtocol(client, options) {
|
||||
@@ -134,14 +134,6 @@ function configureProtocol(client, options) {
|
||||
if (electron.app.isReady()) {
|
||||
throw new Error("Sentry SDK should be initialized before the Electron app 'ready' event is fired");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ patchedDependencies:
|
|||
hash: 5b5ab1ba36e8c0d7ffee912ebf29c1a18bc101c9c661ceb1bb0bda3deaf4c667
|
||||
path: patches/@pengx17__electron-forge-maker-appimage.patch
|
||||
'@sentry/electron':
|
||||
hash: b5efa039abfa14f7833b762e8a7c1c3ae147fda22954d860ffa3387db9acf8eb
|
||||
hash: a5a19cbba4427bc1e6f675a47170c6e03adc0fcec259258bfa07288185b5a979
|
||||
path: patches/@sentry__electron.patch
|
||||
daisyui@4.12.24:
|
||||
hash: d393ab1cbfbfcff21dce0796a59c2d8a37e2c6dd634a8ab476cbc67e47b93d9c
|
||||
|
|
@ -356,7 +356,7 @@ importers:
|
|||
version: 1.0.1
|
||||
'@sentry/electron':
|
||||
specifier: 6.8.0
|
||||
version: 6.8.0(patch_hash=b5efa039abfa14f7833b762e8a7c1c3ae147fda22954d860ffa3387db9acf8eb)
|
||||
version: 6.8.0(patch_hash=a5a19cbba4427bc1e6f675a47170c6e03adc0fcec259258bfa07288185b5a979)
|
||||
builder-util-runtime:
|
||||
specifier: 9.3.1
|
||||
version: 9.3.1
|
||||
|
|
@ -21940,7 +21940,7 @@ snapshots:
|
|||
|
||||
'@sentry/core@9.30.0': {}
|
||||
|
||||
'@sentry/electron@6.8.0(patch_hash=b5efa039abfa14f7833b762e8a7c1c3ae147fda22954d860ffa3387db9acf8eb)':
|
||||
'@sentry/electron@6.8.0(patch_hash=a5a19cbba4427bc1e6f675a47170c6e03adc0fcec259258bfa07288185b5a979)':
|
||||
dependencies:
|
||||
'@sentry/browser': 9.26.0
|
||||
'@sentry/core': 9.26.0
|
||||
|
|
|
|||
Loading…
Reference in New Issue