feat: like posts
This commit is contained in:
parent
34105452f9
commit
3da5ada3e3
|
|
@ -41,6 +41,7 @@
|
|||
"busboy": "^1.6.0",
|
||||
"clsx": "^1.1.1",
|
||||
"cookie": "^0.5.0",
|
||||
"crossbell.js": "^0.10.13",
|
||||
"dayjs": "^1.11.1",
|
||||
"dotenv": "^16.0.0",
|
||||
"ethers": "^5.6.9",
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ specifiers:
|
|||
clsx: ^1.1.1
|
||||
cookie: ^0.5.0
|
||||
cross-spawn: ^7.0.3
|
||||
crossbell.js: ^0.10.13
|
||||
dayjs: ^1.11.1
|
||||
dotenv: ^16.0.0
|
||||
esbuild: ^0.14.39
|
||||
|
|
@ -123,6 +124,7 @@ dependencies:
|
|||
busboy: 1.6.0
|
||||
clsx: 1.1.1
|
||||
cookie: 0.5.0
|
||||
crossbell.js: 0.10.13
|
||||
dayjs: 1.11.3
|
||||
dotenv: 16.0.1
|
||||
ethers: 5.6.9
|
||||
|
|
@ -3144,6 +3146,23 @@ packages:
|
|||
which: 2.0.2
|
||||
dev: true
|
||||
|
||||
/crossbell.js/0.10.13:
|
||||
resolution: {integrity: sha512-D+uc+oP6P4sr1MOTFCZDjjrNzMEYCUytGSVAo6D0TcCiVVPqNoaaAJhzGILosmXUHlLZxbqam9zk/zfEJ0PrHg==}
|
||||
dependencies:
|
||||
async-retry: 1.3.3
|
||||
ethers: 5.6.9
|
||||
fetch-undici: 3.0.1
|
||||
is-ipfs: 6.0.2
|
||||
query-string: 7.1.1
|
||||
ts-mixer: 6.0.1
|
||||
web3-providers-ws: 1.7.4
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- node-fetch
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
dev: false
|
||||
|
||||
/crossbell.js/0.10.2:
|
||||
resolution: {integrity: sha512-FtoxTeinXcFjLbB5ya/NwyJX3d0VNbeOlj0fCNCpdiOf7+bibQWMkjdhnKpIieDYqt6i2dNfMbeygxhIHLWZkw==}
|
||||
dependencies:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
import React from "react"
|
||||
|
||||
export const CollectIcon: React.FC<React.ComponentPropsWithoutRef<"svg">> = (
|
||||
props,
|
||||
) => {
|
||||
return (
|
||||
<svg
|
||||
width="1em"
|
||||
height="1em"
|
||||
{...props}
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M877.632 456.8c14.976-14.72 20.384-32.96 14.816-49.984-5.536-17.024-20.608-28.544-41.344-31.584l-190.24-27.84c-6.976-1.024-18.464-9.472-21.6-15.904l-85.12-173.696c-9.28-18.944-24.896-29.76-42.88-29.76-17.952 0-33.6 10.816-42.816 29.76l-85.12 173.696c-3.104 6.432-14.592 14.848-21.6 15.904l-190.24 27.84c-20.704 3.04-35.776 14.56-41.344 31.584-5.568 17.024-0.16 35.232 14.816 49.984l137.696 135.232c5.088 4.992 9.536 18.816 8.32 25.92l-32.48 190.912c-3.552 20.832 2.752 38.816 17.344 49.344 7.52 5.44 16.224 8.16 25.472 8.16 8.576 0 17.6-2.336 26.56-7.04l170.176-90.176c6.048-3.2 20.448-3.2 26.528 0l170.144 90.112c18.528 9.856 37.504 9.44 52.064-1.056 14.56-10.528 20.864-28.48 17.344-49.28l-32.48-190.976c-1.28-7.104 3.2-20.928 8.32-25.92l137.664-135.232z"></path>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import React from "react"
|
||||
|
||||
export const LikeIcon: React.FC<React.ComponentPropsWithoutRef<"svg">> = (
|
||||
props,
|
||||
) => {
|
||||
return (
|
||||
<svg
|
||||
width="1em"
|
||||
height="1em"
|
||||
{...props}
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M780.8 204.8c-83.2-44.8-179.2-19.2-243.2 44.8L512 275.2 486.4 249.6c-64-64-166.4-83.2-243.2-44.8C108.8 275.2 89.6 441.6 185.6 537.6l32 32 153.6 153.6 102.4 102.4c25.6 25.6 57.6 25.6 83.2 0l102.4-102.4 153.6-153.6 32-32C934.4 441.6 915.2 275.2 780.8 204.8z"
|
||||
p-id="2929"
|
||||
></path>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
import { Note } from "~/lib/types"
|
||||
import { LikeIcon } from "~/components/icons/LikeIcon"
|
||||
import { CollectIcon } from "~/components/icons/CollectIcon"
|
||||
import {
|
||||
useLikePage,
|
||||
useUnlikePage,
|
||||
useGetLikes,
|
||||
useCheckLike,
|
||||
} from "~/queries/page"
|
||||
import { useAccount } from "wagmi"
|
||||
import { useConnectModal } from "@rainbow-me/rainbowkit"
|
||||
import { useState, useEffect } from "react"
|
||||
import { Button } from "../ui/Button"
|
||||
|
||||
export const PostFooter: React.FC<{
|
||||
page?: Note
|
||||
}> = ({ page }) => {
|
||||
const likePage = useLikePage()
|
||||
const unlikePage = useUnlikePage()
|
||||
const { address } = useAccount()
|
||||
const { openConnectModal } = useConnectModal()
|
||||
const [likeProgress, setLikeProgress] = useState(false)
|
||||
|
||||
const like = async () => {
|
||||
if (!address) {
|
||||
setLikeProgress(true)
|
||||
openConnectModal?.()
|
||||
} else if (page?.id) {
|
||||
if (isLike.data?.count) {
|
||||
unlikePage.mutate({
|
||||
address,
|
||||
pageId: page?.id,
|
||||
})
|
||||
} else {
|
||||
likePage.mutate({
|
||||
address,
|
||||
pageId: page?.id,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const likes = useGetLikes({
|
||||
pageId: page?.id,
|
||||
})
|
||||
const isLike = useCheckLike({
|
||||
address,
|
||||
pageId: page?.id,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (likeProgress && address && isLike.isSuccess && page?.id) {
|
||||
if (!isLike.data.count) {
|
||||
likePage.mutate({
|
||||
address,
|
||||
pageId: page?.id,
|
||||
})
|
||||
}
|
||||
setLikeProgress(false)
|
||||
}
|
||||
}, [
|
||||
likeProgress,
|
||||
address,
|
||||
isLike.isSuccess,
|
||||
isLike.data?.count,
|
||||
likePage,
|
||||
page?.id,
|
||||
])
|
||||
|
||||
return (
|
||||
<div className="flex fill-gray-400 text-gray-500 mt-10">
|
||||
<Button
|
||||
variant="text"
|
||||
className="flex items-center mr-10"
|
||||
isAutoWidth={true}
|
||||
onClick={like}
|
||||
isLoading={likePage.isLoading || unlikePage.isLoading || likeProgress}
|
||||
style={
|
||||
isLike.isSuccess && isLike.data.count
|
||||
? {
|
||||
color: "#f91880",
|
||||
fill: "#f91880",
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<LikeIcon className="mr-2 w-10 h-10" />
|
||||
<span>{likes.data?.count || 0}</span>
|
||||
</Button>
|
||||
<Button variant="text" className="flex items-center" isAutoWidth={true}>
|
||||
<CollectIcon className="mr-2 w-10 h-10" />
|
||||
<span>0</span>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,40 +1,11 @@
|
|||
import { formatDate } from "~/lib/date"
|
||||
import { getUserContentsUrl } from "~/lib/user-contents"
|
||||
import { Avatar } from "../ui/Avatar"
|
||||
|
||||
type Author = {
|
||||
id: string
|
||||
name: string
|
||||
avatar: string | null
|
||||
}
|
||||
|
||||
export const PostAuthors: React.FC<{ authors: Author[] }> = ({ authors }) => {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
{authors.map((author) => {
|
||||
return (
|
||||
<span key={author.id} className="flex items-center space-x-2">
|
||||
<Avatar
|
||||
size={24}
|
||||
images={[getUserContentsUrl(author.avatar)]}
|
||||
name={author.name}
|
||||
/>
|
||||
<span>{author.name}</span>
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const PostMeta: React.FC<{
|
||||
publishedAt: string
|
||||
authors: Author[]
|
||||
}> = ({ authors, publishedAt }) => {
|
||||
}> = ({ publishedAt }) => {
|
||||
return (
|
||||
<div className="text-zinc-400 mt-2 flex items-center space-x-5">
|
||||
<span>{formatDate(publishedAt)}</span>
|
||||
<PostAuthors authors={authors} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,17 +88,13 @@ export const SiteHeader: React.FC<{
|
|||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
followProgress &&
|
||||
address &&
|
||||
subscription.isSuccess &&
|
||||
!subscription.data &&
|
||||
site?.username
|
||||
) {
|
||||
subscribeToSite.mutate({
|
||||
userId: address,
|
||||
siteId: site.username,
|
||||
})
|
||||
if (followProgress && address && subscription.isSuccess && site?.username) {
|
||||
if (!subscription.data) {
|
||||
subscribeToSite.mutate({
|
||||
userId: address,
|
||||
siteId: site.username,
|
||||
})
|
||||
}
|
||||
setFollowProgress(false)
|
||||
}
|
||||
}, [
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type { PageType } from "~/lib/db.server"
|
|||
import { Rendered } from "~/markdown"
|
||||
import { PageContent } from "../common/PageContent"
|
||||
import { PostMeta } from "./PostMeta"
|
||||
import { PostFooter } from "./PostFooter"
|
||||
import { Profile, Note } from "~/lib/types"
|
||||
|
||||
export const SitePage: React.FC<{
|
||||
|
|
@ -17,21 +18,11 @@ export const SitePage: React.FC<{
|
|||
<h2 className="text-xl font-bold page-title">{page?.title}</h2>
|
||||
)}
|
||||
{page?.tags?.includes("post") && (
|
||||
<PostMeta
|
||||
publishedAt={page.date_published}
|
||||
authors={
|
||||
[
|
||||
{
|
||||
id: site?.username || "",
|
||||
name: site?.name || "",
|
||||
avatar: site?.avatars?.[0] || null,
|
||||
},
|
||||
] || []
|
||||
}
|
||||
/>
|
||||
<PostMeta publishedAt={page.date_published} />
|
||||
)}
|
||||
</div>
|
||||
<PageContent contentHTML={page?.body?.content || ""} className="my-10" />
|
||||
<PageContent contentHTML={page?.body?.content || ""} className="mt-10" />
|
||||
<PostFooter page={page} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@
|
|||
@apply mt-0;
|
||||
}
|
||||
|
||||
.prose > *:last-child {
|
||||
@apply mb-0;
|
||||
}
|
||||
|
||||
.prose {
|
||||
@apply text-lg;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,9 +87,9 @@ body {
|
|||
}
|
||||
|
||||
.button.is-text {
|
||||
@apply shadow-none;
|
||||
@apply text-gray-500;
|
||||
@apply hover:bg-gray-50 focus:bg-gray-100;
|
||||
@apply shadow-none p-0;
|
||||
@apply text-gray-500 fill-gray-500;
|
||||
@apply hover:text-gray-600 hover:fill-gray-600;
|
||||
}
|
||||
|
||||
.button.is-green {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
import { Contract, Indexer } from "crossbell.js"
|
||||
|
||||
const indexer = new Indexer()
|
||||
const contract =
|
||||
typeof window !== "undefined"
|
||||
? new Contract((<any>window).ethereum)
|
||||
: undefined
|
||||
if (contract) {
|
||||
contract.connect()
|
||||
}
|
||||
|
||||
export { indexer, contract }
|
||||
|
|
@ -13,6 +13,7 @@ import { PageVisibilityEnum, Notes } from "~/lib/types"
|
|||
import { isUUID } from "~/lib/uuid"
|
||||
import { stripHTML } from "~/lib/utils"
|
||||
import unidata from "~/lib/unidata"
|
||||
import { indexer, contract } from "~/lib/crossbell"
|
||||
|
||||
const checkPageSlug = async ({
|
||||
slug,
|
||||
|
|
@ -93,31 +94,6 @@ export async function createOrUpdatePage(input: {
|
|||
)
|
||||
}
|
||||
|
||||
export async function scheduleEmailForPost(
|
||||
gate: Gate,
|
||||
input: { pageId: string; emailSubject?: string },
|
||||
) {
|
||||
// const page = await getPage(gate, { page: input.pageId })
|
||||
// if (!gate.allows({ type: "can-update-page", siteId: page.siteId })) {
|
||||
// throw gate.permissionError()
|
||||
// }
|
||||
// if (page.emailStatus) {
|
||||
// throw new Error("Email already scheduled or sent")
|
||||
// }
|
||||
// await prismaPrimary.page.update({
|
||||
// where: {
|
||||
// id: page.id,
|
||||
// },
|
||||
// data: {
|
||||
// emailStatus: "PENDING",
|
||||
// emailSubject: input.emailSubject,
|
||||
// },
|
||||
// })
|
||||
// await notifySubscribersForNewPost(gate, {
|
||||
// pageId: page.id,
|
||||
// })
|
||||
}
|
||||
|
||||
export async function getPagesBySite(input: {
|
||||
site?: string
|
||||
type: "post" | "page"
|
||||
|
|
@ -313,69 +289,77 @@ export async function getPage<TRender extends boolean = false>(input: {
|
|||
return page
|
||||
}
|
||||
|
||||
export const notifySubscribersForNewPost = async (
|
||||
gate: Gate,
|
||||
input: {
|
||||
pageId: string
|
||||
},
|
||||
) => {
|
||||
// const page = await getPage(gate, { page: input.pageId, render: true })
|
||||
// const site = await getSite(page.siteId)
|
||||
// if (page.emailStatus !== PageEmailStatus.PENDING) {
|
||||
// return
|
||||
// }
|
||||
// if (!page.published || page.publishedAt > new Date()) {
|
||||
// return
|
||||
// }
|
||||
// if (page.type === "PAGE") {
|
||||
// throw new Error("You can only notify subscribers for post updates")
|
||||
// }
|
||||
// if (!gate.allows({ type: "can-notify-site-subscribers", site })) {
|
||||
// throw gate.permissionError()
|
||||
// }
|
||||
// await prismaPrimary.page.update({
|
||||
// where: {
|
||||
// id: page.id,
|
||||
// },
|
||||
// data: {
|
||||
// emailStatus: PageEmailStatus.RUNNING,
|
||||
// },
|
||||
// })
|
||||
// const memberships = await prismaPrimary.membership.findMany({
|
||||
// where: {
|
||||
// role: MembershipRole.SUBSCRIBER,
|
||||
// siteId: site.id,
|
||||
// },
|
||||
// include: {
|
||||
// user: true,
|
||||
// },
|
||||
// })
|
||||
// const emailSubscribers = memberships
|
||||
// .filter((member) => (member.config as any).email)
|
||||
// .map((member) => member.user)
|
||||
// try {
|
||||
// await sendEmailForNewPost({
|
||||
// post: page,
|
||||
// site,
|
||||
// subscribers: emailSubscribers,
|
||||
// })
|
||||
// await prismaPrimary.page.update({
|
||||
// where: {
|
||||
// id: page.id,
|
||||
// },
|
||||
// data: {
|
||||
// emailStatus: PageEmailStatus.SUCCESS,
|
||||
// },
|
||||
// })
|
||||
// } catch (error) {
|
||||
// await prismaPrimary.page.update({
|
||||
// where: {
|
||||
// id: page.id,
|
||||
// },
|
||||
// data: {
|
||||
// emailStatus: PageEmailStatus.FAILED,
|
||||
// },
|
||||
// })
|
||||
// console.error("failed to send email", error)
|
||||
// }
|
||||
async function getPrimaryCharacter(address: string) {
|
||||
const character = await indexer.getPrimaryCharacter(address)
|
||||
return character?.characterId
|
||||
}
|
||||
|
||||
export async function likePage({
|
||||
address,
|
||||
pageId,
|
||||
}: {
|
||||
address: string
|
||||
pageId: string
|
||||
}) {
|
||||
const characterId = await getPrimaryCharacter(address)
|
||||
if (!characterId) {
|
||||
throw notFound(`character not found`)
|
||||
} else {
|
||||
return contract?.linkNote(
|
||||
characterId,
|
||||
pageId.split("-")[0],
|
||||
pageId.split("-")[1],
|
||||
"like",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function unlikePage({
|
||||
address,
|
||||
pageId,
|
||||
}: {
|
||||
address: string
|
||||
pageId: string
|
||||
}) {
|
||||
const characterId = await getPrimaryCharacter(address)
|
||||
if (!characterId) {
|
||||
throw notFound(`character not found`)
|
||||
} else {
|
||||
return contract?.unlinkNote(
|
||||
characterId,
|
||||
pageId.split("-")[0],
|
||||
pageId.split("-")[1],
|
||||
"like",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getLikes({ pageId }: { pageId: string }) {
|
||||
return indexer.getBacklinksOfNote(
|
||||
pageId.split("-")[0],
|
||||
pageId.split("-")[1],
|
||||
{
|
||||
linkType: "like",
|
||||
limit: 0,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export async function checkLike({
|
||||
address,
|
||||
pageId,
|
||||
}: {
|
||||
address: string
|
||||
pageId: string
|
||||
}) {
|
||||
const characterId = await getPrimaryCharacter(address)
|
||||
if (!characterId) {
|
||||
throw notFound(`character not found`)
|
||||
} else {
|
||||
return indexer.getLinks(characterId, {
|
||||
linkType: "like",
|
||||
toCharacterId: pageId.split("-")[0],
|
||||
toNoteId: pageId.split("-")[1],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,35 @@ export const useGetPage = (input: Parameters<typeof pageModel.getPage>[0]) => {
|
|||
})
|
||||
}
|
||||
|
||||
export const useGetLikes = (input: { pageId?: string }) => {
|
||||
return useQuery(["getLikes", input.pageId], async () => {
|
||||
if (!input.pageId) {
|
||||
return {
|
||||
count: 0,
|
||||
list: [],
|
||||
}
|
||||
}
|
||||
return pageModel.getLikes({
|
||||
pageId: input.pageId,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const useCheckLike = (input: { address?: string; pageId?: string }) => {
|
||||
return useQuery(["checkLike", input.pageId, input.address], async () => {
|
||||
if (!input.pageId || !input.address) {
|
||||
return {
|
||||
count: 0,
|
||||
list: [],
|
||||
}
|
||||
}
|
||||
return pageModel.checkLike({
|
||||
address: input.address,
|
||||
pageId: input.pageId,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function useCreateOrUpdatePage() {
|
||||
const queryClient = useQueryClient()
|
||||
const mutation = useMutation(
|
||||
|
|
@ -46,3 +75,41 @@ export function useDeletePage() {
|
|||
},
|
||||
)
|
||||
}
|
||||
|
||||
export function useLikePage() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation(
|
||||
async (input: Parameters<typeof pageModel.likePage>[0]) => {
|
||||
return pageModel.likePage(input)
|
||||
},
|
||||
{
|
||||
onSuccess: (data, variables) => {
|
||||
queryClient.invalidateQueries([
|
||||
"checkLike",
|
||||
variables.pageId,
|
||||
variables.address,
|
||||
])
|
||||
queryClient.invalidateQueries(["getLikes", variables.pageId])
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export function useUnlikePage() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation(
|
||||
async (input: Parameters<typeof pageModel.unlikePage>[0]) => {
|
||||
return pageModel.unlikePage(input)
|
||||
},
|
||||
{
|
||||
onSuccess: (data, variables) => {
|
||||
queryClient.invalidateQueries([
|
||||
"checkLike",
|
||||
variables.pageId,
|
||||
variables.address,
|
||||
])
|
||||
queryClient.invalidateQueries(["getLikes", variables.pageId])
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue