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
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ export function usePostEdit(entry: Entry | undefined) {
parentPermlink: category,
title,
body: newBody,
jsonMetadata: jsonMeta
jsonMetadata: jsonMeta,
isUpdate: true
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
});

try {
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/app/submit/_api/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ export function useUpdateApi(onClear: () => void) {
parentPermlink: category,
title,
body: newBody,
jsonMetadata: jsonMeta
jsonMetadata: jsonMeta,
isUpdate: true
});

try {
Expand Down
6 changes: 6 additions & 0 deletions packages/sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 2.3.86

### Patch Changes

- fix(sdk): stop recording content activity for edits (#1491)

## 2.3.85

### Patch Changes
Expand Down
23 changes: 21 additions & 2 deletions packages/sdk/dist/browser/index.d.ts

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions packages/sdk/dist/browser/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/sdk/dist/browser/index.js.map

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions packages/sdk/dist/node/index.cjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/sdk/dist/node/index.cjs.map

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions packages/sdk/dist/node/index.mjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/sdk/dist/node/index.mjs.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@ecency/sdk",
"private": false,
"version": "2.3.85",
"version": "2.3.86",
"description": "Ecency SDK",
"repository": {
"type": "git",
Expand Down
93 changes: 93 additions & 0 deletions packages/sdk/src/modules/posts/mutations/use-comment.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { describe, it, expect, vi, beforeEach } from "vitest";

const mockUseBroadcastMutation = vi.hoisted(() => vi.fn());

vi.mock("@/modules/core", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/modules/core")>();
return {
...actual,
useBroadcastMutation: mockUseBroadcastMutation
};
});

import { resolveContentActivityType, useComment } from "./use-comment";

describe("resolveContentActivityType", () => {
it("earns post activity for a new top-level post", () => {
expect(resolveContentActivityType({ parentAuthor: "" })).toBe(100);
});

it("earns comment activity for a new reply", () => {
expect(resolveContentActivityType({ parentAuthor: "alice" })).toBe(110);
});

it("earns nothing when a post is edited", () => {
expect(resolveContentActivityType({ parentAuthor: "", isUpdate: true })).toBeNull();
});

it("earns nothing when a reply is edited", () => {
expect(resolveContentActivityType({ parentAuthor: "alice", isUpdate: true })).toBeNull();
});

it("still earns when isUpdate is explicitly false", () => {
expect(resolveContentActivityType({ parentAuthor: "", isUpdate: false })).toBe(100);
});
});

describe("useComment post-broadcast activity", () => {
const makeAdapter = () => ({
recordActivity: vi.fn().mockResolvedValue(undefined),
invalidateQueries: vi.fn().mockResolvedValue(undefined)
});

// useBroadcastMutation is called with the post-broadcast handler in position 3.
const runBroadcastHandler = async (adapter: any, variables: any) => {
mockUseBroadcastMutation.mockReturnValue({} as any);
useComment("alice", { adapter } as any);

const onBroadcast = mockUseBroadcastMutation.mock.calls[0][3];
await onBroadcast({ id: "tx-1", block_num: 42 }, variables);
};

const payload = {
author: "alice",
permlink: "a-post",
parentAuthor: "",
parentPermlink: "hive-125125",
title: "t",
body: "b",
jsonMetadata: {}
};

beforeEach(() => {
mockUseBroadcastMutation.mockReset();
});

it("records a post for a new top-level post", async () => {
const adapter = makeAdapter();
await runBroadcastHandler(adapter, payload);

expect(adapter.recordActivity).toHaveBeenCalledWith(100, "tx-1", 42);
});

it("records a comment for a new reply", async () => {
const adapter = makeAdapter();
await runBroadcastHandler(adapter, { ...payload, parentAuthor: "bob" });

expect(adapter.recordActivity).toHaveBeenCalledWith(110, "tx-1", 42);
});

it("records nothing when the payload is an update", async () => {
const adapter = makeAdapter();
await runBroadcastHandler(adapter, { ...payload, isUpdate: true });

expect(adapter.recordActivity).not.toHaveBeenCalled();
});

it("still invalidates caches for an update", async () => {
const adapter = makeAdapter();
await runBroadcastHandler(adapter, { ...payload, isUpdate: true });

expect(adapter.invalidateQueries).toHaveBeenCalled();
});
});
34 changes: 31 additions & 3 deletions packages/sdk/src/modules/posts/mutations/use-comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ export interface CommentPayload {
body: string;
/** JSON metadata object */
jsonMetadata: Record<string, any>;
/**
* Optional: set when this operation edits existing content rather than creating it.
*
* A `comment` operation is byte-identical for a create and an update, so only the
* caller knows which it is. When set, no content activity is recorded. Activity
* rewards content creation. Without this, an edit of content published elsewhere
* is credited as content created here. Never broadcast.
*/
isUpdate?: boolean;
/** Optional: Root post author (for nested replies, used for discussions cache invalidation) */
rootAuthor?: string;
/** Optional: Root post permlink (for nested replies, used for discussions cache invalidation) */
Expand Down Expand Up @@ -64,7 +73,8 @@ export interface CommentPayload {
*
* @remarks
* **Post-Broadcast Actions:**
* - Records activity (type 100 for posts, 110 for comments) if adapter.recordActivity is available
* - Records activity (type 100 for posts, 110 for comments) if adapter.recordActivity is
* available, unless the payload sets `isUpdate`
* - Invalidates feed caches to show the new content
* - Invalidates parent post cache if this is a reply
*
Expand Down Expand Up @@ -115,6 +125,24 @@ export interface CommentPayload {
* });
* ```
*/
/**
* Resolve which content activity a broadcast earns, or `null` for none.
*
* Content activity rewards publishing, so an update earns nothing: the `comment`
* operation an edit broadcasts is indistinguishable from a create on chain, which
* leaves the caller as the only party that can tell them apart. Without this, editing
* a post first published on another frontend is credited here as a post.
*/
export function resolveContentActivityType(
payload: Pick<CommentPayload, "parentAuthor" | "isUpdate">
): 100 | 110 | null {
if (payload.isUpdate) {
return null;
}

return payload.parentAuthor ? 110 : 100;
}

export function useComment(
username: string | undefined,
auth?: AuthContextV2,
Expand Down Expand Up @@ -187,13 +215,13 @@ export function useComment(
async (result: any, variables) => {
// Determine if this is a post or comment
const isPost = !variables.parentAuthor;
const activityType = isPost ? 100 : 110;
const activityType = resolveContentActivityType(variables);

// Activity tracking (fire-and-forget — non-critical, shouldn't block mutation completion)
// Async broadcasts return BroadcastResult ({ tx_id, status }) instead of TransactionConfirmation,
// so fall back to tx_id when id is absent.
const txId = result?.id ?? result?.tx_id;
if (auth?.adapter?.recordActivity && txId) {
if (activityType !== null && auth?.adapter?.recordActivity && txId) {
auth.adapter.recordActivity(activityType, txId, result?.block_num).catch(() => {});
}

Expand Down
57 changes: 57 additions & 0 deletions packages/sdk/src/modules/posts/mutations/use-update-reply.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { describe, it, expect, vi, beforeEach } from "vitest";

const mockUseBroadcastMutation = vi.hoisted(() => vi.fn());

vi.mock("@/modules/core", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/modules/core")>();
return {
...actual,
useBroadcastMutation: mockUseBroadcastMutation
};
});

import { useUpdateReply } from "./use-update-reply";

describe("useUpdateReply post-broadcast activity", () => {
const payload = {
author: "alice",
permlink: "re-bob-a-post",
parentAuthor: "bob",
parentPermlink: "a-post",
title: "",
body: "edited body",
jsonMetadata: {}
};

const makeAdapter = () => ({
recordActivity: vi.fn().mockResolvedValue(undefined),
invalidateQueries: vi.fn().mockResolvedValue(undefined)
});

// useBroadcastMutation is called with the post-broadcast handler in position 3.
const runBroadcastHandler = async (adapter: any) => {
mockUseBroadcastMutation.mockReturnValue({} as any);
useUpdateReply("alice", { adapter } as any);

const onBroadcast = mockUseBroadcastMutation.mock.calls[0][3];
await onBroadcast({ id: "tx-1", block_num: 42 }, payload);
};

beforeEach(() => {
mockUseBroadcastMutation.mockReset();
});

it("records no activity, because this mutation only ever edits existing content", async () => {
const adapter = makeAdapter();
await runBroadcastHandler(adapter);

expect(adapter.recordActivity).not.toHaveBeenCalled();
});

it("still invalidates caches", async () => {
const adapter = makeAdapter();
await runBroadcastHandler(adapter);

expect(adapter.invalidateQueries).toHaveBeenCalled();
});
});
16 changes: 2 additions & 14 deletions packages/sdk/src/modules/posts/mutations/use-update-reply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,20 +164,8 @@ export function useUpdateReply(
return operations;
},
async (_result: any, variables) => {
// Activity tracking (fire-and-forget — non-critical, shouldn't block mutation completion)
// Async broadcasts return BroadcastResult ({ tx_id, status }) instead of TransactionConfirmation,
// so fall back to tx_id when id is absent.
const txId = _result?.id ?? _result?.tx_id;
if (auth?.adapter?.recordActivity && txId) {
auth.adapter.recordActivity(110, txId, _result?.block_num).catch((error) => {
console.debug("[SDK][Posts][useUpdateReply] recordActivity failed", {
activityType: 110,
blockNum: _result?.block_num,
transactionId: txId,
error
});
});
}
// No activity is recorded here. Activity rewards creating content. Every
// broadcast from this mutation edits content that already exists.
Comment thread
qodo-code-review[bot] marked this conversation as resolved.

// Cache invalidation
if (auth?.adapter?.invalidateQueries) {
Expand Down
7 changes: 7 additions & 0 deletions packages/wallets/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# @ecency/wallets

## 5.0.86

### Patch Changes

- Updated dependencies []:
- @ecency/sdk@2.3.86

## 5.0.85

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/wallets/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@ecency/wallets",
"private": false,
"version": "5.0.85",
"version": "5.0.86",
"description": "Ecency wallets",
"repository": {
"type": "git",
Expand Down
Loading