Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: feed boosting functionality #1030

Draft
wants to merge 5 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/renderer/src/hooks/biz/useFeedActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { FeedViewType } from "~/lib/enum"
import type { NativeMenuItem, NullableNativeMenuItem } from "~/lib/native-menu"
import { UrlBuilder } from "~/lib/url-builder"
import { isBizId } from "~/lib/utils"
import { useBoostModal } from "~/modules/boost/hooks"
import { useFeedClaimModal } from "~/modules/claim"
import { FeedForm } from "~/modules/discover/feed-form"
import { InboxForm } from "~/modules/discover/inbox-form"
Expand Down Expand Up @@ -52,6 +53,7 @@ export const useFeedActions = ({

const { mutateAsync: addFeedToListMutation } = useAddFeedToFeedList()
const { mutateAsync: removeFeedFromListMutation } = useRemoveFeedFromFeedList()
const openBoostModal = useBoostModal()

const listByView = useOwnedList(view!)

Expand Down Expand Up @@ -86,6 +88,13 @@ export const useFeedActions = ({
},
]
: []),
{
type: "text" as const,
label: "Boost",
click: () => {
openBoostModal(feedId)
},
},
{
type: "separator" as const,
disabled: isEntryList,
Expand Down
21 changes: 21 additions & 0 deletions apps/renderer/src/modules/boost/hooks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useCallback } from "react"

import { useModalStack } from "~/components/ui/modal"

import { BoostModalContent } from "./modal"

export const useBoostModal = () => {
const { present } = useModalStack()

return useCallback(
(feedId: string) => {
present({
id: "boost",
title: "Boost",
content: () => <BoostModalContent feedId={feedId} />,
overlay: true,
})
},
[present],
)
}
137 changes: 137 additions & 0 deletions apps/renderer/src/modules/boost/modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { from } from "dnum"
import { useCallback, useState } from "react"

import { Button } from "~/components/ui/button"
import { LoadingWithIcon } from "~/components/ui/loading"
import { useCurrentModal } from "~/components/ui/modal"
import { useAuthQuery, useI18n } from "~/hooks/common"
import { boosts, useBoostFeedMutation } from "~/queries/boosts"
import { useWallet } from "~/queries/wallet"

import { Balance } from "../wallet/balance"
import { RadioCards } from "./radio-cards"

export const BoostModalContent = ({ feedId }: { feedId: string }) => {
const t = useI18n()
const myWallet = useWallet()
const myWalletData = myWallet.data?.[0]

const dPowerBigInt = BigInt(myWalletData?.dailyPowerToken ?? 0)
const cPowerBigInt = BigInt(myWalletData?.cashablePowerToken ?? 0)
const balanceBigInt = cPowerBigInt + dPowerBigInt
const [amount, setAmount] = useState<number>(0)
const amountBigInt = from(amount, 18)[0]
const wrongNumberRange = amountBigInt > balanceBigInt || amountBigInt <= BigInt(0)

const { data: boostStatus, isLoading } = useAuthQuery(boosts.getStatus({ feedId }))
const boostFeedMutation = useBoostFeedMutation()
const { dismiss } = useCurrentModal()

const handleBoost = useCallback(() => {
if (boostFeedMutation.isPending) return
boostFeedMutation.mutate({ feedId, amount: amountBigInt.toString() })
}, [amountBigInt, boostFeedMutation, feedId])

if (isLoading || !boostStatus) {
return (
<div className="center pointer-events-none grow -translate-y-16">
<LoadingWithIcon icon={<i className="i-mgc-trophy-cute-re" />} size="large" />
</div>
)
}

if (boostFeedMutation.isSuccess) {
return (
<div className="flex w-[80vw] max-w-[350px] flex-col gap-5">
<p className="text-sm text-theme-foreground/80">{t("tip_modal.tip_sent")}</p>
<BoostProgress {...boostStatus} />
<p>
<Balance className="mr-1 inline-block text-sm" withSuffix>
{amountBigInt}
</Balance>{" "}
{t("tip_modal.tip_amount_sent")}
</p>

<div className="flex justify-end">
<Button variant="primary" onClick={() => dismiss()}>
{t.common("ok")}
</Button>
</div>
</div>
)
}

return (
<div className="flex w-[80vw] max-w-[350px] flex-col gap-3">
<div className="relative flex w-full grow flex-col items-center gap-3">
<div className="mt-4 text-xl font-bold">🚀 Boost Feed</div>

<small className="center mt-1 gap-1 text-theme-vibrancyFg">
Boost feed to get more privilege, everyone subscribed to this feed will thank you.
</small>

<BoostProgress {...boostStatus} />
<RadioCards
monthlyBoostCost={boostStatus.monthlyBoostCost}
value={amount}
onValueChange={setAmount}
/>
</div>

<Button
disabled={boostFeedMutation.isSuccess || boostFeedMutation.isPending || wrongNumberRange}
isLoading={boostFeedMutation.isPending}
variant={boostFeedMutation.isSuccess ? "outline" : "primary"}
onClick={handleBoost}
>
{boostFeedMutation.isSuccess && (
<i className="i-mgc-check-circle-filled mr-2 bg-green-500" />
)}
Boost
</Button>
</div>
)
}

const BoostProgress = ({
level,
boostCount,
remainingBoostsToLevelUp,
}: {
level: number
boostCount: number
remainingBoostsToLevelUp: number
}) => {
const percentage = (boostCount / (boostCount + remainingBoostsToLevelUp)) * 100
const nextLevel = level + 1
return (
<div className="flex w-full flex-col px-2">
<div className="relative w-full pt-12">
<span
className="absolute bottom-0 mb-10 flex h-8 w-12 -translate-x-1/2 items-center justify-center whitespace-nowrap rounded-full bg-white px-3.5 py-2 text-sm font-bold text-gray-800 shadow-[0px_12px_30px_0px_rgba(16,24,40,0.1)] transition-all duration-500 ease-out after:absolute after:bottom-[-5px] after:left-1/2 after:-z-10 after:flex after:size-3 after:-translate-x-1/2 after:rotate-45 after:bg-white"
style={{ left: `${percentage}%` }}
>
⚡️ {boostCount}
</span>
<div className="relative flex h-6 w-full overflow-hidden rounded-3xl bg-gray-100 dark:bg-gray-800">
<div
role="progressbar"
aria-valuenow={boostCount}
aria-valuemin={0}
aria-valuemax={remainingBoostsToLevelUp}
style={{ width: `${percentage}%` }}
className="flex h-full items-center justify-center rounded-3xl bg-accent text-white transition-all duration-500 ease-out"
/>
</div>
</div>

<div className="mt-2 flex items-center justify-between">
<span className="text-lg font-bold text-accent">Lv. {level}</span>
<span className="text-lg font-bold text-accent">Lv. {nextLevel}</span>
</div>
<small className="center mt-1 gap-1">
{remainingBoostsToLevelUp} more boost will unlock the next level of privilege.
</small>
</div>
)
}
54 changes: 54 additions & 0 deletions apps/renderer/src/modules/boost/radio-cards.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { RadioGroup } from "~/components/ui/radio-group"
import { RadioCard } from "~/components/ui/radio-group/RadioCard"

const radios = [
{
name: "1 Month",
value: 1,
},
{
name: "3 Months",
value: 3,
},
{
name: "6 Months",
value: 6,
},
{
name: "1 Year",
value: 12,
},
]

export const RadioCards = ({
monthlyBoostCost,
value,
onValueChange,
}: {
monthlyBoostCost: number
value: number
onValueChange: (value: number) => void
}) => {
return (
<RadioGroup value={value.toString()} onValueChange={(value) => onValueChange(+value)}>
<div className="grid w-full grid-cols-2 gap-2">
{radios.map((item) => (
<RadioCard
key={item.name}
wrapperClassName="justify-center"
value={(item.value * monthlyBoostCost).toString()}
label={
<div>
<h3 className="pr-3 font-medium leading-none">{item.name}</h3>
<p className="mt-1 flex items-center justify-center gap-1 text-sm text-theme-vibrancyFg">
{item.value * monthlyBoostCost}
<i className="i-mgc-power text-accent" />
</p>
</div>
}
/>
))}
</div>
</RadioGroup>
)
}
36 changes: 36 additions & 0 deletions apps/renderer/src/queries/boosts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useMutation } from "@tanstack/react-query"
import { toast } from "sonner"

import { apiClient } from "~/lib/api-fetch"
import { defineQuery } from "~/lib/defineQuery"
import { toastFetchError } from "~/lib/error-parser"

export const boosts = {
getStatus: ({ feedId }: { feedId: string }) =>
defineQuery(["boostFeed", feedId], async () => {
const res = await apiClient.boosts.$get({
query: {
feedId,
},
})
return res.data
}),
}

export const useBoostFeedMutation = () =>
useMutation({
mutationKey: ["boostFeed"],
mutationFn: (data: Parameters<typeof apiClient.boosts.$post>[0]["json"]) =>
apiClient.boosts.$post({ json: data }),
onError(err) {
toastFetchError(err)
},
onSuccess(_, variables) {
boosts.getStatus({ feedId: variables.feedId }).invalidate()
window.analytics?.capture("boost_sent", {
amount: variables.amount,
feedId: variables.feedId,
})
toast("🎉 Boosted.")
},
})
Loading