diff --git a/networks.json b/networks.json index 33a10bd1a3..3f8be63d81 100644 --- a/networks.json +++ b/networks.json @@ -7,7 +7,8 @@ "icon": "icons/networks/teritori.svg", "features": [ "NFTMarketplace", - "Organizations" + "Organizations", + "SocialFeed" ], "walletUrlForStaking": "https://explorer.teritori.com/teritori/staking", "currencies": [ @@ -132,7 +133,8 @@ "icon": "icons/networks/teritori.svg", "features": [ "NFTMarketplace", - "Organizations" + "Organizations", + "SocialFeed" ], "currencies": [ { @@ -567,7 +569,8 @@ "displayName": "Gno Dev", "icon": "icons/networks/gno.svg", "features": [ - "Organizations" + "Organizations", + "SocialFeed" ], "currencies": [ { @@ -600,7 +603,8 @@ "displayName": "Gno Teritori", "icon": "icons/networks/gno.svg", "features": [ - "Organizations" + "Organizations", + "SocialFeed" ], "currencies": [ { diff --git a/packages/components/TopMenu/TopMenuMyWallets.tsx b/packages/components/TopMenu/TopMenuMyWallets.tsx index 478fe765de..8d62e46fce 100644 --- a/packages/components/TopMenu/TopMenuMyWallets.tsx +++ b/packages/components/TopMenu/TopMenuMyWallets.tsx @@ -11,7 +11,7 @@ import { useSelectedNetworkInfo, } from "../../hooks/useSelectedNetwork"; import useSelectedWallet from "../../hooks/useSelectedWallet"; -import { getStakingCurrency, NetworkKind } from "../../networks"; +import { CurrencyInfo, getStakingCurrency, NetworkKind } from "../../networks"; import { DepositWithdrawModal } from "../../screens/WalletManager/components/DepositWithdrawModal"; import { useAppNavigation } from "../../utils/navigation"; import { @@ -189,7 +189,7 @@ export const TopMenuMyWallets: React.FC = () => { const atomIbcCurrency = useMemo(() => { return selectedNetworkInfo?.currencies.find( - (currencyInfo) => + (currencyInfo: CurrencyInfo) => currencyInfo.kind === "ibc" && currencyInfo.sourceDenom === "uatom" ); }, [selectedNetworkInfo]); diff --git a/packages/components/socialFeed/NewsFeed/NewsFeedInput.tsx b/packages/components/socialFeed/NewsFeed/NewsFeedInput.tsx index d4acbedebd..634cca1b39 100644 --- a/packages/components/socialFeed/NewsFeed/NewsFeedInput.tsx +++ b/packages/components/socialFeed/NewsFeed/NewsFeedInput.tsx @@ -1,4 +1,5 @@ import { coin } from "@cosmjs/amino"; +import { GnoJSONRPCProvider } from "@gnolang/gno-js-client"; import React, { useImperativeHandle, useRef, useState } from "react"; import { useForm } from "react-hook-form"; import { @@ -35,12 +36,17 @@ import { useUpdatePostFee } from "../../../hooks/feed/useUpdatePostFee"; import { useBalances } from "../../../hooks/useBalances"; import { useIsMobile } from "../../../hooks/useIsMobile"; import { useMaxResolution } from "../../../hooks/useMaxResolution"; -import { useSelectedNetworkId } from "../../../hooks/useSelectedNetwork"; +import { useSelectedNetworkInfo } from "../../../hooks/useSelectedNetwork"; import useSelectedWallet from "../../../hooks/useSelectedWallet"; -import { getUserId, mustGetCosmosNetwork } from "../../../networks"; +import { + NetworkKind, + getUserId, + mustGetCosmosNetwork, +} from "../../../networks"; import { selectNFTStorageAPI } from "../../../store/slices/settings"; import { prettyPrice } from "../../../utils/coins"; import { defaultSocialFeedFee } from "../../../utils/fee"; +import { adenaDoContract } from "../../../utils/gno"; import { generateIpfsKey, uploadFilesToPinata } from "../../../utils/ipfs"; import { AUDIO_MIME_TYPES, @@ -89,6 +95,7 @@ import { FileUploader } from "../../fileUploader"; import { SpacerColumn } from "../../spacer"; import { EmojiSelector } from "../EmojiSelector"; import { GIFSelector } from "../GIFSelector"; +import { GNO_SOCIAL_FEEDS_PKG_PATH, TERITORI_FEED_ID } from "../const"; interface NewsFeedInputProps { type: "comment" | "post"; @@ -142,7 +149,8 @@ export const NewsFeedInput = React.forwardRef< const inputMinHeight = 20; const inputHeight = useSharedValue(20); const wallet = useSelectedWallet(); - const selectedNetworkId = useSelectedNetworkId(); + const selectedNetwork = useSelectedNetworkInfo(); + const selectedNetworkId = selectedNetwork?.id || "teritori"; const selectedWallet = useSelectedWallet(); const userId = getUserId(selectedNetworkId, selectedWallet?.address); const inputRef = useRef(null); @@ -170,10 +178,7 @@ export const NewsFeedInput = React.forwardRef< onCloseCreateModal && onCloseCreateModal(); }; - const balances = useBalances( - process.env.TERITORI_NETWORK_ID, - wallet?.address - ); + const balances = useBalances(selectedNetworkId, wallet?.address); const { setValue, handleSubmit, reset, watch } = useForm( { @@ -199,8 +204,11 @@ export const NewsFeedInput = React.forwardRef< ); const processSubmit = async () => { - const toriBalance = balances.find((bal) => bal.denom === "utori"); - if (postFee > Number(toriBalance?.amount) && !freePostCount) { + const denom = selectedNetwork?.currencies[0].denom; + + const currentBalance = balances.find((bal) => bal.denom === denom); + + if (postFee > Number(currentBalance?.amount) && !freePostCount) { return setNotEnoughFundModal(true); } @@ -254,14 +262,8 @@ export const NewsFeedInput = React.forwardRef< }); return; } - const postCategory = getPostCategory(formValues); - const client = await signingSocialFeedClient({ - networkId: selectedNetworkId, - walletAddress: wallet?.address || "", - }); - const metadata: SocialFeedMetadata = generatePostMetadata({ title: formValues.title || "", message: finalMessage, @@ -282,6 +284,7 @@ export const NewsFeedInput = React.forwardRef< if (daoId) { const network = mustGetCosmosNetwork(selectedNetworkId); + if (!network.socialFeedContractAddress) { throw new Error("Social feed contract address not found"); } @@ -306,15 +309,51 @@ export const NewsFeedInput = React.forwardRef< ], }); } else { - await mutateAsync({ - client, - msg, - args: { - fee: defaultSocialFeedFee, - memo: "", - funds: [coin(postFee, "utori")], - }, - }); + if (selectedNetwork?.kind === NetworkKind.Gno) { + // const provider = new GnoJSONRPCProvider(selectedNetwork.endpoint); + + const msg = { + category: postCategory, + identifier, + metadata: JSON.stringify(metadata), + parentPostIdentifier: hasUsername ? replyTo?.parentId : parentId, + }; + + const vmCall = { + caller: selectedWallet?.address || "", + send: "", + pkg_path: GNO_SOCIAL_FEEDS_PKG_PATH, + func: "CreatePost", + args: [ + TERITORI_FEED_ID, + msg.parentPostIdentifier || "0", + msg.category.toString(), + msg.metadata, + ], + }; + + const txHash = await adenaDoContract(selectedNetworkId, [ + { type: "/vm.m_call", value: vmCall }, + ]); + + const provider = new GnoJSONRPCProvider(selectedNetwork.endpoint); + await provider.waitForTransaction(txHash); + onPostCreationSuccess(); + } else { + const client = await signingSocialFeedClient({ + networkId: selectedNetworkId, + walletAddress: wallet?.address || "", + }); + await mutateAsync({ + client, + msg, + args: { + fee: defaultSocialFeedFee, + memo: "", + funds: [coin(postFee, "utori")], + }, + }); + } if ( postCategory === PostCategory.Question || @@ -552,7 +591,7 @@ export const NewsFeedInput = React.forwardRef< : `The cost for this ${type} is ${prettyPrice( selectedNetworkId, postFee.toString(), - "utori" + selectedNetwork?.currencies?.[0].denom || "utori" )}`} diff --git a/packages/components/socialFeed/NewsFeed/NewsFeedQueries.ts b/packages/components/socialFeed/NewsFeed/NewsFeedQueries.ts index 473e622467..bba7afe8b8 100644 --- a/packages/components/socialFeed/NewsFeed/NewsFeedQueries.ts +++ b/packages/components/socialFeed/NewsFeed/NewsFeedQueries.ts @@ -1,9 +1,10 @@ import { coin } from "@cosmjs/amino"; +import { GnoJSONRPCProvider } from "@gnolang/gno-js-client"; import { v4 as uuidv4 } from "uuid"; import { - PostCategory, NewPostFormValues, + PostCategory, SocialFeedMetadata, } from "./NewsFeed.type"; import { @@ -11,9 +12,13 @@ import { signingSocialFeedClient, } from "../../../client-creators/socialFeedClient"; import { Wallet } from "../../../context/WalletsProvider"; +import { mustGetNetwork, NetworkKind } from "../../../networks"; import { defaultSocialFeedFee } from "../../../utils/fee"; +import { adenaDoContract } from "../../../utils/gno"; import { ipfsURLToHTTPURL, uploadFilesToPinata } from "../../../utils/ipfs"; import { RemoteFileData } from "../../../utils/types/files"; +import { GNO_SOCIAL_FEEDS_PKG_PATH, TERITORI_FEED_ID } from "../const"; + interface GetAvailableFreePostParams { networkId: string; wallet?: Wallet; @@ -122,11 +127,6 @@ export const createPost = async ({ return; } - const client = await signingSocialFeedClient({ - networkId, - walletAddress: wallet.address, - }); - let files: RemoteFileData[] = []; if (formValues.files?.length && pinataJWTKey) { @@ -161,17 +161,58 @@ export const createPost = async ({ mentions: formValues.mentions || [], }); - await client.createPost( - { + const network = mustGetNetwork(networkId); + + if (network.kind === NetworkKind.Gno) { + const msg = { category, - identifier: identifier || uuidv4(), + identifier, metadata: JSON.stringify(metadata), parentPostIdentifier: parentId, - }, - defaultSocialFeedFee, - "", - freePostCount ? undefined : [coin(fee, "utori")] - ); + }; + + const vmCall = { + caller: wallet.address, + send: "", + pkg_path: GNO_SOCIAL_FEEDS_PKG_PATH, + func: "CreatePost", + args: [ + TERITORI_FEED_ID, + msg.parentPostIdentifier || "0", + msg.category.toString(), + msg.metadata, + ], + }; + + const txHash = await adenaDoContract( + networkId, + [{ type: "/vm.m_call", value: vmCall }], + { + gasWanted: 2_000_000, + } + ); + + const provider = new GnoJSONRPCProvider(network.endpoint); + await provider.waitForTransaction(txHash); + } else { + const client = await signingSocialFeedClient({ + networkId, + walletAddress: wallet.address, + }); + + await client.createPost( + { + category, + identifier: identifier || uuidv4(), + metadata: JSON.stringify(metadata), + parentPostIdentifier: parentId, + }, + defaultSocialFeedFee, + "", + freePostCount ? undefined : [coin(fee, "utori")] + ); + } + return true; }; diff --git a/packages/components/socialFeed/SocialThread/SocialCommentCard.tsx b/packages/components/socialFeed/SocialThread/SocialCommentCard.tsx index 70e66c7e6d..c2d771641f 100644 --- a/packages/components/socialFeed/SocialThread/SocialCommentCard.tsx +++ b/packages/components/socialFeed/SocialThread/SocialCommentCard.tsx @@ -1,3 +1,4 @@ +import { GnoJSONRPCProvider } from "@gnolang/gno-js-client"; import React, { useEffect, useMemo, useState } from "react"; import { ActivityIndicator, @@ -20,10 +21,11 @@ import { import { useIsMobile } from "../../../hooks/useIsMobile"; import { useNSUserInfo } from "../../../hooks/useNSUserInfo"; import { usePrevious } from "../../../hooks/usePrevious"; -import { useSelectedNetworkId } from "../../../hooks/useSelectedNetwork"; +import { useSelectedNetworkInfo } from "../../../hooks/useSelectedNetwork"; import useSelectedWallet from "../../../hooks/useSelectedWallet"; -import { parseUserId } from "../../../networks"; +import { NetworkKind, parseUserId } from "../../../networks"; import { OnPressReplyType } from "../../../screens/FeedPostView/FeedPostViewScreen"; +import { adenaDoContract } from "../../../utils/gno"; import { useAppNavigation } from "../../../utils/navigation"; import { DEFAULT_USERNAME, @@ -56,6 +58,7 @@ import { nbReactionsShown, Reactions } from "../SocialActions/Reactions"; import { ReplyButton } from "../SocialActions/ReplyButton"; import { ShareButton } from "../SocialActions/ShareButton"; import { TipButton } from "../SocialActions/TipButton"; +import { GNO_SOCIAL_FEEDS_PKG_PATH, TERITORI_FEED_ID } from "../const"; const BREAKPOINT_S = 480; @@ -90,7 +93,8 @@ export const SocialCommentCard: React.FC = ({ const [replyListYOffset, setReplyListYOffset] = useState([]); const [replyListLayout, setReplyListLayout] = useState(); const wallet = useSelectedWallet(); - const selectedNetworkId = useSelectedNetworkId(); + const selectedNetwork = useSelectedNetworkInfo(); + const selectedNetworkId = selectedNetwork?.id || ""; const [, userAddress] = parseUserId(localComment.createdBy); const { data, refetch, fetchNextPage, hasNextPage, isFetching } = useFetchComments({ @@ -163,6 +167,51 @@ export const SocialCommentCard: React.FC = ({ } }; + const cosmosReaction = async (icon: string, walletAddress: string) => { + const client = await signingSocialFeedClient({ + networkId: selectedNetworkId, + walletAddress, + }); + + postMutate({ + client, + msg: { + icon, + identifier: localComment.identifier, + up: true, + }, + }); + }; + + const gnoReaction = async (icon: string, rpcEndpoint: string) => { + const vmCall = { + caller: wallet?.address || "", + send: "", + pkg_path: GNO_SOCIAL_FEEDS_PKG_PATH, + func: "ReactPost", + args: [TERITORI_FEED_ID, localComment.identifier, icon, "true"], + }; + + const txHash = await adenaDoContract( + selectedNetworkId || "", + [{ type: "/vm.m_call", value: vmCall }], + { + gasWanted: 2_000_000, + } + ); + + const provider = new GnoJSONRPCProvider(rpcEndpoint); + + // Wait for tx done + await provider.waitForTransaction(txHash); + + const reactions = getUpdatedReactions(localComment.reactions, icon); + setLocalComment({ + ...localComment, + reactions: reactions as ReactionFromContract[], + }); + }; + const handleReply = () => onPressReply?.({ username, @@ -175,19 +224,12 @@ export const SocialCommentCard: React.FC = ({ if (!wallet?.connected || !wallet.address) { return; } - const client = await signingSocialFeedClient({ - networkId: selectedNetworkId, - walletAddress: wallet.address, - }); - postMutate({ - client, - msg: { - icon: e, - identifier: localComment.identifier, - up: true, - }, - }); + if (selectedNetwork?.kind === NetworkKind.Gno) { + gnoReaction(e, selectedNetwork.endpoint); + } else { + cosmosReaction(e, wallet.address); + } }; return ( diff --git a/packages/components/socialFeed/SocialThread/SocialThreadCard.tsx b/packages/components/socialFeed/SocialThread/SocialThreadCard.tsx index 966d3bb338..f7e78fea8f 100644 --- a/packages/components/socialFeed/SocialThread/SocialThreadCard.tsx +++ b/packages/components/socialFeed/SocialThread/SocialThreadCard.tsx @@ -1,3 +1,4 @@ +import { GnoJSONRPCProvider } from "@gnolang/gno-js-client"; import React, { useEffect, useState } from "react"; import { StyleProp, View, ViewStyle } from "react-native"; @@ -7,10 +8,11 @@ import { Post } from "../../../api/feed/v1/feed"; import { signingSocialFeedClient } from "../../../client-creators/socialFeedClient"; import { useTeritoriSocialFeedReactPostMutation } from "../../../contracts-clients/teritori-social-feed/TeritoriSocialFeed.react-query"; import { useNSUserInfo } from "../../../hooks/useNSUserInfo"; -import { useSelectedNetworkId } from "../../../hooks/useSelectedNetwork"; +import { useSelectedNetworkInfo } from "../../../hooks/useSelectedNetwork"; import useSelectedWallet from "../../../hooks/useSelectedWallet"; -import { parseUserId } from "../../../networks"; +import { NetworkKind, parseUserId } from "../../../networks"; import { OnPressReplyType } from "../../../screens/FeedPostView/FeedPostViewScreen"; +import { adenaDoContract } from "../../../utils/gno"; import { useAppNavigation } from "../../../utils/navigation"; import { getUpdatedReactions } from "../../../utils/social-feed"; import { @@ -32,6 +34,7 @@ import { ReplyButton } from "../SocialActions/ReplyButton"; import { ShareButton } from "../SocialActions/ShareButton"; import { SocialThreadGovernance } from "../SocialActions/SocialThreadGovernance"; import { TipButton } from "../SocialActions/TipButton"; +import { GNO_SOCIAL_FEEDS_PKG_PATH, TERITORI_FEED_ID } from "../const"; const BREAKPOINT_S = 480; @@ -67,7 +70,8 @@ export const SocialThreadCard: React.FC<{ }); const wallet = useSelectedWallet(); - const selectedNetworkId = useSelectedNetworkId(); + const selectedNetworkInfo = useSelectedNetworkInfo(); + const selectedNetworkId = selectedNetworkInfo?.id; const authorNSInfo = useNSUserInfo(localPost.createdBy); const [, userAddress] = parseUserId(localPost.createdBy); const userInfo = useNSUserInfo(wallet?.userId); @@ -82,13 +86,10 @@ export const SocialThreadCard: React.FC<{ // return getCommunityHashtag(metadata?.hashtags || []); // }, [metadata]); - const handleReaction = async (emoji: string) => { - if (!wallet?.connected || !wallet.address) { - return; - } + const cosmosReaction = async (emoji: string, walletAddress: string) => { const client = await signingSocialFeedClient({ - networkId: selectedNetworkId, - walletAddress: wallet.address, + networkId: selectedNetworkId || "", + walletAddress, }); postMutate({ @@ -101,6 +102,54 @@ export const SocialThreadCard: React.FC<{ }); }; + const gnoReaction = async (emoji: string, rpcEndpoint: string) => { + const vmCall = { + caller: wallet?.address || "", + send: "", + pkg_path: GNO_SOCIAL_FEEDS_PKG_PATH, + func: "ReactPost", + args: [TERITORI_FEED_ID, localPost.identifier, emoji, "true"], + }; + + const txHash = await adenaDoContract( + selectedNetworkId || "", + [{ type: "/vm.m_call", value: vmCall }], + { + gasWanted: 2_000_000, + } + ); + + const provider = new GnoJSONRPCProvider(rpcEndpoint); + + // Wait for tx done + await provider.waitForTransaction(txHash); + + const reactions = [...post.reactions]; + const currentReactionIdx = reactions.findIndex((r) => r.icon === emoji); + + if (currentReactionIdx > -1) { + reactions[currentReactionIdx].count++; + } else { + reactions.push({ + icon: emoji, + count: 1, + }); + } + setLocalPost({ ...localPost, reactions }); + }; + + const handleReaction = async (emoji: string) => { + if (!wallet?.connected || !wallet.address) { + return; + } + + if (selectedNetworkInfo?.kind === NetworkKind.Gno) { + gnoReaction(emoji, selectedNetworkInfo?.endpoint || ""); + } else { + cosmosReaction(emoji, wallet.address); + } + }; + const handleReply = () => onPressReply?.({ username, diff --git a/packages/components/socialFeed/const.ts b/packages/components/socialFeed/const.ts new file mode 100644 index 0000000000..b713c5d931 --- /dev/null +++ b/packages/components/socialFeed/const.ts @@ -0,0 +1,2 @@ +export const GNO_SOCIAL_FEEDS_PKG_PATH = "gno.land/r/demo/social_feeds_v3"; +export const TERITORI_FEED_ID = "1"; diff --git a/packages/components/socialFeed/utils.ts b/packages/components/socialFeed/utils.ts new file mode 100644 index 0000000000..b767946521 --- /dev/null +++ b/packages/components/socialFeed/utils.ts @@ -0,0 +1,81 @@ +import { Post, Reaction } from "../../api/feed/v1/feed"; +import { getUserId } from "../../networks"; + +export const decodeGnoPost = (networkId: string, postData: string): Post => { + const buf = Buffer.from(postData, "base64"); + + let offset = 0; + + // HACK: we don't have readBigUint64BE so we shift 4 position to read readUInt32BE + offset += 4; + const identifier = buf.readUInt32BE(offset); + offset += 4; + + // HACK: we don't have readBigUint64BE so we shift 4 position to read readUInt32BE + offset += 4; + const parentPostIdentifier = buf.readUInt32BE(offset); + offset += 4; + + // HACK: we don't have readBigUint64BE so we shift 4 position to read readUInt32BE + offset += 4; + // const feedId = buf.readUInt32BE(offset); + offset += 4; + + offset += 4; + const category = buf.readUInt32BE(offset); + offset += 4; + + const metadataLen = buf.readUInt32BE(offset); + offset += 4; + const metadata = buf.slice(offset, offset + metadataLen).toString(); + offset += metadataLen; + + const addrLen = buf.readUInt16BE(offset); + offset += 2; + const createdBy = buf.slice(offset, offset + addrLen).toString(); + offset += addrLen; + + const createdAt = buf.readUInt32BE(offset); + offset += 4; + + const reactionsStrLen = buf.readUInt32BE(offset); + offset += 4; + const reactionsStr = buf.slice(offset, offset + reactionsStrLen).toString(); + + const reactions: Reaction[] = []; + for (const reactionStr of reactionsStr.split(",")) { + if (!reactionStr) continue; + const splitted = reactionStr.split(":"); + reactions.push({ icon: splitted[0], count: parseInt(splitted[1], 10) }); + } + offset += reactionsStrLen; + + const subpostsStrLen = buf.readUInt32BE(offset); + offset += 4; + + let subpostIDs: string[] = []; + if (subpostsStrLen === 0) { + subpostIDs = []; + } else { + subpostIDs = buf + .slice(offset, offset + subpostsStrLen) + .toString() + .split(","); + } + offset += subpostsStrLen; + + const post: Post = { + category, + isDeleted: false, + identifier: "" + identifier, + metadata, + parentPostIdentifier: "" + parentPostIdentifier, + subPostLength: subpostIDs.length, + createdBy: getUserId(networkId, createdBy), + createdAt: +createdAt * 1000, + tipAmount: 0, + reactions, + }; + + return post; +}; diff --git a/packages/hooks/feed/useCreatePost.tsx b/packages/hooks/feed/useCreatePost.tsx index 05f8827d8d..067f05a7a3 100644 --- a/packages/hooks/feed/useCreatePost.tsx +++ b/packages/hooks/feed/useCreatePost.tsx @@ -41,7 +41,7 @@ export const useCreatePost = ({ ); onMutate && onMutate(); // Return a context with the previous user and updated user - return { prevData, newComment }; // contextx + return { prevData, newComment }; // context }, onSuccess: async (_, data, context: any) => { const updatedComment = context.newComment as PostResultExtra; diff --git a/packages/hooks/feed/useFetchComments.ts b/packages/hooks/feed/useFetchComments.ts index fc94f9a8b0..a5eaee0ff6 100644 --- a/packages/hooks/feed/useFetchComments.ts +++ b/packages/hooks/feed/useFetchComments.ts @@ -1,8 +1,17 @@ +import { GnoJSONRPCProvider } from "@gnolang/gno-js-client"; import { useInfiniteQuery } from "@tanstack/react-query"; +import { Post } from "../../api/feed/v1/feed"; import { nonSigningSocialFeedClient } from "../../client-creators/socialFeedClient"; +import { + GNO_SOCIAL_FEEDS_PKG_PATH, + TERITORI_FEED_ID, +} from "../../components/socialFeed/const"; +import { decodeGnoPost } from "../../components/socialFeed/utils"; import { PostResult } from "../../contracts-clients/teritori-social-feed/TeritoriSocialFeed.types"; -import { useSelectedNetworkId } from "../useSelectedNetwork"; +import { GnoNetworkInfo, NetworkKind } from "../../networks"; +import { extractGnoString } from "../../utils/gno"; +import { useSelectedNetworkInfo } from "../useSelectedNetwork"; export type FetchCommentResponse = { list: PostResult[]; @@ -20,30 +29,66 @@ type ConfigType = { enabled?: boolean; }; +const fetchTeritoriComments = async ( + networkId: string, + pageParam: number, + parentId?: string +) => { + const client = await nonSigningSocialFeedClient({ + networkId, + }); + + const subComment = await client.querySubPosts({ + count: 5, + from: pageParam || 0, + sort: "desc", + identifier: parentId || "", + }); + + return { list: subComment }; +}; + +const fetchGnoComments = async ( + selectedNetwork: GnoNetworkInfo, + parentId: string +) => { + const provider = new GnoJSONRPCProvider(selectedNetwork.endpoint); + const output = await provider.evaluateExpression( + GNO_SOCIAL_FEEDS_PKG_PATH, + `GetComments(${TERITORI_FEED_ID}, ${parentId})` + ); + + const posts: Post[] = []; + + const outputStr = extractGnoString(output); + for (const postData of outputStr.split(",")) { + const post = decodeGnoPost(selectedNetwork.id, postData); + posts.push(post); + } + return { list: posts.sort((p1, p2) => p2.createdAt - p1.createdAt) }; +}; + export const useFetchComments = ({ parentId, totalCount, enabled, }: ConfigType) => { // variable - const selectedNetworkId = useSelectedNetworkId(); + const selectedNetwork = useSelectedNetworkInfo(); // request const data = useInfiniteQuery( - ["FetchComment", parentId], + ["FetchComment", parentId, selectedNetwork?.id], async ({ pageParam }) => { - const client = await nonSigningSocialFeedClient({ - networkId: selectedNetworkId, - }); - - const subComment = await client.querySubPosts({ - count: 5, - from: pageParam || 0, - sort: "desc", - identifier: parentId || "", - }); - - return { list: subComment }; + if (selectedNetwork?.kind === NetworkKind.Gno) { + return await fetchGnoComments(selectedNetwork, parentId || ""); + } else { + return await fetchTeritoriComments( + selectedNetwork?.id || "", + pageParam, + parentId + ); + } }, { getNextPageParam: (_, pages) => { diff --git a/packages/hooks/feed/useFetchFeed.ts b/packages/hooks/feed/useFetchFeed.ts index 7933d532dd..6e27dd8774 100644 --- a/packages/hooks/feed/useFetchFeed.ts +++ b/packages/hooks/feed/useFetchFeed.ts @@ -1,9 +1,18 @@ +import { GnoJSONRPCProvider } from "@gnolang/gno-js-client"; import { useInfiniteQuery } from "@tanstack/react-query"; import { Post, PostsRequest } from "../../api/feed/v1/feed"; import { nonSigningSocialFeedClient } from "../../client-creators/socialFeedClient"; +import { + GNO_SOCIAL_FEEDS_PKG_PATH, + TERITORI_FEED_ID, +} from "../../components/socialFeed/const"; +import { decodeGnoPost } from "../../components/socialFeed/utils"; +import { GnoNetworkInfo, NetworkInfo, NetworkKind } from "../../networks"; import { mustGetFeedClient } from "../../utils/backend"; -import { useSelectedNetworkId } from "../useSelectedNetwork"; +import { extractGnoString } from "../../utils/gno"; +import { useSelectedNetworkInfo } from "../useSelectedNetwork"; +import useSelectedWallet from "../useSelectedWallet"; export type PostsList = { list: Post[]; @@ -13,31 +22,79 @@ export type PostsList = { export const combineFetchFeedPages = (pages: PostsList[]) => pages.reduce((acc: Post[], page) => [...acc, ...(page?.list || [])], []); +const fetchTeritoriFeed = async ( + selectedNetwork: NetworkInfo, + req: PostsRequest, + pageParam: number +) => { + try { + // ===== We use social-feed contract to get the total posts count + const client = await nonSigningSocialFeedClient({ + networkId: selectedNetwork.id, + }); + const mainPostsCount = await client.queryMainPostsCount(); + + // Overriding the posts request with the current pageParam as offset + const postsRequest: PostsRequest = { ...req, offset: pageParam || 0 }; + // Getting posts + const list = await getPosts(selectedNetwork.id, postsRequest); + + return { list, totalCount: mainPostsCount } as PostsList; + } catch (err) { + console.error("teritori initData err", err); + return { list: [], totalCount: 0 } as PostsList; + } +}; + +const fetchGnoFeed = async ( + selectedNetwork: GnoNetworkInfo, + req: PostsRequest, + pageParam: number +) => { + try { + const offset = pageParam || 0; + const limit = 10; + const category = req.filter?.categories?.[0] || 2; // Normal + + const provider = new GnoJSONRPCProvider(selectedNetwork.endpoint); + const output = await provider.evaluateExpression( + GNO_SOCIAL_FEEDS_PKG_PATH, + `GetPosts(${TERITORI_FEED_ID}, ${category}, ${offset}, ${limit})` + ); + + const posts: Post[] = []; + + const outputStr = extractGnoString(output); + for (const postData of outputStr.split(",")) { + const post = decodeGnoPost(selectedNetwork.id, postData); + posts.push(post); + } + + return { + list: posts.sort((p1, p2) => p2.createdAt - p1.createdAt), + totalCount: posts.length, + } as PostsList; + } catch (err) { + throw err; + } +}; + export const useFetchFeed = (req: PostsRequest) => { - const selectedNetworkId = useSelectedNetworkId(); + const selectedNetwork = useSelectedNetworkInfo(); + const wallet = useSelectedWallet(); const { data, isFetching, refetch, hasNextPage, fetchNextPage, isLoading } = useInfiniteQuery( - ["posts", selectedNetworkId, { ...req }], + ["posts", selectedNetwork?.id, wallet?.address, { ...req }], async ({ pageParam = req.offset }) => { - try { - // ===== We use social-feed contract to get the total posts count - const client = await nonSigningSocialFeedClient({ - networkId: selectedNetworkId, - }); - const mainPostsCount = await client.queryMainPostsCount(); - - // Overriding the posts request with the current pageParam as offset - const postsRequest: PostsRequest = { ...req, offset: pageParam || 0 }; - // Getting posts - const list = await getPosts(selectedNetworkId, postsRequest); - - return { list, totalCount: mainPostsCount } as PostsList; - } catch (err) { - console.error("initData err", err); - return { list: [], totalCount: 0 } as PostsList; + if (selectedNetwork?.kind === NetworkKind.Cosmos) { + return fetchTeritoriFeed(selectedNetwork, req, pageParam); + } else if (selectedNetwork?.kind === NetworkKind.Gno) { + return fetchGnoFeed(selectedNetwork, req, pageParam); } + + throw Error(`Network ${selectedNetwork?.id} is not supported`); }, { getNextPageParam: (lastPage, pages) => { diff --git a/packages/hooks/feed/usePost.ts b/packages/hooks/feed/usePost.ts index 9679c9699b..934a741483 100644 --- a/packages/hooks/feed/usePost.ts +++ b/packages/hooks/feed/usePost.ts @@ -1,15 +1,45 @@ +import { GnoJSONRPCProvider } from "@gnolang/gno-js-client"; import { useQuery } from "@tanstack/react-query"; import { nonSigningSocialFeedClient } from "../../client-creators/socialFeedClient"; +import { GNO_SOCIAL_FEEDS_PKG_PATH } from "../../components/socialFeed/const"; +import { decodeGnoPost } from "../../components/socialFeed/utils"; +import { NetworkKind, getNetwork } from "../../networks"; +import { extractGnoString } from "../../utils/gno"; export const usePost = (id: string, networkId: string) => { const { data, ...other } = useQuery( ["social-post", id, networkId], async () => { - const client = await nonSigningSocialFeedClient({ - networkId, - }); - return await client.queryPost({ identifier: id }); + const network = getNetwork(networkId); + + if (network?.kind === NetworkKind.Gno) { + const provider = new GnoJSONRPCProvider(network.endpoint); + const output = await provider.evaluateExpression( + GNO_SOCIAL_FEEDS_PKG_PATH, + `GetPost(1, ${id})` + ); + + const postData = extractGnoString(output); + const post = decodeGnoPost(network.id, postData); + + return { + identifier: id, + parent_post_identifier: post.parentPostIdentifier, // identifier of linked post + category: post.category, // PostCategory + metadata: post.metadata, + reactions: post.reactions, + user_reactions: [], // TODO: What this for ? + post_by: post.createdBy, + deleted: post.isDeleted, + sub_post_length: 0, // TODO: handle this + }; + } else { + const client = await nonSigningSocialFeedClient({ + networkId, + }); + return await client.queryPost({ identifier: id }); + } } ); return { post: data, ...other }; diff --git a/packages/networks/gno-dev/index.ts b/packages/networks/gno-dev/index.ts index c1f2cbe84a..34327e490f 100644 --- a/packages/networks/gno-dev/index.ts +++ b/packages/networks/gno-dev/index.ts @@ -6,7 +6,7 @@ export const gnoDevNetwork: GnoNetworkInfo = { kind: NetworkKind.Gno, displayName: "Gno Dev", icon: "icons/networks/gno.svg", - features: [NetworkFeature.Organizations], + features: [NetworkFeature.Organizations, NetworkFeature.SocialFeed], currencies: gnoCurrencies, stakeCurrency: "ugnot", idPrefix: "gnodev", diff --git a/packages/networks/gno-teritori/index.ts b/packages/networks/gno-teritori/index.ts index d0a3b37135..681a92d868 100644 --- a/packages/networks/gno-teritori/index.ts +++ b/packages/networks/gno-teritori/index.ts @@ -6,7 +6,7 @@ export const gnoTeritoriNetwork: GnoNetworkInfo = { kind: NetworkKind.Gno, displayName: "Gno Teritori", icon: "icons/networks/gno.svg", - features: [NetworkFeature.Organizations], + features: [NetworkFeature.Organizations, NetworkFeature.SocialFeed], currencies: gnoCurrencies, stakeCurrency: "ugnot", idPrefix: "gnotori", diff --git a/packages/networks/index.ts b/packages/networks/index.ts index 63721b45dc..c1e53b56c7 100644 --- a/packages/networks/index.ts +++ b/packages/networks/index.ts @@ -28,6 +28,7 @@ import { teritoriNetwork } from "./teritori"; import { teritoriTestnetNetwork } from "./teritori-testnet"; import { CosmosNetworkInfo, + CurrencyInfo, EthereumNetworkInfo, GnoNetworkInfo, NativeCurrencyInfo, @@ -74,11 +75,11 @@ export const getToriNativeCurrency = (networkId: string) => { const network = getNetwork(networkId); if (network?.kind === NetworkKind.Cosmos) return network?.currencies.find( - (currencyInfo) => currencyInfo.kind === "native" + (currencyInfo: CurrencyInfo) => currencyInfo.kind === "native" ) as NativeCurrencyInfo; else { const toriIbcCurrency = network?.currencies.find( - (currencyInfo) => + (currencyInfo: CurrencyInfo) => currencyInfo.kind === "ibc" && currencyInfo.sourceDenom === "utori" ); return getNativeCurrency(networkId, toriIbcCurrency?.denom); diff --git a/packages/networks/teritori-testnet/index.ts b/packages/networks/teritori-testnet/index.ts index c5efb816f7..11e18e687c 100644 --- a/packages/networks/teritori-testnet/index.ts +++ b/packages/networks/teritori-testnet/index.ts @@ -13,7 +13,11 @@ export const teritoriTestnetNetwork: NetworkInfo = { chainId: "teritori-testnet-v3", displayName: "Teritori Testnet", icon: "icons/networks/teritori.svg", - features: [NetworkFeature.NFTMarketplace, NetworkFeature.Organizations], + features: [ + NetworkFeature.NFTMarketplace, + NetworkFeature.Organizations, + NetworkFeature.SocialFeed, + ], currencies: teritoriTestnetCurrencies, txExplorer: "https://explorer.teritori.com/teritori-testnet/tx/$hash", accountExplorer: diff --git a/packages/networks/teritori/index.ts b/packages/networks/teritori/index.ts index 814bf3e830..4eb370cedf 100644 --- a/packages/networks/teritori/index.ts +++ b/packages/networks/teritori/index.ts @@ -12,7 +12,11 @@ export const teritoriNetwork: CosmosNetworkInfo = { chainId: "teritori-1", displayName: "Teritori", icon: "icons/networks/teritori.svg", - features: [NetworkFeature.NFTMarketplace, NetworkFeature.Organizations], + features: [ + NetworkFeature.NFTMarketplace, + NetworkFeature.Organizations, + NetworkFeature.SocialFeed, + ], walletUrlForStaking: "https://explorer.teritori.com/teritori/staking", currencies: teritoriCurrencies, txExplorer: "https://www.mintscan.io/teritori/txs/$hash", diff --git a/packages/networks/types.ts b/packages/networks/types.ts index 0eda004191..5c54e439fe 100644 --- a/packages/networks/types.ts +++ b/packages/networks/types.ts @@ -128,4 +128,5 @@ export enum NetworkFeature { NameService = "NameService", Swap = "Swap", Organizations = "Organizations", + SocialFeed = "SocialFeed", } diff --git a/packages/screens/Feed/FeedScreen.tsx b/packages/screens/Feed/FeedScreen.tsx index 96ab2f7fbe..f34be1e60d 100644 --- a/packages/screens/Feed/FeedScreen.tsx +++ b/packages/screens/Feed/FeedScreen.tsx @@ -7,7 +7,7 @@ import { ScreenContainer } from "../../components/ScreenContainer"; import { MobileTitle } from "../../components/ScreenContainer/ScreenContainerMobile"; import { NewsFeed } from "../../components/socialFeed/NewsFeed/NewsFeed"; import { useIsMobile } from "../../hooks/useIsMobile"; -import { NetworkKind } from "../../networks"; +import { NetworkFeature } from "../../networks"; import { ScreenFC } from "../../utils/navigation"; import { feedTabToCategories, feedsTabItems } from "../../utils/social-feed"; @@ -36,7 +36,7 @@ export const FeedScreen: ScreenFC<"Feed"> = () => { noMargin noScroll footerChildren={<>} - forceNetworkKind={NetworkKind.Cosmos} + forceNetworkFeatures={[NetworkFeature.SocialFeed]} headerChildren={Social Feed} > = () => { const isMobile = useIsMobile(); - const selectedNetworkId = useSelectedNetworkId(); + const selectNetworkInfo = useSelectedNetworkInfo(); + const selectedNetworkId = selectNetworkInfo?.id || ""; const wallet = useSelectedWallet(); const { postFee } = useUpdatePostFee(selectedNetworkId, PostCategory.Article); const { freePostCount } = useUpdateAvailableFreePost( @@ -180,7 +181,7 @@ export const FeedNewArticleScreen: ScreenFC<"FeedNewArticle"> = () => { return ( = () => { : `The cost for this Article is ${prettyPrice( selectedNetworkId, postFee.toString(), - "utori" + selectNetworkInfo?.currencies?.[0].denom || "utori" )}`} diff --git a/packages/screens/FeedPostView/FeedPostViewScreen.tsx b/packages/screens/FeedPostView/FeedPostViewScreen.tsx index 7bddc76116..32c6f1920a 100644 --- a/packages/screens/FeedPostView/FeedPostViewScreen.tsx +++ b/packages/screens/FeedPostView/FeedPostViewScreen.tsx @@ -42,7 +42,7 @@ import { useIsMobile } from "../../hooks/useIsMobile"; import { useMaxResolution } from "../../hooks/useMaxResolution"; import { useNSUserInfo } from "../../hooks/useNSUserInfo"; import { useSelectedNetworkId } from "../../hooks/useSelectedNetwork"; -import { getUserId, NetworkKind, parseUserId } from "../../networks"; +import { NetworkFeature, getUserId, parseUserId } from "../../networks"; import { ScreenFC, useAppNavigation } from "../../utils/navigation"; import { DEFAULT_USERNAME, postResultToPost } from "../../utils/social-feed"; import { primaryColor } from "../../utils/style/colors"; @@ -164,7 +164,7 @@ export const FeedPostViewScreen: ScreenFC<"FeedPostView"> = ({ return ( { const atomCurrency = useMemo( () => selectedNetwork?.currencies.find( - (currencyInfo) => + (currencyInfo: CurrencyInfo) => getNativeCurrency(selectedNetworkId, currencyInfo?.denom)?.denom === cosmosNetwork?.stakeCurrency ), @@ -187,7 +187,7 @@ export const SwapView: React.FC = () => { const toriCurrency = useMemo( () => selectedNetwork?.currencies.find( - (currencyInfo) => + (currencyInfo: CurrencyInfo) => getNativeCurrency(selectedNetworkId, currencyInfo?.denom)?.denom === teritoriNetwork?.stakeCurrency ), @@ -222,7 +222,7 @@ export const SwapView: React.FC = () => { const selectableCurrencies = useMemo( () => selectedNetwork?.currencies.filter( - (currencyInfo) => + (currencyInfo: CurrencyInfo) => currencyIn?.denom !== currencyInfo.denom && currencyOut?.denom !== currencyInfo.denom && ((currencyInfo.kind === "ibc" && !currencyInfo.deprecated) ||