Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 28 additions & 74 deletions apps/web/src/app/publish/_api/use-publish.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,22 @@
import { validatePostCreating } from "@ecency/sdk";
import { enforceThreeSpeakBeneficiary } from "@/api/threespeak-embed";
import { linkThreeSpeakEmbed } from "@/api/threespeak-embed/link-after-broadcast";
import {
collectPresentMemeAttribution,
DECENTMEMES_FRONTEND,
enforceDecentMemesBeneficiary,
ensureDecentMemesTag
} from "@/api/decentmemes";
import { useCommentMutation, useReblogMutation } from "@/api/sdk-mutations";
import { EcencyEntriesCacheManagement } from "@/core/caches";
import { getQueryClient } from "@/core/react-query";
import { getAccountFullQueryOptions, getPostHeaderQueryOptions } from "@ecency/sdk";
import { FullAccount, RewardType } from "@/entities";
import { EntryBodyManagement, EntryMetadataManagement } from "@/features/entry-management";
import { FullAccount } from "@/entities";
import { PollSnapshot } from "@/features/polls";
import { QueryKeys, type Poll } from "@ecency/sdk";
import { info, success } from "@/features/shared";
import {
createPermlink,
isCommunity,
makeCommentOptions,
tempEntry
} from "@/utils";
import { createPermlink, isCommunity, tempEntry } from "@/utils";
import { scheduleQuestsRefresh } from "@/utils/refresh-quests";
import { postBodySummary } from "@ecency/render-helper";
import { EcencyAnalytics } from "@ecency/sdk";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import i18next from "i18next";
import { usePublishState } from "../_hooks";
import { sentry } from "@/core/sentry/lazy-sentry";
import { SUBMIT_DESCRIPTION_MAX_LENGTH } from "@/app/submit/_consts";
import { useActiveAccount } from "@/core/hooks";
import { buildPublishOperation } from "../_utils/build-publish-operation";

