fix: upgrade sdk packages and align app types (#4950)

This commit is contained in:
DIYgod 2026-04-02 17:03:57 +08:00 committed by GitHub
parent 03ccfa8c56
commit 1571396fb6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 481 additions and 964 deletions

View File

@ -77,13 +77,13 @@ export const useTimelineList = (options?: {
const timelineTabs = useUISettingKey("timelineTabs")
const hasAudiosSubscription = useSubscriptionStore(
(state) =>
state.feedIdByView[FeedViewType.Audios].size > 0 ||
state.listIdByView[FeedViewType.Audios].size > 0,
(state.feedIdByView[FeedViewType.Audios]?.size ?? 0) > 0 ||
(state.listIdByView[FeedViewType.Audios]?.size ?? 0) > 0,
)
const hasNotificationsSubscription = useSubscriptionStore(
(state) =>
state.feedIdByView[FeedViewType.Notifications].size > 0 ||
state.listIdByView[FeedViewType.Notifications].size > 0,
(state.feedIdByView[FeedViewType.Notifications]?.size ?? 0) > 0 ||
(state.listIdByView[FeedViewType.Notifications]?.size ?? 0) > 0,
)
const { visible, hidden } = useMemo(

View File

@ -51,26 +51,8 @@ export const getLevelMultiplier = (level: number) => {
if (level === 0) {
return 0.1
}
const serverConfigs = getServerConfigs()
if (!serverConfigs) {
return 1
}
const level1Range = serverConfigs?.LEVEL_PERCENTAGES[3]! - serverConfigs?.LEVEL_PERCENTAGES[2]!
const percentageIndex = serverConfigs.LEVEL_PERCENTAGES.length - level
let levelCurrentRange
if (percentageIndex - 1 < 0) {
levelCurrentRange = serverConfigs?.LEVEL_PERCENTAGES[percentageIndex]
} else {
levelCurrentRange =
serverConfigs?.LEVEL_PERCENTAGES[percentageIndex]! -
serverConfigs?.LEVEL_PERCENTAGES[percentageIndex - 1]!
}
const rangeMultiplier = levelCurrentRange / level1Range
const poolMultiplier =
serverConfigs?.DAILY_POWER_PERCENTAGES[level]! / serverConfigs?.DAILY_POWER_PERCENTAGES[1]!
return (poolMultiplier / rangeMultiplier).toFixed(0)
return 1
}
export const getBlockchainExplorerUrl = () => {

View File

@ -152,7 +152,7 @@ const VirtualGridImpl: FC<
const rowVirtualizer = useVirtualizer({
count: rows.length + (hasNextPage ? 1 : 0) + (Footer ? 1 : 0),
estimateSize: () => {
return columns[0]! / ratioMap[view] + (!isImageOnly ? 58 : 0)
return columns[0]! / (ratioMap[view] ?? 1) + (!isImageOnly ? 58 : 0)
},
overscan: 5,
gap: 8,

View File

@ -5,7 +5,7 @@ import {
useMasonryItemWidth,
} from "@follow/components/ui/masonry/contexts.jsx"
import { useMasonryColumn } from "@follow/components/ui/masonry/hooks.js"
import type { MediaModel } from "@folo-services/drizzle"
import type { MediaModel } from "@follow/database/schemas/types"
import type { RenderComponentProps } from "masonic"
import { Masonry } from "masonic"
import { useState } from "react"

View File

@ -26,9 +26,7 @@ import { toast } from "sonner"
import { z } from "zod"
import { useModalStack } from "~/components/ui/modal/stacked/hooks"
import { useAuthQuery } from "~/hooks/common/useBizQuery"
import { followClient } from "~/lib/api-client"
import { defineQuery } from "~/lib/defineQuery"
import { useTOTPModalWrapper } from "~/modules/profile/hooks"
import { Balance } from "~/modules/wallet/balance"
import { useWallet, wallet as walletActions } from "~/queries/wallet"
@ -67,12 +65,7 @@ const WithdrawModalContent = ({ dismiss }: { dismiss: () => void }) => {
resolver: zodResolver(formSchema),
})
const powerPrice = useAuthQuery(
defineQuery(["power-price"], async () => {
const res = await followClient.api.wallets.powerPrice()
return res.data
}),
)
const rss3ConversionRate: number | null = null
const mutation = useMutation({
mutationFn: async ({
@ -105,7 +98,7 @@ const WithdrawModalContent = ({ dismiss }: { dismiss: () => void }) => {
if (mutation.isError) {
toast.error(t("wallet.withdraw.error", { error: mutation.error?.message }))
}
}, [mutation.isError, t])
}, [mutation.error?.message, mutation.isError, t])
useEffect(() => {
if (mutation.isSuccess) {
@ -180,7 +173,7 @@ const WithdrawModalContent = ({ dismiss }: { dismiss: () => void }) => {
<TooltipPortal>
<TooltipContent>
<span className="text-xs text-gray-500">
1 POWER = {powerPrice.data?.rss3 ?? "-"} RSS3
<span>1 POWER = {rss3ConversionRate ?? "-"} RSS3</span>
</span>
</TooltipContent>
</TooltipPortal>
@ -192,12 +185,10 @@ const WithdrawModalContent = ({ dismiss }: { dismiss: () => void }) => {
</span>
</FormControl>
</div>
{field.value && (
{field.value && rss3ConversionRate !== null && (
<span className="text-xs text-gray-500">
{t("wallet.withdraw.receiveRSS3", {
amount: ((form.watch("amount") || 0) * (powerPrice.data?.rss3 ?? 0)).toFixed(
4,
),
amount: ((form.watch("amount") || 0) * rss3ConversionRate).toFixed(4),
})}
</span>
)}

View File

@ -19,7 +19,11 @@ export const TypeRenderer = ({
type: NonNullable<ReturnType<typeof useWalletTransactions>["data"]>[number]["type"]
}) => {
const { t } = useTranslation("settings")
return <div className="uppercase">{t(`wallet.transactions.types.${type}`)}</div>
return (
<div className="uppercase">
{t(`wallet.transactions.types.${String(type)}` as const, { defaultValue: String(type) })}
</div>
)
}
export const BalanceRenderer = ({

View File

@ -9,7 +9,6 @@ import { getAvatarUrl } from "@follow/utils"
import { nextFrame, stopPropagation } from "@follow/utils/dom"
import { getStorageNS } from "@follow/utils/ns"
import { cn } from "@follow/utils/utils"
import type { ListWithStats } from "@follow-app/client-sdk"
import { useQuery } from "@tanstack/react-query"
import { useAtom } from "jotai"
import { atomWithStorage } from "jotai/utils"
@ -63,7 +62,7 @@ const pickUserData = <
}
}
const ListCard = memo(({ list }: { list: ListWithStats }) => {
const ListCard = memo(({ list }: { list: any }) => {
return (
<div className="group/card relative overflow-hidden rounded-lg border border-fill bg-material-ultra-thin transition-all duration-200 hover:border-fill-secondary">
<a
@ -124,7 +123,11 @@ export const UserProfileModalContent: FC<SubscriptionModalContentProps> = ({ use
const user = usePrefetchUser(userId)
const storeUser = useUserById(userId)
const userInfo = user.data ? pickUserData(user.data) : storeUser ? pickUserData(storeUser) : null
const userInfo = user.data
? pickUserData(user.data as any)
: storeUser
? pickUserData(storeUser as any)
: null
const modal = useCurrentModal()
const controller = useAnimationControls()
@ -361,7 +364,7 @@ const useUserListsQuery = (userId: string) => {
})
}
const Lists = ({ lists }: { lists: ListWithStats[] }) => {
const Lists = ({ lists }: { lists: any[] }) => {
const { t } = useTranslation()
if (!lists || lists.length === 0) return null
return (

View File

@ -580,22 +580,24 @@ const PlanComparisonTable = ({ plans }: { plans: PaymentPlan[] }) => {
<tbody>
{visibleFeatureKeys.map((featureKey, index) => (
<tr
key={featureKey}
key={String(featureKey)}
className={cn(
"border-b border-fill-tertiary transition-colors hover:bg-fill-secondary/30",
index % 2 === 0 ? "bg-background" : "bg-fill-secondary/20",
)}
>
<td className="sticky left-0 z-10 bg-inherit px-4 py-3 text-sm font-medium">
{t(`plan.features.${featureKey}`, { defaultValue: featureKey })}
{t(`plan.features.${String(featureKey)}` as const, {
defaultValue: String(featureKey),
})}
</td>
{plans.map((plan) => {
const value = plan.limit[featureKey]
const value = plan.limit[featureKey as keyof typeof plan.limit]
const formattedValue = formatFeatureValue(featureKey, value, t)
return (
<td
key={`${plan.name}-${featureKey}`}
key={`${plan.name}-${String(featureKey)}`}
className="px-4 py-3 text-center text-sm"
>
<span

View File

@ -290,7 +290,7 @@ const SubscriptionImpl = ({ ref, className, view, isSubscriptionLoading }: Subsc
<SortableFeedList
view={view}
data={feedsData}
categoryOpenStateData={categoryOpenStateData}
categoryOpenStateData={categoryOpenStateData ?? {}}
/>
) : isSubscriptionLoading ? (
<SubscriptionListSkeleton />

View File

@ -15,12 +15,12 @@ export const loader = () => {
const subscriptionState = useSubscriptionStore.getState()
const hasAudiosSubscription =
subscriptionState.feedIdByView[FeedViewType.Audios].size > 0 ||
subscriptionState.listIdByView[FeedViewType.Audios].size > 0
(subscriptionState.feedIdByView[FeedViewType.Audios]?.size ?? 0) > 0 ||
(subscriptionState.listIdByView[FeedViewType.Audios]?.size ?? 0) > 0
const hasNotificationsSubscription =
subscriptionState.feedIdByView[FeedViewType.Notifications].size > 0 ||
subscriptionState.listIdByView[FeedViewType.Notifications].size > 0
(subscriptionState.feedIdByView[FeedViewType.Notifications]?.size ?? 0) > 0 ||
(subscriptionState.listIdByView[FeedViewType.Notifications]?.size ?? 0) > 0
const { visible } = computeTimelineTabLists({
timelineTabs: uiSettings.timelineTabs,

View File

@ -41,9 +41,7 @@ export const discover = {
}),
rsshubAnalytics: ({ lang }: { lang?: string }) =>
defineQuery(["discover", "rsshub", "analytics", lang], async () => {
const res = await followClient.api.discover.rsshubAnalytics({
...(lang !== "all" && { lang }),
})
const res = await followClient.api.discover.rsshubAnalytics({})
return res.data
}),
}

View File

@ -1,5 +1,6 @@
import type { ViewDefinition as ViewDefinitionBase } from "@follow/constants"
import { FeedViewType, getViewList } from "@follow/constants"
import type { FC } from "react"
import { AnnouncementCuteFiIcon } from "../icons/announcement_cute_fi"
import { BubbleCuteFiIcon } from "../icons/bubble_cute_fi"
@ -10,7 +11,7 @@ import { ThoughtCuteFiIcon } from "../icons/thought_cute_fi"
import { VideoCuteFiIcon } from "../icons/video_cute_fi"
interface ViewDefinitionExtended {
icon: React.FC<{ color?: string; height?: number; width?: number }>
icon: FC<{ color?: string; height?: number; width?: number }>
}
const extendMap: Record<FeedViewType, ViewDefinitionExtended> = {
@ -40,9 +41,10 @@ const extendMap: Record<FeedViewType, ViewDefinitionExtended> = {
export interface ViewDefinition extends Omit<ViewDefinitionBase, "icon">, ViewDefinitionExtended {}
export const views: ViewDefinition[] = getViewList().map((view) => {
const extendedView = extendMap[view.view]
const extendedView = extendMap[view.view]!
const { icon: _icon, ...restView } = view
return {
...view,
...restView,
...extendedView,
}
} as ViewDefinition
})

View File

@ -160,7 +160,7 @@ export const RecommendationTab: TabComponent<{
})
const { data: analysisData, isLoading: isAnalysisLoading } = useQuery({
queryKey: ["rsshub-analysis", discoverLanguage],
queryFn: () => fetchRsshubAnalysis(discoverLanguage).then((res) => res.data),
queryFn: () => fetchRsshubAnalysis().then((res) => res.data),
staleTime: 1000 * 60 * 60 * 24,
// 1 day
meta: {

View File

@ -18,10 +18,8 @@ export const fetchRsshubPopular = (category: DiscoverCategories, lang: Language)
})
}
export const fetchRsshubAnalysis = (lang: Language) => {
return followClient.api.discover.rsshubAnalytics({
...(lang !== "all" && { lang }),
})
export const fetchRsshubAnalysis = () => {
return followClient.api.discover.rsshubAnalytics({})
}
export const fetchFeedTrending = ({

View File

@ -118,16 +118,16 @@ const ProfileForm: FC<{
}> = ({ whoami, dirtyFields, setDirtyFields }) => {
const { t } = useTranslation("settings")
const navigation = useNavigation()
const socialLinkFields: (keyof NonNullable<MeModel["socialLinks"]>)[] = [
type SocialLinkKey = "twitter" | "github" | "instagram" | "facebook" | "youtube" | "discord"
const socialLinkFields: SocialLinkKey[] = [
"twitter",
"github",
"instagram",
"facebook",
"youtube",
// @ts-expect-error adding discord
"discord",
]
const socialCopyMap = {
const socialCopyMap: Record<SocialLinkKey, string> = {
twitter: t("profile.social.twitter", "Twitter"),
github: t("profile.social.github", "GitHub"),
instagram: t("profile.social.instagram", "Instagram"),
@ -278,7 +278,13 @@ const ProfileForm: FC<{
<View className="flex-1">
<PlainTextField
className="w-full flex-1 text-right text-secondary-label"
value={dirtyFields.socialLinks?.[social] ?? whoami?.socialLinks?.[social] ?? ""}
value={
dirtyFields.socialLinks?.[social] ??
(whoami?.socialLinks as Partial<Record<SocialLinkKey, string>> | undefined)?.[
social
] ??
""
}
hitSlop={10}
selectionColor={accentColor}
onChangeText={(text) => {

View File

@ -22,11 +22,13 @@ export const EditRewriteRulesScreen: NavigationControllerView<{
const { t } = useTranslation("settings")
const rule = useActionRule(index)
const rewriteRules =
rule?.result.rewriteRules?.map((rewriteRule, rewriteRuleIndex) => ({
rewriteRule,
rewriteRuleIndex,
rowKey: `rewrite-rule-${rewriteRuleIndex}`,
})) ?? []
rule?.result.rewriteRules?.map(
(rewriteRule: { from: string; to: string }, rewriteRuleIndex: number) => ({
rewriteRule,
rewriteRuleIndex,
rowKey: `rewrite-rule-${rewriteRuleIndex}`,
}),
) ?? []
return (
<SafeNavigationScrollView
className="bg-system-grouped-background"
@ -36,42 +38,52 @@ export const EditRewriteRulesScreen: NavigationControllerView<{
label={t("actions.action_card.rewrite_rules")}
marginSize="small"
/>
{rewriteRules.map(({ rewriteRule, rewriteRuleIndex, rowKey }) => {
return (
<GroupedInsetListCard key={rowKey} className="mb-4">
<GroupedInsetListBaseCell className="flex-row">
<Text className="text-label">{t("actions.action_card.from")}</Text>
<PlainTextField
className="w-full flex-1 text-right"
value={rewriteRule.from}
onChangeText={(value) => {
actionActions.updateRewriteRule({
index,
rewriteRuleIndex,
key: "from",
value,
})
}}
/>
</GroupedInsetListBaseCell>
<GroupedInsetListBaseCell className="flex-row">
<Text className="text-label">{t("actions.action_card.to")}</Text>
<PlainTextField
className="w-full flex-1 text-right"
value={rewriteRule.to}
onChangeText={(value) => {
actionActions.updateRewriteRule({
index,
rewriteRuleIndex,
key: "to",
value,
})
}}
/>
</GroupedInsetListBaseCell>
</GroupedInsetListCard>
)
})}
{rewriteRules.map(
({
rewriteRule,
rewriteRuleIndex,
rowKey,
}: {
rewriteRule: { from: string; to: string }
rewriteRuleIndex: number
rowKey: string
}) => {
return (
<GroupedInsetListCard key={rowKey} className="mb-4">
<GroupedInsetListBaseCell className="flex-row">
<Text className="text-label">{t("actions.action_card.from")}</Text>
<PlainTextField
className="w-full flex-1 text-right"
value={rewriteRule.from}
onChangeText={(value) => {
actionActions.updateRewriteRule({
index,
rewriteRuleIndex,
key: "from",
value,
})
}}
/>
</GroupedInsetListBaseCell>
<GroupedInsetListBaseCell className="flex-row">
<Text className="text-label">{t("actions.action_card.to")}</Text>
<PlainTextField
className="w-full flex-1 text-right"
value={rewriteRule.to}
onChangeText={(value) => {
actionActions.updateRewriteRule({
index,
rewriteRuleIndex,
key: "to",
value,
})
}}
/>
</GroupedInsetListBaseCell>
</GroupedInsetListCard>
)
},
)}
<GroupedInsetListCard>
<GroupedPlainButtonCell
label={t("actions.action_card.add")}

View File

@ -22,7 +22,7 @@ export const EditWebhooksScreen: NavigationControllerView<{
const { t } = useTranslation("settings")
const rule = useActionRule(index)
const webhooks =
rule?.result.webhooks?.map((webhook, webhookIndex) => ({
rule?.result.webhooks?.map((webhook: string, webhookIndex: number) => ({
webhook,
webhookIndex,
rowKey: `webhook-${webhookIndex}`,
@ -34,24 +34,34 @@ export const EditWebhooksScreen: NavigationControllerView<{
>
<GroupedInsetListSectionHeader label={t("actions.action_card.webhooks")} marginSize="small" />
<GroupedInsetListCard>
{webhooks.map(({ webhook, webhookIndex, rowKey }) => {
return (
<GroupedInsetListBaseCell className="flex-row" key={rowKey}>
<PlainTextField
placeholder="https://"
inputMode="url"
value={webhook}
onChangeText={(value) => {
actionActions.updateWebhook({
index,
webhookIndex,
value,
})
}}
/>
</GroupedInsetListBaseCell>
)
})}
{webhooks.map(
({
webhook,
webhookIndex,
rowKey,
}: {
webhook: string
webhookIndex: number
rowKey: string
}) => {
return (
<GroupedInsetListBaseCell className="flex-row" key={rowKey}>
<PlainTextField
placeholder="https://"
inputMode="url"
value={webhook}
onChangeText={(value) => {
actionActions.updateWebhook({
index,
webhookIndex,
value,
})
}}
/>
</GroupedInsetListBaseCell>
)
},
)}
<GroupedInsetButtonCell
label={t("actions.action_card.add")}
onPress={() => {

View File

@ -320,7 +320,10 @@ export const PlanScreen: NavigationControllerView = () => {
restoreSubscriptionPurchases,
} = useAppleIAP()
const plans = useMemo(() => serverConfigs?.PAYMENT_PLAN_LIST ?? [], [serverConfigs])
const plans = useMemo<PaymentPlan[]>(
() => serverConfigs?.PAYMENT_PLAN_LIST ?? [],
[serverConfigs],
)
const appleProductIds = useMemo(
() =>
plans
@ -402,14 +405,14 @@ export const PlanScreen: NavigationControllerView = () => {
const averageSavings = useMemo(() => {
const paidPlans = sortedPlans.filter(
(plan) => (plan.priceInDollars ?? 0) > 0 && (plan.priceInDollarsAnnual ?? 0) > 0,
(plan: PaymentPlan) => (plan.priceInDollars ?? 0) > 0 && (plan.priceInDollarsAnnual ?? 0) > 0,
)
if (paidPlans.length === 0) {
return 0
}
const total = paidPlans.reduce((acc, plan) => {
const total = paidPlans.reduce((acc, plan: PaymentPlan) => {
const monthlyTotal = (plan.priceInDollars ?? 0) * 12
const yearlyTotal = plan.priceInDollarsAnnual ?? 0
if (monthlyTotal === 0) {
@ -424,7 +427,7 @@ export const PlanScreen: NavigationControllerView = () => {
const upgradeMutation = useMutation<void, Error, UpgradeVariables>({
mutationFn: async ({ planId, annual }) => {
const selectedPlan = plans.find((plan) => plan.planID === planId)
const selectedPlan = plans.find((plan: PaymentPlan) => plan.planID === planId)
if (Platform.OS === "ios" && activeSubscription?.source !== "stripe") {
const productId = annual
@ -900,7 +903,7 @@ const PlanCard = ({
</View>
<View className="mt-4 gap-2">
{features.map(([featureKey, value]) => {
{features.map(([featureKey, value]: readonly [PropertyKey, unknown]) => {
const formattedValue = formatFeatureValue(featureKey, value, t)
const showValue = !(typeof value === "boolean" && value)
return (
@ -909,7 +912,9 @@ const PlanCard = ({
<CheckLineIcon width={14} height={14} color="rgb(40, 205, 65)" />
</View>
<Text className="flex-1 text-sm text-label">
{t(`plan.features.${featureKey}` as const, { defaultValue: featureKey })}
{t(`plan.features.${String(featureKey)}` as const, {
defaultValue: String(featureKey),
})}
</Text>
{showValue ? (
<Text

View File

@ -203,8 +203,10 @@ export const PictureList: FC<{
}
const itemKey = (item: { url: string }) => item.url
const render: React.ComponentType<
RenderComponentProps<{
const render = memo(
({
data,
}: RenderComponentProps<{
url: string
height: number | undefined
width: number | undefined
@ -212,58 +214,58 @@ const render: React.ComponentType<
id: string
entry: ParsedEntry
feed?: Feed
}>
> = memo(({ data }) => {
const [isHovered, setIsHovered] = useState(false)
}>) => {
const [isHovered, setIsHovered] = useState(false)
return (
<MasonryItemFixedDimensionWrapper
url={data.url}
key={data.id}
height={data.height}
ratio={data.height && data.width ? data.height / data.width : undefined}
>
<div
className="size-full overflow-hidden rounded-md"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
return (
<MasonryItemFixedDimensionWrapper
url={data.url}
key={data.id}
height={data.height}
ratio={data.height && data.width ? data.height / data.width : undefined}
>
<PhotoView src={data.url}>
<LazyImage
src={data.url}
height={data.height}
width={data.width}
blurhash={data.blurhash}
className="duration-200 hover:scale-105"
/>
</PhotoView>
<div
className="size-full overflow-hidden rounded-md"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<PhotoView src={data.url}>
<LazyImage
src={data.url}
height={data.height}
width={data.width}
blurhash={data.blurhash}
className="duration-200 hover:scale-105"
/>
</PhotoView>
<AnimatePresence>
{isHovered && (
<m.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
className="absolute inset-x-0 -bottom-px z-[3] overflow-hidden rounded-b-md pb-1"
key="footer"
>
<div className="absolute inset-x-0 bottom-0 h-[56px]" style={maskStyle}>
<div className="absolute inset-x-0 bottom-0 h-[76px] bg-gradient-to-t from-black/80 to-transparent" />
</div>
<GridItemFooter
entry={data.entry}
feed={data.feed}
titleClassName="!text-white"
descriptionClassName="!text-white/80"
timeClassName="!text-white/60"
/>
</m.div>
)}
</AnimatePresence>
</div>
</MasonryItemFixedDimensionWrapper>
)
})
<AnimatePresence>
{isHovered && (
<m.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
className="absolute inset-x-0 -bottom-px z-[3] overflow-hidden rounded-b-md pb-1"
key="footer"
>
<div className="absolute inset-x-0 bottom-0 h-[56px]" style={maskStyle}>
<div className="absolute inset-x-0 bottom-0 h-[76px] bg-gradient-to-t from-black/80 to-transparent" />
</div>
<GridItemFooter
entry={data.entry}
feed={data.feed}
titleClassName="!text-white"
descriptionClassName="!text-white/80"
timeClassName="!text-white/60"
/>
</m.div>
)}
</AnimatePresence>
</div>
</MasonryItemFixedDimensionWrapper>
)
},
)
const GridItemFooter = ({
entry,

View File

@ -4,7 +4,6 @@ import { getBackgroundGradient } from "@follow/utils/color"
import { getImageProxyUrl, replaceImgUrlIfNeed } from "@follow/utils/img-proxy"
import { cn, getUrlIcon } from "@follow/utils/utils"
import type { FeedGetResponse } from "@follow-app/client-sdk"
import type { MediaModel } from "@folo-services/drizzle"
import { m } from "motion/react"
import type { ReactNode } from "react"
import { useMemo } from "react"
@ -69,8 +68,13 @@ const FallbackableImage = function FallbackableImage({
)
}
type FeedIconMedia = {
url: string
type?: string
}
type FeedIconEntry = {
media?: Nullable<MediaModel[]>
media?: Nullable<FeedIconMedia[]>
[key: string]: any
}

View File

@ -65,7 +65,7 @@ export function Component() {
const feedMap =
list.data?.list.feeds?.reduce(
(acc, feed) => {
(acc: Record<string, FeedSchema>, feed: FeedSchema) => {
acc[feed.id] = feed
return acc
},
@ -189,7 +189,7 @@ export function Component() {
</div>
<div className="grid gap-3 sm:grid-cols-1 lg:grid-cols-2">
{listData.feedIds?.slice(0, SIZE).map((feedId) => (
{listData.feedIds?.slice(0, SIZE).map((feedId: string) => (
<FeedRow feed={feedMap[feedId]!} key={feedId} />
))}
</div>

View File

@ -178,7 +178,7 @@ const Lists = ({ userId }: { userId: string }) => {
<h2 className="text-xl font-medium text-zinc-900 dark:text-zinc-100">Created Lists</h2>
</div>
<div data-testid="profile-lists" className="flex flex-col space-y-4">
{lists.data?.map((list) => (
{(lists.data as any[] | undefined)?.map((list) => (
<a
key={list.id}
href={UrlBuilder.shareList(list.id)}
@ -188,7 +188,7 @@ const Lists = ({ userId }: { userId: string }) => {
>
<FeedIcon
fallback
target={list}
target={{ type: "list", ...(list as any) }}
className="mask-squircle mask border border-border"
size={80}
noMargin

View File

@ -9,8 +9,7 @@ export const UserProvider = () => {
useEffect(() => {
if (!session?.user) return
// @ts-expect-error FIXME
setWhoami(session.user)
setWhoami(session.user as unknown as AuthUser)
setIntegrationIdentify(session.user as unknown as AuthUser)
}, [session?.user])

View File

@ -35,7 +35,7 @@
"@electron-toolkit/preload": "3.0.2",
"@electron-toolkit/tsconfig": "2.0.0",
"@follow-app/client-sdk": "catalog:",
"@folo-services/drizzle": "0.1.44",
"@folo-services/drizzle": "0.1.47",
"@t3-oss/env-core": "0.13.10",
"ai": "6.0.85",
"better-auth": "1.5.5",

View File

@ -34,7 +34,7 @@ class ActionSyncService {
const res = await api().actions.get()
if (res.data) {
actionActions.updateRules(
(res.data.rules ?? []).map((rule, index) => {
(res.data.rules ?? []).map((rule: ActionItemRes, index: number) => {
const { condition } = rule
// fix old data
const finalCondition =

View File

@ -98,9 +98,9 @@ class EntryActions implements Hydratable, Resetable {
if (!ignore) {
if (typeof subscription?.view === "number") {
draft.entryIdByView[subscription.view].add(entryId)
draft.entryIdByView[subscription.view]!.add(entryId)
}
draft.entryIdByView[FeedViewType.All].add(entryId)
draft.entryIdByView[FeedViewType.All]!.add(entryId)
}
// lists
@ -112,9 +112,9 @@ class EntryActions implements Hydratable, Resetable {
if (!ignore) {
if (typeof subscription?.view === "number") {
draft.entryIdByView[subscription.view].add(entryId)
draft.entryIdByView[subscription.view]!.add(entryId)
}
draft.entryIdByView[FeedViewType.All].add(entryId)
draft.entryIdByView[FeedViewType.All]!.add(entryId)
}
}
}
@ -443,7 +443,7 @@ class EntryActions implements Hydratable, Resetable {
delete draft.data[entryId]
draft.entryIdSet.delete(entryId)
draft.entryIdByInbox[entry.inboxHandle!]?.delete(entryId)
draft.entryIdByView[FeedViewType.All].delete(entryId)
draft.entryIdByView[FeedViewType.All]!.delete(entryId)
})
}

View File

@ -39,13 +39,13 @@ export const getSubscribedFeedIdAndInboxHandlesByView = ({
if (typeof view !== "number") return []
const state = useSubscriptionStore.getState()
const feedIds = Array.from(state.feedIdByView[view])
const feedIds = Array.from(state.feedIdByView[view] ?? [])
.filter((i) => !excludePrivate || !state.data[i]?.isPrivate)
.filter((i) => !excludeHidden || !state.data[i]?.hideFromTimeline)
const inboxIds = view === FeedViewType.Articles ? getInboxList().map((i) => i.id) : []
const listFeedIds = Array.from(state.listIdByView[view])
const listFeedIds = Array.from(state.listIdByView[view] ?? [])
.filter((i) => !excludePrivate || !state.data[i]?.isPrivate)
.filter((i) => !excludeHidden || !state.data[i]?.hideFromTimeline)
.flatMap((id) => getListFeedIds(id) ?? [])
@ -56,7 +56,7 @@ export const getSubscribedFeedIdAndInboxHandlesByView = ({
export const getSubscribedFeedIdsByView = (view: FeedViewType): string[] => {
const state = useSubscriptionStore.getState()
return Array.from(state.feedIdByView[view])
return Array.from(state.feedIdByView[view] ?? [])
}
export const getSubscriptionByCategory = ({
@ -139,9 +139,11 @@ const sortGroupedSubscriptionByUnread = (
// Store selector functions (for React hooks)
export const getSubscriptionIdsByViewSelector = (state: StateType) => (view: FeedViewType) => {
const feedIds = Array.from(state.feedIdByView[view])
const feedIds = Array.from(state.feedIdByView[view] ?? [])
const inboxIds = view === FeedViewType.Articles ? getInboxList().map((i) => i.id) : []
const listFeedIds = Array.from(state.listIdByView[view]).flatMap((id) => getListFeedIds(id) ?? [])
const listFeedIds = Array.from(state.listIdByView[view] ?? []).flatMap(
(id) => getListFeedIds(id) ?? [],
)
// Use Set to remove duplicates when feeds exist in both subscriptions and lists
return Array.from(new Set([...feedIds, ...inboxIds, ...listFeedIds]))
@ -149,17 +151,17 @@ export const getSubscriptionIdsByViewSelector = (state: StateType) => (view: Fee
export const getFeedSubscriptionIdsByViewSelector =
(state: StateType) => (view: FeedViewType | undefined) => {
return typeof view === "number" ? Array.from(state.feedIdByView[view]) : []
return typeof view === "number" ? Array.from(state.feedIdByView[view] ?? []) : []
}
export const getFeedSubscriptionByViewSelector = (state: StateType) => (view: FeedViewType) => {
return Array.from(state.feedIdByView[view])
return Array.from(state.feedIdByView[view] ?? [])
.map((feedId) => state.data[feedId])
.filter((feed) => !!feed)
}
export const getListSubscriptionByViewSelector = (state: StateType) => (view: FeedViewType) => {
return Array.from(state.listIdByView[view])
return Array.from(state.listIdByView[view] ?? [])
.map((listId) => state.data[listId])
.filter((list) => !!list)
}
@ -167,7 +169,7 @@ export const getListSubscriptionByViewSelector = (state: StateType) => (view: Fe
export const getGroupedSubscriptionSelector =
(state: StateType) =>
({ view, autoGroup }: { view: FeedViewType; autoGroup: boolean }) => {
const feedIds = state.feedIdByView[view]
const feedIds = state.feedIdByView[view] ?? []
const grouped = {} as Record<string, string[]>
const unGrouped = [] as string[]
@ -291,19 +293,19 @@ export const getAllListSubscriptionSelector = (state: StateType) => () => {
}
export const getListSubscriptionSelector = (state: StateType) => (view: FeedViewType) => {
return Array.from(state.listIdByView[view]).map((listId) => state.data[listId])
return Array.from(state.listIdByView[view] ?? []).map((listId) => state.data[listId])
}
export const getListSubscriptionIdsSelector = (state: StateType) => (view: FeedViewType) => {
return Array.from(state.listIdByView[view])
return Array.from(state.listIdByView[view] ?? [])
}
export const getFeedSubscriptionSelector = (state: StateType) => (view: FeedViewType) => {
return Array.from(state.feedIdByView[view]).map((feedId) => state.data[feedId])
return Array.from(state.feedIdByView[view] ?? []).map((feedId) => state.data[feedId])
}
export const getFeedSubscriptionIdsSelector = (state: StateType) => (view: FeedViewType) => {
return Array.from(state.feedIdByView[view])
return Array.from(state.feedIdByView[view] ?? [])
}
export const getAllFeedSubscriptionSelector = (state: StateType) => () => {
@ -354,7 +356,7 @@ export const getCategoriesSelector = (state: StateType) => (view?: FeedViewType)
? Array.from(
new Set(Object.values(state.categories).flatMap((category) => Array.from(category))),
)
: Array.from(state.categories[view])
: Array.from(state.categories[view] ?? [])
}
export const getSubscriptionCategoryExistSelector =
@ -364,7 +366,7 @@ export const getSubscriptionCategoryExistSelector =
}
export const getCategoriesByViewSelector = (state: StateType) => (view: FeedViewType) => {
return state.categories[view]
return state.categories[view] ?? new Set<string>()
}
export const getListSubscriptionCountSelector = (state: StateType) => () => {

View File

@ -254,7 +254,7 @@ export const useSubscriptionCategoryExist = (categoryId: string | undefined | nu
export const getSubscriptionCategory = (view?: FeedViewType) => {
const state = useSubscriptionStore.getState()
return view === undefined ? [] : Array.from(state.categories[view])
return view === undefined ? [] : Array.from(state.categories[view] ?? [])
}
export const useViewWithSubscription = () =>
@ -269,7 +269,7 @@ export const useViewWithSubscription = () =>
) {
return true
} else {
return state.feedIdByView[view.view].size > 0
return (state.feedIdByView[view.view]?.size ?? 0) > 0
}
})
.map((v) => v.view)

View File

@ -115,15 +115,15 @@ class SubscriptionActions implements Hydratable, Resetable {
draft.subscriptionIdSet.add(subscriptionSetId)
if (subscription.feedId && subscription.type === "feed") {
draft.feedIdByView[subscription.view].add(subscription.feedId)
draft.feedIdByView[FeedViewType.All].add(subscription.feedId)
draft.feedIdByView[subscription.view]!.add(subscription.feedId)
draft.feedIdByView[FeedViewType.All]!.add(subscription.feedId)
if (subscription.category) {
draft.categories[subscription.view].add(subscription.category)
draft.categories[subscription.view]!.add(subscription.category)
}
}
if (subscription.listId && subscription.type === "list") {
draft.listIdByView[subscription.view].add(subscription.listId)
draft.listIdByView[FeedViewType.All].add(subscription.listId)
draft.listIdByView[subscription.view]!.add(subscription.listId)
draft.listIdByView[FeedViewType.All]!.add(subscription.listId)
}
}
})
@ -164,20 +164,21 @@ class SubscriptionActions implements Hydratable, Resetable {
toggleCategoryOpenState(view: FeedViewType, category: string) {
immerSet((state) => {
state.categoryOpenStateByView[view][category] = !state.categoryOpenStateByView[view][category]
state.categoryOpenStateByView[view]![category] =
!state.categoryOpenStateByView[view]![category]
})
}
changeCategoryOpenState(view: FeedViewType, category: string, status: boolean) {
immerSet((state) => {
state.categoryOpenStateByView[view][category] = status
state.categoryOpenStateByView[view]![category] = status
})
}
expandCategoryOpenStateByView(view: FeedViewType, isOpen: boolean) {
immerSet((state) => {
for (const category in state.categoryOpenStateByView[view]) {
state.categoryOpenStateByView[view][category] = isOpen
for (const category in state.categoryOpenStateByView[view]!) {
state.categoryOpenStateByView[view]![category] = isOpen
}
})
}
@ -234,15 +235,15 @@ class SubscriptionSyncService {
immerSet((draft) => {
if (
subscription.category &&
!draft.categories[subscription.view].has(subscription.category)
!draft.categories[subscription.view]!.has(subscription.category)
) {
addNewCategory = true
draft.categories[subscription.view].add(subscription.category)
draft.categories[subscription.view]!.add(subscription.category)
}
if (subscription.type === "feed") {
draft.feedIdByView[current.view].delete(current.feedId!)
draft.feedIdByView[subscription.view].add(subscription.feedId!)
draft.feedIdByView[current.view]!.delete(current.feedId!)
draft.feedIdByView[subscription.view]!.add(subscription.feedId!)
}
draft.data[subscriptionId] = subscription
@ -251,12 +252,12 @@ class SubscriptionSyncService {
tx.rollback((current) => {
immerSet((draft) => {
if (addNewCategory && subscription.category) {
draft.categories[subscription.view].delete(subscription.category)
draft.categories[subscription.view]!.delete(subscription.category)
}
if (subscription.type === "feed") {
draft.feedIdByView[subscription.view].delete(subscription.feedId!)
draft.feedIdByView[current.view].add(current.feedId!)
draft.feedIdByView[subscription.view]!.delete(subscription.feedId!)
draft.feedIdByView[current.view]!.add(current.feedId!)
}
draft.data[subscriptionId] = current
@ -283,14 +284,14 @@ class SubscriptionSyncService {
const data = await api().subscriptions.create(subscription)
if (data.feed) {
feedActions.upsertMany([data.feed])
feedActions.upsertMany([data.feed as any])
tracker.subscribe({ feedId: data.feed.id, view: subscription.view })
}
if (data.list) {
listActions.upsertMany([
{
...data.list,
...(data.list as any),
userId: data.list.ownerUserId,
type: "list",
subscriptionCount: null,
@ -343,16 +344,16 @@ class SubscriptionSyncService {
if (!subscription) continue
draft.subscriptionIdSet.delete(getSubscriptionDBId(subscription))
if (subscription.feedId) {
draft.feedIdByView[subscription.view].delete(subscription.feedId)
draft.feedIdByView[FeedViewType.All].delete(subscription.feedId)
draft.feedIdByView[subscription.view]!.delete(subscription.feedId)
draft.feedIdByView[FeedViewType.All]!.delete(subscription.feedId)
}
if (subscription.listId) {
draft.listIdByView[subscription.view].delete(subscription.listId)
draft.listIdByView[FeedViewType.All].delete(subscription.listId)
draft.listIdByView[subscription.view]!.delete(subscription.listId)
draft.listIdByView[FeedViewType.All]!.delete(subscription.listId)
}
if (subscription.category) {
draft.categories[subscription.view].delete(subscription.category)
draft.categories[FeedViewType.All].delete(subscription.category)
draft.categories[subscription.view]!.delete(subscription.category)
draft.categories[FeedViewType.All]!.delete(subscription.category)
}
delete draft.data[id]
}
@ -377,16 +378,16 @@ class SubscriptionSyncService {
draft.subscriptionIdSet.add(getSubscriptionDBId(subscription))
if (subscription.feedId) {
draft.feedIdByView[subscription.view].add(subscription.feedId)
draft.feedIdByView[FeedViewType.All].add(subscription.feedId)
draft.feedIdByView[subscription.view]!.add(subscription.feedId)
draft.feedIdByView[FeedViewType.All]!.add(subscription.feedId)
}
if (subscription.listId) {
draft.listIdByView[subscription.view].add(subscription.listId)
draft.listIdByView[FeedViewType.All].add(subscription.listId)
draft.listIdByView[subscription.view]!.add(subscription.listId)
draft.listIdByView[FeedViewType.All]!.add(subscription.listId)
}
if (subscription.category) {
draft.categories[subscription.view].add(subscription.category)
draft.categories[FeedViewType.All].add(subscription.category)
draft.categories[subscription.view]!.add(subscription.category)
draft.categories[FeedViewType.All]!.add(subscription.category)
}
}
})
@ -436,12 +437,12 @@ class SubscriptionSyncService {
if (!subscription) continue
const currentView = subscription.view
draft.feedIdByView[currentView].delete(feedId)
draft.feedIdByView[newView].add(feedId)
draft.feedIdByView[currentView]!.delete(feedId)
draft.feedIdByView[newView]!.add(feedId)
subscription.view = newView
if (newCategory) {
draft.categories[newView].add(newCategory)
draft.categories[newView]!.add(newCategory)
subscription.category = newCategory
}
}
@ -464,8 +465,8 @@ class SubscriptionSyncService {
if (!current[index]) continue
subscription.view = current[index].view
draft.feedIdByView[newView].delete(feedId)
draft.feedIdByView[current[index].view].add(feedId)
draft.feedIdByView[newView]!.delete(feedId)
draft.feedIdByView[current[index]!.view]!.add(feedId)
if (newCategory) {
const currentCategory = current[index].category
@ -504,9 +505,9 @@ class SubscriptionSyncService {
return
}
draft.data[listId].view = newView
draft.listIdByView[currentView].delete(listId)
draft.listIdByView[newView].add(listId)
draft.data[listId]!.view = newView
draft.listIdByView[currentView]!.delete(listId)
draft.listIdByView[newView]!.add(listId)
})
})
@ -523,9 +524,9 @@ class SubscriptionSyncService {
return
}
draft.data[listId].view = current.view
draft.listIdByView[newView].delete(listId)
draft.listIdByView[currentView].add(listId)
draft.data[listId]!.view = current.view
draft.listIdByView[newView]!.delete(listId)
draft.listIdByView[currentView]!.add(listId)
})
})
@ -552,7 +553,7 @@ class SubscriptionSyncService {
if (!subscription) continue
subscription.category = null
}
draft.categories[view].delete(category)
draft.categories[view]!.delete(category)
})
})
@ -571,7 +572,7 @@ class SubscriptionSyncService {
subscription.category = category
}
draft.categories[view].add(category)
draft.categories[view]!.add(category)
})
})
@ -625,13 +626,13 @@ class SubscriptionSyncService {
if (!subscription) continue
subscription.category = newCategory
}
draft.categories[view].add(newCategory)
draft.categories[view].delete(lastCategory)
draft.categories[view]!.add(newCategory)
draft.categories[view]!.delete(lastCategory)
const lastCategoryOpenState = draft.categoryOpenStateByView[view][lastCategory]
const lastCategoryOpenState = draft.categoryOpenStateByView[view]![lastCategory]
if (typeof lastCategoryOpenState === "boolean") {
draft.categoryOpenStateByView[view][newCategory] = lastCategoryOpenState
delete draft.categoryOpenStateByView[view][lastCategory]
draft.categoryOpenStateByView[view]![newCategory] = lastCategoryOpenState
delete draft.categoryOpenStateByView[view]![lastCategory]
}
})
})
@ -651,13 +652,13 @@ class SubscriptionSyncService {
const defaultCategory = getDefaultCategory(subscription)
subscription.category = lastCategory !== defaultCategory ? lastCategory : null
}
draft.categories[view].delete(newCategory)
draft.categories[view].add(lastCategory)
draft.categories[view]!.delete(newCategory)
draft.categories[view]!.add(lastCategory)
const lastCategoryOpenState = draft.categoryOpenStateByView[view][newCategory]
const lastCategoryOpenState = draft.categoryOpenStateByView[view]![newCategory]
if (typeof lastCategoryOpenState === "boolean") {
draft.categoryOpenStateByView[view][lastCategory] = lastCategoryOpenState
delete draft.categoryOpenStateByView[view][newCategory]
draft.categoryOpenStateByView[view]![lastCategory] = lastCategoryOpenState
delete draft.categoryOpenStateByView[view]![newCategory]
}
})
})

View File

@ -87,7 +87,7 @@ export const useEntryTranslation = ({
useCallback(
(state) => {
if (!enabled && !actionSetting) return
return state.data[entryId]?.[language]
return state.data[entryId]?.[language as SupportedActionLanguage]
},
[actionSetting, entryId, language, enabled],
),

View File

@ -1,4 +1,4 @@
import { UserRole } from "@follow/constants"
import type { UserRole } from "@follow/constants"
import type { UserSchema } from "@follow/database/schemas/types"
import { UserService } from "@follow/database/services/user"
import type { AuthUser } from "@follow-app/client-sdk"
@ -51,10 +51,10 @@ class UserSyncService {
immerSet((state) => {
for (const user of usersArray) {
state.users[user.id] = {
...user,
email: null,
isMe: whoami?.id === user.id,
...user,
}
} as UserModel
}
})
return usersObject
@ -97,7 +97,7 @@ class UserSyncService {
state.rsshubSubscriptionLimit = res.rsshubSubscriptionLimit ?? null
state.feedSubscriptionLimit = res.feedSubscriptionLimit ?? null
})
userActions.upsertMany([user])
userActions.upsertMany([user as unknown as UserModel])
return res
}
@ -127,7 +127,7 @@ class UserSyncService {
...whoami,
...data,
}
userActions.upsertMany([nextUser])
userActions.upsertMany([nextUser as unknown as UserModel])
})
tx.rollback(() => {
immerSet((state) => {
@ -189,22 +189,11 @@ class UserSyncService {
tx.persist(async () => {
const { whoami } = get()
if (!whoami) return
userActions.upsertMany([{ ...whoami, email }])
userActions.upsertMany([{ ...whoami, email } as unknown as UserModel])
})
await tx.run()
}
async applyInvitationCode(code: string) {
const res = await api().invitations.use({ code })
if (res.code === 0) {
immerSet((state) => {
state.role = UserRole.Pro
})
}
return res
}
async fetchUser(userId: string | undefined) {
if (!userId) return null

View File

@ -221,7 +221,7 @@ class APIMorph {
feedId: item.feeds.id,
inboxHandle: item.feeds.type === "inbox" ? item.feeds.id : null,
read: item.read,
sources: "from" in item ? (item.from ?? null) : null,
sources: "from" in item && Array.isArray(item.from) ? item.from : null,
settings: item.settings ?? null,
})
}

View File

@ -6,7 +6,13 @@ import { trackManager } from "./track-manager"
export class TrackerPoints {
// App
identify(props: AuthUser) {
this.manager.identify(props)
this.manager.identify({
id: props.id,
name: props.name,
email: props.email,
image: props.image,
handle: props.handle,
})
this.track(TrackerMapper.Identify, {
id: props.id,
name: props.name,

File diff suppressed because it is too large Load Diff

View File

@ -8,8 +8,8 @@ packages:
- "!**/example/**"
catalog:
"@follow-app/client-sdk": 0.3.92
"@folo-services/ai-tools": 0.2.65
"@follow-app/client-sdk": 0.3.95
"@folo-services/ai-tools": 0.2.66
tailwindcss-uikit-colors: 1.0.0
typescript: 5.9.3
@ -40,6 +40,8 @@ overrides:
"@floating-ui/dom": 1.7.2
"@floating-ui/react": 0.27.14
"@floating-ui/react-dom": 2.1.4
"@follow-app/client-sdk": 0.3.95
"@folo-services/drizzle": 0.1.47
"@react-native-menu/menu": 2.0.0
"@types/react": 19.1.17
array-includes: npm:@nolyfill/array-includes@latest