diff --git a/src/common/api/mutations.ts b/src/common/api/mutations/account-claiming.ts similarity index 56% rename from src/common/api/mutations.ts rename to src/common/api/mutations/account-claiming.ts index 5733eb948a1..6422a564317 100644 --- a/src/common/api/mutations.ts +++ b/src/common/api/mutations/account-claiming.ts @@ -1,21 +1,7 @@ +import { FullAccount } from "../../store/accounts/types"; import { useMutation } from "@tanstack/react-query"; -import { usrActivity } from "./private-api"; -import { claimAccount, claimAccountByKeychain } from "./operations"; -import { FullAccount } from "../store/accounts/types"; import { PrivateKey } from "@hiveio/dhive"; - -interface Params { - bl?: string | number; - tx?: string | number; -} - -export function useUserActivity(username: string | undefined, ty: number) { - return useMutation(["user-activity", username, ty], async (params: Params | undefined) => { - if (username) { - await usrActivity(username, ty, params?.bl, params?.tx); - } - }); -} +import { claimAccount, claimAccountByKeychain } from "../operations"; export function useAccountClaiming(account: FullAccount) { return useMutation( diff --git a/src/common/api/mutations/create-reply.ts b/src/common/api/mutations/create-reply.ts new file mode 100644 index 00000000000..dadc3dd09e6 --- /dev/null +++ b/src/common/api/mutations/create-reply.ts @@ -0,0 +1,94 @@ +import { Entry } from "../../store/entries/types"; +import { useMappedStore } from "../../store/use-mapped-store"; +import { useContext } from "react"; +import { EntriesCacheContext, QueryIdentifiers } from "../../core"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { comment, CommentOptions, formatError, MetaData } from "../operations"; +import tempEntry from "../../helper/temp-entry"; +import { FullAccount } from "../../store/accounts/types"; +import * as ss from "../../util/session-storage"; +import { error } from "../../components/feedback"; + +export function useCreateReply(entry: Entry | null, parent?: Entry, onSuccess?: () => void) { + const { activeUser } = useMappedStore(); + const { addReply, updateRepliesCount, updateCache } = useContext(EntriesCacheContext); + const queryClient = useQueryClient(); + + return useMutation( + ["reply-create", activeUser?.username, entry?.author, entry?.permlink], + async ({ + permlink, + text, + jsonMeta, + options, + point + }: { + permlink: string; + text: string; + jsonMeta: MetaData; + point: boolean; + options?: CommentOptions; + }) => { + if (!activeUser || !activeUser.data.__loaded || !entry) { + throw new Error("[Reply][Create] – no active user provided"); + } + + await comment( + activeUser.username, + entry.author, + entry.permlink, + permlink, + "", + text, + jsonMeta, + options ?? null, + point + ); + return tempEntry({ + author: activeUser.data as FullAccount, + permlink, + parentAuthor: entry.author, + parentPermlink: entry.permlink, + title: "", + body: text, + tags: [], + description: null + }); + }, + { + onSuccess: (data) => { + if (!entry) { + return; + } + + addReply(entry, data); + updateCache([data]); + + // remove reply draft + ss.remove(`reply_draft_${entry.author}_${entry.permlink}`); + + if (entry.children === 0) { + // Update parent comment. + updateRepliesCount(entry, 1); + } + const previousReplies = + queryClient.getQueryData([ + QueryIdentifiers.FETCH_DISCUSSIONS, + parent?.author ?? entry.author, + parent?.permlink ?? entry.permlink + ]) ?? []; + queryClient.setQueryData( + [ + QueryIdentifiers.FETCH_DISCUSSIONS, + parent?.author ?? entry.author, + parent?.permlink ?? entry.permlink + ], + [data, ...previousReplies] + ); + + onSuccess?.(); + }, + onError: (e) => error(...formatError(e)) + } + ); +} diff --git a/src/common/api/mutations/index.ts b/src/common/api/mutations/index.ts new file mode 100644 index 00000000000..b7ded518e31 --- /dev/null +++ b/src/common/api/mutations/index.ts @@ -0,0 +1,5 @@ +export * from "./account-claiming"; +export * from "./create-reply"; +export * from "./update-reply"; +export * from "./user-activity"; +export * from "./pin-reply"; diff --git a/src/common/api/mutations/pin-reply.ts b/src/common/api/mutations/pin-reply.ts new file mode 100644 index 00000000000..aee0f3e5945 --- /dev/null +++ b/src/common/api/mutations/pin-reply.ts @@ -0,0 +1,26 @@ +import { useMutation } from "@tanstack/react-query"; +import { Entry } from "../../store/entries/types"; +import { createPatch, makeJsonMetaDataReply } from "../../helper/posting"; +import { version } from "../../../../package.json"; +import { useUpdateReply } from "./update-reply"; +import { MetaData } from "../operations"; + +export function usePinReply(reply: Entry, parent: Entry) { + const { mutateAsync: updateReply } = useUpdateReply(parent); + + return useMutation(["reply-pin", reply, parent], async ({ pin }: { pin: boolean }) => { + const meta = makeJsonMetaDataReply( + parent.json_metadata.tags || ["ecency"], + version + ) as MetaData; + + let newBody = parent.body.replace(/[\x00-\x09\x0B-\x0C\x0E-\x1F\x7F-\x9F]/g, ""); + const patch = createPatch(parent.body, newBody.trim()); + if (patch && patch.length < Buffer.from(parent.body, "utf-8").length) { + newBody = patch; + } + + meta.pinned_reply = pin ? `${reply.author}/${reply.permlink}` : undefined; + return updateReply({ text: newBody, point: true, jsonMeta: meta }); + }); +} diff --git a/src/common/api/mutations/update-reply.ts b/src/common/api/mutations/update-reply.ts new file mode 100644 index 00000000000..a5d68086eb6 --- /dev/null +++ b/src/common/api/mutations/update-reply.ts @@ -0,0 +1,63 @@ +import { Entry } from "../../store/entries/types"; +import { useMappedStore } from "../../store/use-mapped-store"; +import { useContext } from "react"; +import { EntriesCacheContext } from "../../core"; +import { useMutation } from "@tanstack/react-query"; +import { comment, CommentOptions, formatError, MetaData } from "../operations"; +import * as ss from "../../util/session-storage"; +import { error } from "../../components/feedback"; + +export function useUpdateReply(entry: Entry | null, onSuccess?: () => void) { + const { activeUser } = useMappedStore(); + const { updateCache } = useContext(EntriesCacheContext); + + return useMutation( + ["reply-update", activeUser?.username, entry?.author, entry?.permlink], + async ({ + text, + jsonMeta, + options, + point + }: { + text: string; + jsonMeta: MetaData; + point: boolean; + options?: CommentOptions; + }) => { + if (!activeUser || !activeUser.data.__loaded || !entry) { + throw new Error("[Reply][Create] – no active user provided"); + } + + await comment( + activeUser.username, + entry.parent_author ?? "", + entry.parent_permlink ?? entry.category, + entry.permlink, + "", + text, + jsonMeta, + options ?? null, + point + ); + return { + ...entry, + json_metadata: jsonMeta, + body: text + }; + }, + { + onSuccess: (data) => { + if (!entry) { + return; + } + + updateCache([data]); + + // remove reply draft + ss.remove(`reply_draft_${entry.author}_${entry.permlink}`); + onSuccess?.(); + }, + onError: (e) => error(...formatError(e)) + } + ); +} diff --git a/src/common/api/mutations/user-activity.ts b/src/common/api/mutations/user-activity.ts new file mode 100644 index 00000000000..6e47ea33347 --- /dev/null +++ b/src/common/api/mutations/user-activity.ts @@ -0,0 +1,15 @@ +import { useMutation } from "@tanstack/react-query"; +import { usrActivity } from "../private-api"; + +interface Params { + bl?: string | number; + tx?: string | number; +} + +export function useUserActivity(username: string | undefined, ty: number) { + return useMutation(["user-activity", username, ty], async (params: Params | undefined) => { + if (username) { + await usrActivity(username, ty, params?.bl, params?.tx); + } + }); +} diff --git a/src/common/api/operations.ts b/src/common/api/operations.ts index ffb93c5d002..82236d1dac1 100644 --- a/src/common/api/operations.ts +++ b/src/common/api/operations.ts @@ -41,6 +41,7 @@ export interface MetaData { description?: string; video?: any; type?: string; + pinned_reply?: string; // author/permlink } export interface BeneficiaryRoute { diff --git a/src/common/api/queries.ts b/src/common/api/queries.ts index a3062356030..f255e081377 100644 --- a/src/common/api/queries.ts +++ b/src/common/api/queries.ts @@ -1,11 +1,15 @@ -import { useQueries, useQuery } from "@tanstack/react-query"; -import { QueryIdentifiers } from "../core"; +import { useQueries, useQuery, UseQueryOptions } from "@tanstack/react-query"; +import { EntriesCacheContext, QueryIdentifiers } from "../core"; import { getPoints, getPointTransactions } from "./private-api"; import { useMappedStore } from "../store/use-mapped-store"; import axios from "axios"; import { catchPostImage } from "@ecency/render-helper"; import { Entry } from "../store/entries/types"; -import { getAccountFull } from "./hive"; +import { getAccountFull, getFollowing } from "./hive"; +import { getAccountPosts, getDiscussion } from "./bridge"; +import { SortOrder } from "../store/discussion/types"; +import { useContext } from "react"; +import { sortDiscussions } from "../util/sort-discussions"; const DEFAULT = { points: "0.000", @@ -105,3 +109,54 @@ export function useGetAccountsFullQuery(usernames: string[]) { })) }); } + +export function useGetAccountPostsQuery(username?: string) { + return useQuery({ + queryKey: [QueryIdentifiers.GET_POSTS, username], + queryFn: () => getAccountPosts("posts", username!).then((response) => response ?? []), + enabled: !!username, + initialData: [] + }); +} + +export function useFetchDiscussionsQuery( + entry: Entry, + order: SortOrder, + queryOptions?: UseQueryOptions +) { + const { updateCache } = useContext(EntriesCacheContext); + + return useQuery( + [QueryIdentifiers.FETCH_DISCUSSIONS, entry?.author, entry?.permlink], + async () => { + const response = await getDiscussion(entry.author, entry.permlink); + if (response) { + const entries = Array.from(Object.values(response)); + updateCache([...entries], true); + return entries; + } + return []; + }, + { + ...queryOptions, + initialData: [], + select: (data) => sortDiscussions(entry, data, order) + } + ); +} + +export function useFetchMutedUsersQuery(username?: string) { + const { activeUser } = useMappedStore(); + + return useQuery( + [QueryIdentifiers.FETCH_MUTED_USERS, username ?? activeUser?.username ?? "anon"], + async () => { + const response = await getFollowing(username ?? activeUser!!.username, "", "ignore", 100); + return response.map((user) => user.following); + }, + { + initialData: [], + enabled: !!username || !!activeUser + } + ); +} diff --git a/src/common/app.tsx b/src/common/app.tsx index 552dfdbeb13..03c6c6b7bd0 100644 --- a/src/common/app.tsx +++ b/src/common/app.tsx @@ -30,12 +30,13 @@ import { UserActivityRecorder } from "./components/user-activity-recorder"; import { useGlobalLoader } from "./util/use-global-loader"; import useMount from "react-use/lib/useMount"; import { ChatPopUp } from "./features/chats/components/chat-popup"; -import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { ChatContextProvider } from "@ecency/ns-query"; import { useGetAccountFullQuery } from "./api/queries"; +import { UIManager } from "@ui/core"; import defaults from "./constants/defaults.json"; import { getAccessToken } from "./helper/user-token"; + // Define lazy pages const ProfileContainer = loadable(() => import("./pages/profile-functional")); const ProfilePage = (props: any) => ; @@ -113,116 +114,132 @@ const App = (props: any) => { ); return ( - - {/*Excluded from production*/} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -