From e6a45a1857a96e00b960f80ee82c6c21dc934b24 Mon Sep 17 00:00:00 2001 From: shixuewen Date: Wed, 21 Feb 2024 09:06:08 +0800 Subject: [PATCH] feat: cast super like & cast layout --- .../components/shared/button/SaveButton.tsx | 20 +- apps/u3/src/components/social/PostCard.tsx | 186 ++++++++++-- .../src/components/social/farcaster/FCast.tsx | 264 ++++++++++++++++-- .../components/social/farcaster/FCastLike.tsx | 120 +------- .../social/farcaster/FCastRecast.tsx | 130 +-------- .../social/farcaster/FCastSuperLike.tsx | 97 +++++++ .../social/farcaster/FarcasterChannel.tsx | 25 +- .../src/container/community/CommonStyles.tsx | 62 ++++ .../community/FarcasterPostDetail.tsx | 1 + .../src/container/community/PostsFcNewest.tsx | 4 +- .../container/community/PostsFcTrending.tsx | 4 +- apps/u3/src/container/social/CommonStyles.tsx | 17 ++ apps/u3/src/hooks/shared/useLinkId.ts | 43 +++ .../farcaster/useFarcasterLikeAction.ts | 158 +++++++++++ .../farcaster/useFarcasterRecastAction.ts | 155 ++++++++++ 15 files changed, 1013 insertions(+), 273 deletions(-) create mode 100644 apps/u3/src/components/social/farcaster/FCastSuperLike.tsx create mode 100644 apps/u3/src/container/community/CommonStyles.tsx create mode 100644 apps/u3/src/hooks/shared/useLinkId.ts create mode 100644 apps/u3/src/hooks/social/farcaster/useFarcasterLikeAction.ts create mode 100644 apps/u3/src/hooks/social/farcaster/useFarcasterRecastAction.ts diff --git a/apps/u3/src/components/shared/button/SaveButton.tsx b/apps/u3/src/components/shared/button/SaveButton.tsx index 3c98b705..1bd6619e 100644 --- a/apps/u3/src/components/shared/button/SaveButton.tsx +++ b/apps/u3/src/components/shared/button/SaveButton.tsx @@ -8,17 +8,25 @@ */ import { FavorButton, FavorButtonProps } from '@us3r-network/link'; import { StarIcon, StarFilledIcon } from '@radix-ui/react-icons'; +import { Link } from '@us3r-network/data-model'; +export const formatLink = (link: Link): Link => { + let url = link?.url || ''; + if (url) { + url = url.replace('?', '%3F'); + // todo: 临时解决方案,后续需要在link model里面去 + if (url.length > 100) { + url = url.slice(0, 100); + } + } + return { ...link, url }; +}; export function SaveButton({ ...props }: FavorButtonProps) { if (props.link?.url) { - props.link.url = props.link.url.replace('?', '%3F'); - // todo: 临时解决方案,后续需要在link model里面去掉这个长度限制 - if (props.link.url.length > 100) { - props.link.url = props.link.url.slice(0, 100); - } + props.link = formatLink(props.link); } return ( - + {({ isFavoring, isFavored, favorsCount }) => { return (
diff --git a/apps/u3/src/components/social/PostCard.tsx b/apps/u3/src/components/social/PostCard.tsx index b8fee397..c8d032ed 100644 --- a/apps/u3/src/components/social/PostCard.tsx +++ b/apps/u3/src/components/social/PostCard.tsx @@ -192,20 +192,20 @@ export const PostShareMenuBtn = styled(MultiPlatformShareMenuBtn)` } `; -// export const PostCardWrapper = styled.div<{ isDetail?: boolean }>` -// background: #212228; -// padding: 20px; -// box-sizing: border-box; -// display: flex; -// flex-direction: column; -// gap: 10px; -// cursor: ${(props) => (props.isDetail ? 'initial' : 'pointer')}; -// &:hover { -// background: ${(props) => (props.isDetail ? '#212228' : '#352525')}; -// } -// `; +export const PostCardWrapper = styled.div<{ isDetail?: boolean }>` + background: #212228; + padding: 20px; + box-sizing: border-box; + display: flex; + flex-direction: column; + gap: 10px; + cursor: ${(props) => (props.isDetail ? 'initial' : 'pointer')}; + &:hover { + background: ${(props) => (props.isDetail ? '#212228' : '#000000')}; + } +`; -export function PostCardWrapper({ +export function PostCardWrapperV2({ isDetail, className, ...props @@ -215,26 +215,53 @@ export function PostCardWrapper({ return (
+ ); +} +export function PostCardMainWrapper({ + className, + ...props +}: ComponentPropsWithRef<'div'> & { + isDetail?: boolean; +}) { + return ( +
); } - export const PostCardHeaderWrapper = styled.div` display: flex; justify-content: space-between; gap: 10px; `; -export const PostCardFooterWrapper = styled.div` - display: flex; - justify-content: space-between; - align-items: center; - gap: 10px; -`; +// export const PostCardFooterWrapper = styled.div` +// display: flex; +// justify-content: space-between; +// align-items: center; +// gap: 10px; +// `; + +export function PostCardFooterWrapper({ + className, + ...props +}: ComponentPropsWithRef<'div'>) { + return ( +
+ ); +} export type PostCardUserInfoData = { platform: SocialPlatform; @@ -294,6 +321,121 @@ export function PostCardUserInfo({ ); } +export function PostCardUserInfoV2({ + data, + className, + ...wrapperProps +}: ComponentPropsWithRef<'div'> & { + data: PostCardUserInfoData; +}) { + const { handle, platform } = data; + const profileIdentity = useMemo(() => { + if (handle.endsWith('.eth')) return handle; + switch (platform) { + case SocialPlatform.Lens: + return lensHandleToBioLinkHandle(handle); + case SocialPlatform.Farcaster: + return farcasterHandleToBioLinkHandle(handle); + default: + return ''; + } + }, [handle, platform]); + + return ( +
+ + +
{data.name}
+
+
+ @{data.handle} · {dayjs(data.createdAt).fromNow()} +
+
+ ); +} + +export function PostCardPlatformInfo({ + platform, + className, + ...wrapperProps +}: ComponentPropsWithRef<'div'> & { + platform: SocialPlatform; +}) { + let icon = null; + let name = ''; + switch (platform) { + case SocialPlatform.Lens: + icon = ( + + + + + + + + + + + ); + name = 'Lens'; + break; + case SocialPlatform.Farcaster: + icon = ( + + + + + + ); + name = 'Farcaster'; + break; + default: + break; + } + return ( +
+ {icon} +
{name}
+
+ ); +} + const PostCardUserInfoWrapper = styled.div` /* flex: 1; */ display: flex; diff --git a/apps/u3/src/components/social/farcaster/FCast.tsx b/apps/u3/src/components/social/farcaster/FCast.tsx index bcb1a215..00345279 100644 --- a/apps/u3/src/components/social/farcaster/FCast.tsx +++ b/apps/u3/src/components/social/farcaster/FCast.tsx @@ -29,9 +29,13 @@ import { PostCardActionsWrapper, PostCardContentWrapper, PostCardFooterWrapper, + PostCardMainWrapper, + PostCardPlatformInfo, PostCardShowMoreWrapper, PostCardUserInfo, + PostCardUserInfoV2, PostCardWrapper, + PostCardWrapperV2, PostShareMenuBtn, } from '../PostCard'; import Embed from '../Embed'; @@ -44,9 +48,11 @@ import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; import { pinupCastApi } from '@/services/social/api/farcaster'; import useLogin from '@/hooks/shared/useLogin'; -import { SaveButton } from '@/components/shared/button/SaveButton'; +import { SaveButton, formatLink } from '@/components/shared/button/SaveButton'; import FCastTips from './FCastTips'; import FCastTipDetail from './FCastTipDetail'; +import FCastSuperLike from './FCastSuperLike'; +import { useLinkId } from '@/hooks/shared/useLinkId'; export default function FCast({ cast, @@ -82,7 +88,8 @@ export default function FCast({ }); const [count, setCount] = useState(0); const [showMore, setShowMore] = useState(false); - const { following, pinupHashes, updatePinupHashes } = useFarcasterCtx(); + const { following, pinupHashes, updatePinupHashes, currFid } = + useFarcasterCtx(); const { followAction, unfollowAction, isPending, isFollowing } = useFarcasterFollowAction(); const { isAdmin } = useLogin(); @@ -148,16 +155,241 @@ export default function FCast({ const [linkParam, setLinkParam] = useState(null); useEffect(() => { - if (isDetail) - setLinkParam({ - url: getOfficialCastUrl( - userData.userName, - Buffer.from(castId.hash).toString('hex') - ), - type: 'link', - title: cast.text.slice(0, 200), // todo: expand this limit at model - }); - }, [castId.hash, isDetail]); + setLinkParam({ + url: getOfficialCastUrl( + userData.userName, + Buffer.from(castId.hash).toString('hex') + ), + type: 'link', + title: cast.text.slice(0, 200), // todo: expand this limit at model + }); + }, [castId.hash]); + + const [updatedCast, setUpdatedCast] = useState(cast); + const changeCastLikesWithCurrFid = (liked: boolean) => { + let likes = cast.likes || []; + let likesCount = Number(cast.likesCount) || 0; + const fid = String(currFid); + if (liked) { + likes.push(fid); + likesCount += 1; + } else { + likes = likes.filter((id) => id !== fid); + likesCount -= 1; + } + setUpdatedCast({ + ...cast, + likes, + likesCount: String(likesCount), + }); + }; + const changeCastRecastsWithCurrFid = (recasted: boolean) => { + let recasts = cast.recasts || []; + let recastsCount = Number(cast.recastsCount) || 0; + const fid = String(currFid); + if (recasted) { + recasts.push(fid); + recastsCount += 1; + } else { + recasts = recasts.filter((id) => id !== fid); + recastsCount -= 1; + } + setUpdatedCast({ + ...cast, + recasts, + recastsCount: String(recastsCount), + }); + }; + const formatLinkParam = formatLink(linkParam); + const { getLinkId, linkId, setLinkId } = useLinkId(formatLinkParam); + + /** + * 注:这里是区分v2版本布局,在这里兼容v2是为了保证功能一致改动时方便 + * //TODO 等正式使用v2版本后,删除这个判断,然后删除下面旧的布局 + */ + if (isCommunityLayout) { + return ( + { + if (isDetail) return; + const id = Buffer.from(castId.hash).toString('hex'); + + cardClickAction?.(e); + navigate(`/social/post-detail/fcast/${id}`); + }} + {...wrapperProps} + > +
+ { + setLinkId(newLinkId); + }} + onLikeSuccess={() => { + changeCastLikesWithCurrFid(true); + }} + onRecastSuccess={() => { + changeCastRecastsWithCurrFid(true); + }} + /> +
$5.00
+
+ +
+ + {(cast.parent_url || cast.rootParentUrl) && ( + + )} + +
+
{ + e.stopPropagation(); + }} + > + { + setCount(count + 1); + }} + /> + {isAdmin && ( + + )} +
+
+ + + + + {showMore && ( + + + + )} + {!simpleLayout && ( + + )} + + + { + e.stopPropagation(); + }} + > + { + e.stopPropagation(); + }} + > + { + if (!linkId && linkParam?.url && linkParam?.type) { + getLinkId(linkParam).then((id) => { + if (id) setLinkId(id); + else setLinkId(''); + }); + } + }} + /> + + +
+ { + changeCastLikesWithCurrFid(true); + }} + onRemoveLikeSuccess={() => { + changeCastLikesWithCurrFid(false); + }} + /> + + { + changeCastRecastsWithCurrFid(true); + }} + onRemoveRecastSuccess={() => { + changeCastRecastsWithCurrFid(false); + }} + /> + + + + + ); + } return (
diff --git a/apps/u3/src/components/social/farcaster/FCastLike.tsx b/apps/u3/src/components/social/farcaster/FCastLike.tsx index f97edf83..83b0deb3 100644 --- a/apps/u3/src/components/social/farcaster/FCastLike.tsx +++ b/apps/u3/src/components/social/farcaster/FCastLike.tsx @@ -1,18 +1,7 @@ /* eslint-disable @typescript-eslint/no-shadow */ -import { - CastId, - ReactionType, - makeReactionAdd, - makeReactionRemove, -} from '@farcaster/hub-web'; -import { useCallback, useState } from 'react'; -import { toast } from 'react-toastify'; +import { CastId } from '@farcaster/hub-web'; import { UserData } from 'src/utils/social/farcaster/user-data'; -import { - FARCASTER_NETWORK, - FARCASTER_WEB_CLIENT, -} from '../../../constants/farcaster'; import useFarcasterUserData from '../../../hooks/social/farcaster/useFarcasterUserData'; import useFarcasterCastId from '../../../hooks/social/farcaster/useFarcasterCastId'; // import { getCurrFid } from '../../../utils/farsign-utils'; @@ -25,111 +14,28 @@ import PostLike, { import { FarCast } from '../../../services/social/types'; import { useFarcasterCtx } from '../../../contexts/social/FarcasterCtx'; import useLogin from '../../../hooks/shared/useLogin'; +import useFarcasterLikeAction from '@/hooks/social/farcaster/useFarcasterLikeAction'; export default function FCastLike({ cast, farcasterUserData, farcasterUserDataObj, openFarcasterQR, + onLikeSuccess, + onRemoveLikeSuccess, }: { cast: FarCast; farcasterUserData: { [key: string]: { type: number; value: string }[] }; farcasterUserDataObj?: { [key: string]: UserData } | undefined; openFarcasterQR: () => void; + onLikeSuccess?: () => void; + onRemoveLikeSuccess?: () => void; }) { const { isLogin: isLoginU3, login: loginU3 } = useLogin(); - const { encryptedSigner, isConnected, currFid } = useFarcasterCtx(); - const [likes, setLikes] = useState(Array.from(new Set(cast.likes))); - const [likeCount, setLikeCount] = useState( - Number(cast.like_count || cast.likesCount || 0) - ); - - const likeCast = useCallback( - async (castId: CastId) => { - if (!isConnected) { - openFarcasterQR(); - return; - } - if (!encryptedSigner) { - console.error('no encryptedSigner'); - return; - } - try { - const cast = await makeReactionAdd( - { - type: ReactionType.LIKE, - targetCastId: castId, - }, - { - fid: currFid, - network: FARCASTER_NETWORK, - }, - encryptedSigner - ); - if (cast.isErr()) { - throw new Error(cast.error.message); - } - - const result = await FARCASTER_WEB_CLIENT.submitMessage(cast.value); - if (result.isErr()) { - throw new Error(result.error.message); - } - - const tmpSet = new Set(likes); - tmpSet.add(`${currFid}`); - setLikes(Array.from(tmpSet)); - setLikeCount(likeCount + 1); - - toast.success('like post created'); - } catch (error) { - console.error(error); - toast.error('error like'); - } - }, - [encryptedSigner, isConnected, likeCount, likes, openFarcasterQR, currFid] - ); + const { isConnected } = useFarcasterCtx(); - const removeLikeCast = useCallback( - async (castId: CastId) => { - if (!isConnected) { - openFarcasterQR(); - return; - } - if (!encryptedSigner) return; - // const currFid = getCurrFid(); - try { - const cast = await makeReactionRemove( - { - type: ReactionType.LIKE, - targetCastId: castId, - }, - { - fid: currFid, - network: FARCASTER_NETWORK, - }, - encryptedSigner - ); - if (cast.isErr()) { - throw new Error(cast.error.message); - } - - const result = await FARCASTER_WEB_CLIENT.submitMessage(cast.value); - if (result.isErr()) { - throw new Error(result.error.message); - } - - const tmpSet = new Set(likes); - tmpSet.delete(`${currFid}`); - setLikes(Array.from(tmpSet)); - setLikeCount(likeCount - 1); - - toast.success('like post removed'); - } catch (error) { - toast.error('error like'); - } - }, - [encryptedSigner, isConnected, likeCount, likes, openFarcasterQR, currFid] - ); + const { likes, likeCount, likeCast, removeLikeCast, liked } = + useFarcasterLikeAction({ cast, onLikeSuccess, onRemoveLikeSuccess }); const castId: CastId = useFarcasterCastId({ cast }); @@ -154,13 +60,17 @@ export default function FCastLike({ )} { if (!isLoginU3) { loginU3(); return; } - if (likes.includes(`${currFid}`)) { + if (!isConnected) { + openFarcasterQR(); + return; + } + if (liked) { removeLikeCast(castId); } else { likeCast(castId); diff --git a/apps/u3/src/components/social/farcaster/FCastRecast.tsx b/apps/u3/src/components/social/farcaster/FCastRecast.tsx index eadeaa84..4c7ff0a8 100644 --- a/apps/u3/src/components/social/farcaster/FCastRecast.tsx +++ b/apps/u3/src/components/social/farcaster/FCastRecast.tsx @@ -1,13 +1,6 @@ /* eslint-disable no-underscore-dangle */ /* eslint-disable @typescript-eslint/no-shadow */ -import { - CastAddBody, - CastId, - ReactionType, - makeCastAdd, - makeReactionAdd, - makeReactionRemove, -} from '@farcaster/hub-web'; +import { CastAddBody, CastId, makeCastAdd } from '@farcaster/hub-web'; import { Channelv1 } from '@mod-protocol/farcaster'; import { useCallback, useRef, useState } from 'react'; import { toast } from 'react-toastify'; @@ -41,122 +34,25 @@ import FarcasterInput from './FarcasterInput'; import { FCastChannelPicker } from './FCastChannelPicker'; import { Button } from '@/components/ui/button'; import ModalContainer from '@/components/common/modal/ModalContainer'; +import useFarcasterRecastAction from '@/hooks/social/farcaster/useFarcasterRecastAction'; export default function FCastRecast({ cast, openFarcasterQR, farcasterUserDataObj, + onRecastSuccess, + onRemoveRecastSuccess, }: { cast: FarCast; farcasterUserDataObj: { [key: string]: UserData } | undefined; openFarcasterQR: () => void; + onRecastSuccess?: () => void; + onRemoveRecastSuccess?: () => void; }) { const { isLogin: isLoginU3, login: loginU3 } = useLogin(); - const { encryptedSigner, isConnected, currFid } = useFarcasterCtx(); - const [recasts, setRecasts] = useState( - Array.from(new Set(cast.recasts)) - ); - const [recastCount, setRecastCount] = useState( - Number(cast.recast_count || cast.recastsCount || 0) - ); - - const recast = useCallback( - async (castId: CastId) => { - if (!isConnected) { - openFarcasterQR(); - return; - } - if (!encryptedSigner) return; - try { - const cast = await makeReactionAdd( - { - type: ReactionType.RECAST, - targetCastId: castId, - }, - { - fid: currFid, - network: FARCASTER_NETWORK, - }, - encryptedSigner - ); - if (cast.isErr()) { - throw new Error(cast.error.message); - } - - const result = await FARCASTER_WEB_CLIENT.submitMessage(cast.value); - if (result.isErr()) { - throw new Error(result.error.message); - } - - const tmpSet = new Set(recasts); - tmpSet.add(`${currFid}`); - setRecasts(Array.from(tmpSet)); - setRecastCount(recastCount + 1); - - toast.success('recast created'); - } catch (error) { - toast.error('error recast'); - } - }, - [ - encryptedSigner, - isConnected, - openFarcasterQR, - recastCount, - recasts, - currFid, - ] - ); - - const removeRecast = useCallback( - async (castId: CastId) => { - if (!currFid) return; - if (!isConnected) { - openFarcasterQR(); - return; - } - if (!encryptedSigner) return; - // const currFid = getCurrFid(); - try { - const cast = await makeReactionRemove( - { - type: ReactionType.RECAST, - targetCastId: castId, - }, - { - fid: currFid, - network: FARCASTER_NETWORK, - }, - encryptedSigner - ); - if (cast.isErr()) { - throw new Error(cast.error.message); - } - - const result = await FARCASTER_WEB_CLIENT.submitMessage(cast.value); - if (result.isErr()) { - throw new Error(result.error.message); - } - - const tmpSet = new Set(recasts); - tmpSet.delete(`${currFid}`); - setRecasts(Array.from(tmpSet)); - setRecastCount(recastCount - 1); - - toast.success('removed recast'); - } catch (error) { - toast.error('error recast'); - } - }, - [ - encryptedSigner, - isConnected, - openFarcasterQR, - recastCount, - recasts, - currFid, - ] - ); + const { isConnected } = useFarcasterCtx(); + const { recast, removeRecast, recastCount, recasted } = + useFarcasterRecastAction({ cast, onRecastSuccess, onRemoveRecastSuccess }); // const currFid: string = useFarcasterCurrFid(); const castId: CastId = useFarcasterCastId({ cast }); @@ -165,14 +61,18 @@ export default function FCastRecast({ { if (!isLoginU3) { loginU3(); return; } - if (recasts.includes(`${currFid}`)) { + if (!isConnected) { + openFarcasterQR(); + return; + } + if (recasted) { removeRecast(castId); } else { recast(castId); diff --git a/apps/u3/src/components/social/farcaster/FCastSuperLike.tsx b/apps/u3/src/components/social/farcaster/FCastSuperLike.tsx new file mode 100644 index 00000000..bb2535a2 --- /dev/null +++ b/apps/u3/src/components/social/farcaster/FCastSuperLike.tsx @@ -0,0 +1,97 @@ +/* eslint-disable @typescript-eslint/no-shadow */ +import { CastId } from '@farcaster/hub-web'; +import { ComponentPropsWithRef, useMemo } from 'react'; +import { UserData } from 'src/utils/social/farcaster/user-data'; + +import { useFavorAction } from '@us3r-network/link'; +import { Link } from '@us3r-network/data-model'; +import { toast } from 'react-toastify'; +import useFarcasterCastId from '../../../hooks/social/farcaster/useFarcasterCastId'; +// import { getCurrFid } from '../../../utils/farsign-utils'; +import { FarCast } from '../../../services/social/types'; +import useLogin from '../../../hooks/shared/useLogin'; +import useFarcasterLikeAction from '@/hooks/social/farcaster/useFarcasterLikeAction'; +import useFarcasterRecastAction from '@/hooks/social/farcaster/useFarcasterRecastAction'; +import { useFarcasterCtx } from '@/contexts/social/FarcasterCtx'; +import { cn } from '@/lib/utils'; + +export default function FCastSuperLike({ + cast, + linkId, + link, + openFarcasterQR, + onLikeSuccess, + onRecastSuccess, + onSaveSuccess, +}: ComponentPropsWithRef<'div'> & { + cast: FarCast; + linkId?: string; + link?: Link; + openFarcasterQR: () => void; + onLikeSuccess?: () => void; + onRecastSuccess?: () => void; + onSaveSuccess?: (newLinkId: string) => void; +}) { + const { isLogin: isLoginU3, login: loginU3 } = useLogin(); + const { isConnected } = useFarcasterCtx(); + const { likeCast, liked, likePending } = useFarcasterLikeAction({ + cast, + onLikeSuccess, + }); + const { recast, recasted, recastPending } = useFarcasterRecastAction({ + cast, + onRecastSuccess, + }); + const castId: CastId = useFarcasterCastId({ cast }); + + const { isFavored, isFavoring, onFavor } = useFavorAction(linkId, link, { + onSuccessfullyFavor: (done: boolean, newLinkId: string) => { + onSaveSuccess?.(newLinkId); + toast.success('Save successful'); + }, + onFailedFavor: (err: string) => { + toast.error(`Save failed: ${err}`); + }, + }); + const superLikeAction = () => { + if (likePending || recastPending || isFavoring) { + return; + } + if (!isLoginU3) { + loginU3(); + return; + } + if (!isConnected) { + openFarcasterQR(); + return; + } + if (!liked) { + likeCast(castId); + } + if (!recasted) { + recast(castId); + } + if (!isFavored) { + onFavor(); + } + }; + const superLiked = liked && recasted && isFavored; + console.log('superLiked', { superLiked, liked, recasted, isFavored }); + + return ( +
{ + e.stopPropagation(); + if (!superLiked) { + superLikeAction(); + } + }} + > + 🎩 Super Like +
+ ); +} diff --git a/apps/u3/src/components/social/farcaster/FarcasterChannel.tsx b/apps/u3/src/components/social/farcaster/FarcasterChannel.tsx index 9bbf07dd..585579d0 100644 --- a/apps/u3/src/components/social/farcaster/FarcasterChannel.tsx +++ b/apps/u3/src/components/social/farcaster/FarcasterChannel.tsx @@ -4,7 +4,13 @@ import { Link } from 'react-router-dom'; import { useFarcasterCtx } from '@/contexts/social/FarcasterCtx'; -export default function FarcasterChannel({ url }: { url: string }) { +export default function FarcasterChannel({ + url, + isCommunityLayout, +}: { + url: string; + isCommunityLayout?: boolean; +}) { const { farcasterChannels } = useFarcasterCtx(); const channel = useMemo(() => { @@ -12,6 +18,23 @@ export default function FarcasterChannel({ url }: { url: string }) { }, [farcasterChannels, url]); if (channel) { + if (isCommunityLayout) { + return ( + { + e.stopPropagation(); + }} + > + + + ); + } return ( { diff --git a/apps/u3/src/container/community/CommonStyles.tsx b/apps/u3/src/container/community/CommonStyles.tsx new file mode 100644 index 00000000..61b7459e --- /dev/null +++ b/apps/u3/src/container/community/CommonStyles.tsx @@ -0,0 +1,62 @@ +import { ComponentPropsWithRef } from 'react'; +import { cn } from '@/lib/utils'; + +export function PostList({ + className, + ...props +}: ComponentPropsWithRef<'div'>) { + return ( +
+ ); +} + +export function LoadingWrapper({ + className, + ...props +}: ComponentPropsWithRef<'div'>) { + return ( +
+ ); +} + +export function LoadingMoreWrapper({ + className, + ...props +}: ComponentPropsWithRef<'div'>) { + return ( +
+ ); +} + +export function EndMsgContainer({ + className, + ...props +}: ComponentPropsWithRef<'div'>) { + return ( +
+ ); +} diff --git a/apps/u3/src/container/community/FarcasterPostDetail.tsx b/apps/u3/src/container/community/FarcasterPostDetail.tsx index df255c36..6f40fea4 100644 --- a/apps/u3/src/container/community/FarcasterPostDetail.tsx +++ b/apps/u3/src/container/community/FarcasterPostDetail.tsx @@ -139,6 +139,7 @@ export default function FarcasterPostDetail() { farcasterUserData={{}} farcasterUserDataObj={farcasterUserDataObj} isDetail + isCommunityLayout showMenuBtn />
diff --git a/apps/u3/src/container/community/PostsFcNewest.tsx b/apps/u3/src/container/community/PostsFcNewest.tsx index 1585d6fd..18c8e560 100644 --- a/apps/u3/src/container/community/PostsFcNewest.tsx +++ b/apps/u3/src/container/community/PostsFcNewest.tsx @@ -9,8 +9,8 @@ import { LoadingMoreWrapper, LoadingWrapper, } from '@/components/profile/FollowListWidgets'; -import { PostList } from '../social/CommonStyles'; import useListScroll from '@/hooks/social/useListScroll'; +import { PostList } from './CommonStyles'; export default function PostsFcNewest() { const [parentId] = useState('posts-fc-newest'); @@ -53,7 +53,7 @@ export default function PostsFcNewest() { } scrollableTarget="posts-scroll-wrapper" > - + {fcNewestFeeds.map(({ platform, data }) => { if (platform === 'farcaster') { const key = Buffer.from(data.hash.data).toString('hex'); diff --git a/apps/u3/src/container/community/PostsFcTrending.tsx b/apps/u3/src/container/community/PostsFcTrending.tsx index 880c3509..a29fe2e3 100644 --- a/apps/u3/src/container/community/PostsFcTrending.tsx +++ b/apps/u3/src/container/community/PostsFcTrending.tsx @@ -6,9 +6,9 @@ import { useFarcasterCtx } from 'src/contexts/social/FarcasterCtx'; import Loading from 'src/components/common/loading/Loading'; import { FEEDS_SCROLL_THRESHOLD } from 'src/services/social/api/feeds'; import { LoadingMoreWrapper } from '@/components/profile/FollowListWidgets'; -import { EndMsgContainer, PostList } from '../social/CommonStyles'; import useListScroll from '@/hooks/social/useListScroll'; import useFarcasterTrending from '@/hooks/social/farcaster/useFarcasterTrending'; +import { EndMsgContainer, PostList } from './CommonStyles'; export default function PostsFcTrending() { const [parentId] = useState('posts-fc-trending'); @@ -50,7 +50,7 @@ export default function PostsFcTrending() { scrollThreshold={FEEDS_SCROLL_THRESHOLD} scrollableTarget="posts-scroll-wrapper" > - + {farcasterTrending.map(({ platform, data }) => { if (platform === 'farcaster') { const key = Buffer.from(data.hash.data).toString('hex'); diff --git a/apps/u3/src/container/social/CommonStyles.tsx b/apps/u3/src/container/social/CommonStyles.tsx index cf35263e..6561220d 100644 --- a/apps/u3/src/container/social/CommonStyles.tsx +++ b/apps/u3/src/container/social/CommonStyles.tsx @@ -6,8 +6,10 @@ * @FilePath: /u3/apps/u3/src/container/social/CommonStyles.tsx * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE */ +import { ComponentPropsWithRef } from 'react'; import NoLogin from 'src/components/layout/NoLogin'; import styled from 'styled-components'; +import { cn } from '@/lib/utils'; export const MainCenter = styled.div` width: 100%; @@ -44,6 +46,21 @@ export const PostList = styled.div` } `; +export function PostListV2({ + className, + ...props +}: ComponentPropsWithRef<'div'>) { + return ( +
+ ); +} + export const LoadingWrapper = styled.div` width: 100%; height: 80vh; diff --git a/apps/u3/src/hooks/shared/useLinkId.ts b/apps/u3/src/hooks/shared/useLinkId.ts new file mode 100644 index 00000000..83ba6e70 --- /dev/null +++ b/apps/u3/src/hooks/shared/useLinkId.ts @@ -0,0 +1,43 @@ +/* eslint-disable @typescript-eslint/no-shadow */ +import { useEffect, useState } from 'react'; +import { Link } from '@us3r-network/data-model'; +import { getS3LinkModel, useLinkState } from '@us3r-network/link'; + +export const useLinkId = (link: Link | undefined) => { + const s3LinkModel = getS3LinkModel(); + const { s3LinkModalInitialed } = useLinkState(); + const [linkId, setLinkId] = useState(''); + + const getLinkId = async (link: Link) => { + if (!s3LinkModel || !s3LinkModalInitialed) return ''; + const filters = { + where: { + url: { + equalTo: link.url, + }, + type: { + equalTo: link.type, + }, + }, + }; + const resp = await s3LinkModel?.queryLinks({ + filters, + }); + const links = resp?.data?.linkIndex?.edges; + if (links && links.length > 0) { + return links[0].node.id; + } + return ''; + }; + + useEffect(() => { + if (link && link.url && link.type) { + getLinkId(link).then((id) => { + if (id) setLinkId(id); + else setLinkId(''); + }); + } + }, [link?.url]); + + return { getLinkId, setLinkId, linkId }; +}; diff --git a/apps/u3/src/hooks/social/farcaster/useFarcasterLikeAction.ts b/apps/u3/src/hooks/social/farcaster/useFarcasterLikeAction.ts new file mode 100644 index 00000000..85b0d48f --- /dev/null +++ b/apps/u3/src/hooks/social/farcaster/useFarcasterLikeAction.ts @@ -0,0 +1,158 @@ +/* eslint-disable @typescript-eslint/no-shadow */ +import { useCallback, useEffect, useState } from 'react'; +import { + CastId, + ReactionType, + makeReactionAdd, + makeReactionRemove, +} from '@farcaster/hub-web'; +import { toast } from 'react-toastify'; +import { useFarcasterCtx } from '@/contexts/social/FarcasterCtx'; +import { FarCast } from '@/services/social/types'; +import { FARCASTER_NETWORK, FARCASTER_WEB_CLIENT } from '@/constants/farcaster'; + +export default function useFarcasterLikeAction({ + cast, + onLikeSuccess, + onRemoveLikeSuccess, +}: { + cast: FarCast; + onLikeSuccess?: () => void; + onRemoveLikeSuccess?: () => void; +}) { + const { encryptedSigner, isConnected, currFid } = useFarcasterCtx(); + const [likes, setLikes] = useState(Array.from(new Set(cast.likes))); + const [likeCount, setLikeCount] = useState( + Number(cast.like_count || cast.likesCount || 0) + ); + useEffect(() => { + setLikes(Array.from(new Set(cast.likes))); + setLikeCount(Number(cast.like_count || cast.likesCount || 0)); + }, [cast]); + const [likePending, setLikePending] = useState(false); + + const likeCast = useCallback( + async (castId: CastId) => { + if (likePending) { + return; + } + if (!isConnected) { + return; + } + if (!encryptedSigner) { + console.error('no encryptedSigner'); + return; + } + try { + setLikePending(true); + const cast = await makeReactionAdd( + { + type: ReactionType.LIKE, + targetCastId: castId, + }, + { + fid: currFid, + network: FARCASTER_NETWORK, + }, + encryptedSigner + ); + if (cast.isErr()) { + throw new Error(cast.error.message); + } + + const result = await FARCASTER_WEB_CLIENT.submitMessage(cast.value); + if (result.isErr()) { + throw new Error(result.error.message); + } + + const tmpSet = new Set(likes); + tmpSet.add(`${currFid}`); + setLikes(Array.from(tmpSet)); + setLikeCount(likeCount + 1); + onLikeSuccess?.(); + + toast.success('like post created'); + } catch (error) { + console.error(error); + toast.error('error like'); + } finally { + setLikePending(false); + } + }, + [ + encryptedSigner, + isConnected, + likeCount, + likes, + currFid, + likePending, + onLikeSuccess, + ] + ); + + const removeLikeCast = useCallback( + async (castId: CastId) => { + if (likePending) { + return; + } + if (!isConnected) { + return; + } + if (!encryptedSigner) return; + // const currFid = getCurrFid(); + try { + setLikePending(true); + const cast = await makeReactionRemove( + { + type: ReactionType.LIKE, + targetCastId: castId, + }, + { + fid: currFid, + network: FARCASTER_NETWORK, + }, + encryptedSigner + ); + if (cast.isErr()) { + throw new Error(cast.error.message); + } + + const result = await FARCASTER_WEB_CLIENT.submitMessage(cast.value); + if (result.isErr()) { + throw new Error(result.error.message); + } + + const tmpSet = new Set(likes); + tmpSet.delete(`${currFid}`); + setLikes(Array.from(tmpSet)); + setLikeCount(likeCount - 1); + onRemoveLikeSuccess?.(); + toast.success('like post removed'); + } catch (error) { + toast.error('error like'); + } finally { + setLikePending(false); + } + }, + [ + encryptedSigner, + isConnected, + likeCount, + likes, + currFid, + likePending, + onRemoveLikeSuccess, + ] + ); + + const liked = likes.includes(`${currFid}`); + + return { + likes, + likeCount, + likeCast, + removeLikeCast, + liked, + likePending, + }; +} diff --git a/apps/u3/src/hooks/social/farcaster/useFarcasterRecastAction.ts b/apps/u3/src/hooks/social/farcaster/useFarcasterRecastAction.ts new file mode 100644 index 00000000..b6991166 --- /dev/null +++ b/apps/u3/src/hooks/social/farcaster/useFarcasterRecastAction.ts @@ -0,0 +1,155 @@ +/* eslint-disable @typescript-eslint/no-shadow */ +import { useCallback, useEffect, useState } from 'react'; +import { + CastId, + ReactionType, + makeReactionAdd, + makeReactionRemove, +} from '@farcaster/hub-web'; +import { toast } from 'react-toastify'; +import { useFarcasterCtx } from '@/contexts/social/FarcasterCtx'; +import { FarCast } from '@/services/social/types'; +import { FARCASTER_NETWORK, FARCASTER_WEB_CLIENT } from '@/constants/farcaster'; + +export default function useFarcasterRecastAction({ + cast, + onRecastSuccess, + onRemoveRecastSuccess, +}: { + cast: FarCast; + onRecastSuccess?: () => void; + onRemoveRecastSuccess?: () => void; +}) { + const { encryptedSigner, isConnected, currFid } = useFarcasterCtx(); + const [recasts, setRecasts] = useState( + Array.from(new Set(cast.recasts)) + ); + const [recastCount, setRecastCount] = useState( + Number(cast.recast_count || cast.recastsCount || 0) + ); + useEffect(() => { + setRecasts(Array.from(new Set(cast.recasts))); + setRecastCount(Number(cast.recast_count || cast.recastsCount || 0)); + }, [cast]); + + const [recastPending, setRecastPending] = useState(false); + + const recast = useCallback( + async (castId: CastId) => { + if (recastPending) { + return; + } + if (!isConnected) { + return; + } + if (!encryptedSigner) return; + try { + setRecastPending(true); + const cast = await makeReactionAdd( + { + type: ReactionType.RECAST, + targetCastId: castId, + }, + { + fid: currFid, + network: FARCASTER_NETWORK, + }, + encryptedSigner + ); + if (cast.isErr()) { + throw new Error(cast.error.message); + } + + const result = await FARCASTER_WEB_CLIENT.submitMessage(cast.value); + if (result.isErr()) { + throw new Error(result.error.message); + } + + const tmpSet = new Set(recasts); + tmpSet.add(`${currFid}`); + setRecasts(Array.from(tmpSet)); + setRecastCount(recastCount + 1); + onRecastSuccess?.(); + toast.success('recast created'); + } catch (error) { + toast.error('error recast'); + } finally { + setRecastPending(false); + } + }, + [ + encryptedSigner, + isConnected, + recastCount, + recasts, + currFid, + recastPending, + onRecastSuccess, + ] + ); + + const removeRecast = useCallback( + async (castId: CastId) => { + if (recastPending) { + return; + } + if (!currFid) return; + if (!isConnected) { + return; + } + if (!encryptedSigner) return; + // const currFid = getCurrFid(); + try { + setRecastPending(true); + const cast = await makeReactionRemove( + { + type: ReactionType.RECAST, + targetCastId: castId, + }, + { + fid: currFid, + network: FARCASTER_NETWORK, + }, + encryptedSigner + ); + if (cast.isErr()) { + throw new Error(cast.error.message); + } + + const result = await FARCASTER_WEB_CLIENT.submitMessage(cast.value); + if (result.isErr()) { + throw new Error(result.error.message); + } + + const tmpSet = new Set(recasts); + tmpSet.delete(`${currFid}`); + setRecasts(Array.from(tmpSet)); + setRecastCount(recastCount - 1); + onRemoveRecastSuccess?.(); + + toast.success('removed recast'); + } catch (error) { + toast.error('error recast'); + } finally { + setRecastPending(false); + } + }, + [ + encryptedSigner, + isConnected, + recastCount, + recasts, + currFid, + onRemoveRecastSuccess, + ] + ); + const recasted = recasts.includes(`${currFid}`); + return { + recast, + removeRecast, + recasts, + recastCount, + recasted, + recastPending, + }; +}