export function usePublishApi() {
const queryClient = useQueryClient();
Expand Down Expand Up @@ -63,14 +49,6 @@ export function usePublishApi() {
return useMutation({
mutationKey: ["publish-2.0"],
mutationFn: async () => {
let cleanBody = EntryBodyManagement.EntryBodyManager.shared
.builder()
.buildClearBody(content!);

cleanBody = EntryBodyManagement.EntryBodyManager.shared
.builder()
.withLocation(cleanBody, location);

// Ensure user is logged in and account data is available
if (!username) {
throw new Error("[Publish] No active user");
Expand Down Expand Up @@ -123,62 +101,39 @@ export function usePublishApi() {
throw new Error("[Publish] Failed to generate unique permlink after multiple attempts");
}

const [parentPermlink] = tags!;

// DecentMemes: reconcile tracked memes against the final body so a meme
// whose image was deleted no longer contributes a tag / metadata / beneficiary.
const memeAttribution = collectPresentMemeAttribution(decentMemes, cleanBody);
const hasMeme = memeAttribution.templateIds.length > 0;
const finalTags = hasMeme ? ensureDecentMemesTag(tags ?? []) : tags;

const metaBuilder = await EntryMetadataManagement.EntryMetadataManager.shared
.builder()
.default()
.extractFromBody(content!)
// It should select filled description or if its empty or null/undefined then get auto summary
.withSummary(
metaDescription || postBodySummary(cleanBody, SUBMIT_DESCRIPTION_MAX_LENGTH)
)
.withTags(finalTags)
.withPostLinks(postLinks)
.withLocation(location)
.withSelectedThumbnail(selectedThumbnail);
const jsonMeta = metaBuilder
.withPoll(poll)
.withDecentMemes(
hasMeme
? { templateIds: memeAttribution.templateIds, frontend: DECENTMEMES_FRONTEND }
: undefined
)
.withAiTools(aiTools)
.build();
// Same assembly the RC pre-check prices, so the estimate the author saw
// describes the transaction actually broadcast here.
const { op, jsonMetadata, summary, options, beneficiariesDropped } = await buildPublishOperation({
author,
permlink,
title: title!,
content: content!,
tags: tags!,
metaDescription,
selectedThumbnail,
reward,
beneficiaries,
poll,
postLinks,
location,
decentMemes,
aiTools
});
const parentPermlink = op.parent_permlink;
const cleanBody = op.body;

let finalBeneficiaries = enforceThreeSpeakBeneficiary(beneficiaries, cleanBody);
if (hasMeme) {
// Merge the widget-supplied meme beneficiaries on our own terms: never
// trust its numbers - cap to Hive's 8-slot / 100% limits and keep the
// user's own beneficiaries intact.
const enforced = enforceDecentMemesBeneficiary(
finalBeneficiaries,
memeAttribution.beneficiaries,
author
);
finalBeneficiaries = enforced.beneficiaries;
if (enforced.dropped) {
info(i18next.t("decentmemes.beneficiaries-trimmed"));
}
if (beneficiariesDropped) {
info(i18next.t("decentmemes.beneficiaries-trimmed"));
}

const options = makeCommentOptions(author, permlink, reward as RewardType, finalBeneficiaries);

await commentMutation({
author,
permlink,
parentAuthor: "",
parentPermlink,
title: title!,
body: cleanBody,
jsonMetadata: jsonMeta,
jsonMetadata,
...(options
? {
options: {
Expand All @@ -204,9 +159,8 @@ export function usePublishApi() {
body: content!,

tags: tags!,
description:
metaDescription || postBodySummary(cleanBody, SUBMIT_DESCRIPTION_MAX_LENGTH),
jsonMeta
description: summary,
jsonMeta: jsonMetadata
}),
max_accepted_payout: options?.max_accepted_payout ?? "1000000.000 HBD",
percent_hbd: options?.percent_hbd ?? 10000
Expand Down
84 changes: 80 additions & 4 deletions apps/web/src/app/publish/_components/publish-validate-post.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@ import { PublishActionBarCommunity } from "./publish-action-bar-community";
import { PublishScheduleDialog } from "./publish-schedule-dialog";
import { PublishValidatePostMeta } from "./publish-validate-post-meta";
import { PublishValidatePostThumbnailPicker } from "./publish-validate-post-thumbnail-picker";
import { isCommunity } from "@/utils";
import { createPermlink, isCommunity } from "@/utils";
import { useQuery } from "@tanstack/react-query";
import { buildPublishOperation } from "../_utils/build-publish-operation";
import { wordOverlapSimilarity } from "@/utils/text-similarity";
import { hasPublishContent } from "../_utils/content";
import { useActiveAccount } from "@/core/hooks/use-active-account";
import { AvailableCredits } from "@/features/shared";
import { RcPrecheckBanner } from "@/features/shared/rc-precheck";
import type { RcPrecheckPayload } from "@ecency/sdk";
import {
canFitBeneficiary,
isSupportEcencyRow,
Expand Down Expand Up @@ -53,7 +56,14 @@ export function PublishValidatePost({ onClose, onSuccess }: Props) {
beneficiaries,
setBeneficiaries,
title,
appliedTemplateBody
appliedTemplateBody,
reward,
selectedThumbnail,
poll,
postLinks,
location,
decentMemes,
aiTools
} = usePublishState();

const { activeUser } = useActiveAccount();
Expand Down Expand Up @@ -110,6 +120,68 @@ export function PublishValidatePost({ onClose, onSuccess }: Props) {
);
}, [activeUser?.username]);

// The operation this publish will actually broadcast, assembled by the same
// builder usePublishApi uses. Pricing the raw editor draft instead would
// understate a post carrying a summary, images, links, a poll or
// beneficiaries, since cost tracks serialized size, and understating is the
// one direction that lets the chain reject a post we called affordable.
const { data: publishOperation } = useQuery({
queryKey: [
"publish-rc-operation",
activeUser?.username,
title,
content,
tags,
metaDescription,
selectedThumbnail,
reward,
beneficiaries,
poll,
postLinks,
location,
decentMemes,
aiTools
],
enabled: !!activeUser?.username && !!content,
// Pure function of the draft above, so a given key never needs recomputing.
staleTime: Infinity,
queryFn: () =>
buildPublishOperation({
author: activeUser!.username,
// The real permlink is settled at broadcast time after the collision
// check; only its length feeds the estimate, so use a same-shape one.
permlink: createPermlink(title ?? ""),
title: title ?? "",
content: content ?? "",
tags: tags ?? [],
metaDescription,
selectedThumbnail,
reward,
beneficiaries,
poll,
postLinks,
location,
decentMemes,
aiTools
})
});

const rcPayload = useMemo<RcPrecheckPayload | undefined>(
() =>
publishOperation
? {
kind: "comment",
op: publishOperation.op,
options: publishOperation.options
? {
beneficiaries: publishOperation.options.extensions?.[0]?.[1]?.beneficiaries
}
: undefined
}
: undefined,
[publishOperation]
);

const submit = useCallback(async () => {
if (!title?.trim()) {
feedbackError(i18next.t("submit.empty-title-alert"));
Expand Down Expand Up @@ -313,8 +385,12 @@ export function PublishValidatePost({ onClose, onSuccess }: Props) {

{activeUser?.username && (
<div className="w-full flex flex-col gap-2">
<RcPrecheckBanner operation="comment_operation" />
<AvailableCredits username={activeUser.username} operation="comment_operation" />
<RcPrecheckBanner operation="comment_operation" payload={rcPayload} />
<AvailableCredits
username={activeUser.username}
operation="comment_operation"
payload={rcPayload}
/>
</div>
)}

Expand Down
Loading