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
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ useSubscribe({communityAddress: string}): {subscribed: boolean | undefined, subs
useBlock({address?: string, cid?: string}): {blocked: boolean | undefined, block: Function, unblock: Function}
useSaveComment({commentCid: string}): {saved: boolean | undefined, saveComment: Function, unsaveComment: Function}
usePublishComment(options: UsePublishCommentOptions): {index: number, abandonPublish: () => Promise<void>, ...UsePublishCommentResult}
usePublishVote(options: UsePublishVoteOptions): UsePublishVoteResult
usePublishVote(options: UsePublishVoteOptions): {abandonPublish: () => Promise<void>, ...UsePublishVoteResult}
usePublishCommentEdit(options: UsePublishCommentEditOptions): UsePublishCommentEditResult
usePublishCommentModeration(options: UsePublishCommentModerationOptions): UsePublishCommentModerationResult
usePublishCommunityEdit(options: UsePublishCommunityEditOptions): UsePublishCommunityEditResult
Expand Down Expand Up @@ -682,12 +682,17 @@ const publishVoteOptions = {
onChallengeVerification,
onError,
};
const { state, error, publishVote } = usePublishVote(publishVoteOptions);
const { state, error, publishVote, abandonPublish } = usePublishVote(publishVoteOptions);

await publishVote();
console.log(state);
console.log(error);

// if the user closes the challenge modal and wants to cancel voting:
await abandonPublish();
// the vote publication is stopped and the account vote goes back to what it was
// before publishVote(), so the optimistic vote does not survive a cancelled challenge

// display the user's vote
const { vote } = useAccountVote({ commentCid });

Expand Down
18 changes: 16 additions & 2 deletions llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ useSubscribe({communityAddress: string}): {subscribed: boolean | undefined, subs
useBlock({address?: string, cid?: string}): {blocked: boolean | undefined, block: Function, unblock: Function}
useSaveComment({commentCid: string}): {saved: boolean | undefined, saveComment: Function, unsaveComment: Function}
usePublishComment(options: UsePublishCommentOptions): {index: number, abandonPublish: () => Promise<void>, ...UsePublishCommentResult}
usePublishVote(options: UsePublishVoteOptions): UsePublishVoteResult
usePublishVote(options: UsePublishVoteOptions): {abandonPublish: () => Promise<void>, ...UsePublishVoteResult}
usePublishCommentEdit(options: UsePublishCommentEditOptions): UsePublishCommentEditResult
usePublishCommentModeration(options: UsePublishCommentModerationOptions): UsePublishCommentModerationResult
usePublishCommunityEdit(options: UsePublishCommunityEditOptions): UsePublishCommunityEditResult
Expand Down Expand Up @@ -712,12 +712,17 @@ const publishVoteOptions = {
onChallengeVerification,
onError,
};
const { state, error, publishVote } = usePublishVote(publishVoteOptions);
const { state, error, publishVote, abandonPublish } = usePublishVote(publishVoteOptions);

await publishVote();
console.log(state);
console.log(error);

// if the user closes the challenge modal and wants to cancel voting:
await abandonPublish();
// the vote publication is stopped and the account vote goes back to what it was
// before publishVote(), so the optimistic vote does not survive a cancelled challenge

// display the user's vote
const { vote } = useAccountVote({ commentCid });

Expand Down Expand Up @@ -3282,6 +3287,15 @@ Avoid GitHub MCP and browser MCP servers for this project because they add signi
Source: https://github.com/bitsocialnet/bitsocial-react-hooks/blob/master/CHANGELOG.md

```markdown
## [0.1.43](https://github.com/bitsocialnet/bitsocial-react-hooks/compare/v0.1.42...v0.1.43) (2026-08-29)


### Bug Fixes

* **comments:** preserve published author identity ([#109](https://github.com/bitsocialnet/bitsocial-react-hooks/issues/109)) ([a3bb5c6](https://github.com/bitsocialnet/bitsocial-react-hooks/commit/a3bb5c697f38eec45e4629ddebe7d5d11456036d))



## [0.1.42](https://github.com/bitsocialnet/bitsocial-react-hooks/compare/v0.1.41...v0.1.42) (2026-08-28)


Expand Down
2 changes: 1 addition & 1 deletion llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,5 @@ This file is generated by `scripts/generate-llms-files.mjs`. Do not hand-edit it

## Optional

- [Changelog](https://github.com/bitsocialnet/bitsocial-react-hooks/blob/master/CHANGELOG.md): * **accounts:** add saved comment support ([#106](https://github.com/bitsocialnet/bitsocial-react-hooks/issues/106)) ([661d720](https://github.com/bitsocialnet/bitsocial-react-hooks/commit/661d72031cc39076b2263013160a...
- [Changelog](https://github.com/bitsocialnet/bitsocial-react-hooks/blob/master/CHANGELOG.md): * **comments:** preserve published author identity ([#109](https://github.com/bitsocialnet/bitsocial-react-hooks/issues/109)) ([a3bb5c6](https://github.com/bitsocialnet/bitsocial-react-hooks/commit/a3bb5c697f38eec45e4...
- [TODO](https://github.com/bitsocialnet/bitsocial-react-hooks/blob/master/docs/TODO.md): - e2e test to publish to an electron sub - async useAuthorAddress hook (because resolving ETH address synchronously is too slow) - implement sort by active - implement showing your own pending replies in a comment (wh...
69 changes: 69 additions & 0 deletions src/hooks/actions/actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2147,6 +2147,75 @@ describe("actions", () => {
await testUtils.resetDatabasesAndStores();
});

test(`abandonPublish reverts the account vote and clears the challenge`, async () => {
const publishVoteOptions = {
communityAddress: "12D3KooW... acions.test",
commentCid: "Qm... abandon.test",
vote: 1,
// never answer the challenge so the vote stays abandonable
onChallenge: vi.fn(),
onChallengeVerification: vi.fn(),
};
rendered.rerender(publishVoteOptions);
await waitFor(() => rendered.result.current.state === "ready");

await act(async () => {
await rendered.result.current.publishVote();
});
await waitFor(() => rendered.result.current.challenge !== undefined);
expect(rendered.result.current.accountVote.vote).toBe(1);

await act(async () => {
await rendered.result.current.abandonPublish();
});

await waitFor(() => rendered.result.current.accountVote.vote === 0);
expect(rendered.result.current.accountVote.vote).toBe(0);
expect(rendered.result.current.challenge).toBe(undefined);
// the "stopped" publishing state emitted by the stopped publication is ignored
expect(rendered.result.current.state).toBe("ready");
expect(publishVoteOptions.onChallengeVerification).not.toHaveBeenCalled();
});

test(`abandoned vote's late events do not leak into the next publish`, async () => {
const votes: any[] = [];
const publishVoteOptions = {
communityAddress: "12D3KooW... acions.test",
commentCid: "Qm... leak.test",
vote: 1,
onChallenge: (_challenge: any, vote: any) => votes.push(vote),
onChallengeVerification: vi.fn(),
};
rendered.rerender(publishVoteOptions);
await waitFor(() => rendered.result.current.state === "ready");

await act(async () => {
await rendered.result.current.publishVote();
});
await waitFor(() => rendered.result.current.challenge !== undefined);
await act(async () => {
await rendered.result.current.abandonPublish();
});
await act(async () => {
await rendered.result.current.publishVote();
});
await waitFor(() => rendered.result.current.challenge !== undefined);
expect(votes.length).toBe(2);
expect(rendered.result.current.state).toBe("waiting-challenge-answers");

// a late event from the abandoned publication must not touch the new publication's state
await act(async () => {
votes[0].emit("publishingstatechange", "stopped");
});
expect(rendered.result.current.state).toBe("waiting-challenge-answers");
});

test(`abandonPublish without a commentCid does not throw`, async () => {
rendered.rerender({ communityAddress: "12D3KooW... acions.test", vote: 1 });
await waitFor(() => rendered.result.current.state === "ready");
await expect(rendered.result.current.abandonPublish()).resolves.toBeUndefined();
});

test(`publishChallengeAnswers throws when challenge not yet received`, async () => {
const publishVoteOptions = {
communityAddress: "12D3KooW... acions.test",
Expand Down
34 changes: 33 additions & 1 deletion src/hooks/actions/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,11 @@ export function usePublishVote(options?: UsePublishVoteOptions): UsePublishVoteR
const [challenge, setChallenge] = useState<Challenge>();
const [challengeVerification, setChallengeVerification] = useState<ChallengeVerification>();
const [publishChallengeAnswers, setPublishChallengeAnswers] = useState<PublishChallengeAnswers>();
// each publishVote() call gets its own request id and abandonPublish() clears the active one, so
// the stopped publication's late events (like the "stopped" publishing state) neither overwrite
// the cleared hook state nor leak into the next publication
const publishVoteRequestIdRef = useRef(0);
const activePublishVoteRequestIdRef = useRef<number | undefined>(undefined);

let initialState = "initializing";
// before the accountId and options is defined, nothing can happen
Expand Down Expand Up @@ -492,18 +497,45 @@ export function usePublishVote(options?: UsePublishVoteOptions): UsePublishVoteR
};

const publishVote = async () => {
const requestId = publishVoteRequestIdRef.current + 1;
publishVoteRequestIdRef.current = requestId;
activePublishVoteRequestIdRef.current = requestId;
const isActiveRequest = () => activePublishVoteRequestIdRef.current === requestId;
const activePublishVoteOptions = {
...publishVoteOptions,
onChallenge: (challenge: Challenge, vote: Vote) => {
if (isActiveRequest()) onChallenge(challenge, vote);
},
onChallengeVerification: (challengeVerification: ChallengeVerification, vote: Vote) => {
if (isActiveRequest()) onChallengeVerification(challengeVerification, vote);
},
onPublishingStateChange: (publishingState: string) => {
if (isActiveRequest()) setPublishingState(publishingState);
},
};
try {
await accountsActions.publishVote(publishVoteOptions, accountName);
await accountsActions.publishVote(activePublishVoteOptions, accountName);
} catch (e: any) {
handlePublishVoteError(e, setErrors, originalOnError);
}
};

const abandonPublish = async () => {
activePublishVoteRequestIdRef.current = undefined;
setChallenge(undefined);
setChallengeVerification(undefined);
setPublishChallengeAnswers(undefined);
setPublishingState(undefined);
Comment on lines +523 to +528

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear publishing state after stop callbacks

When abandoning an active vote whose stop() emits publishingstatechange, this reset runs before accountsActions.abandonVote() stops the publication, and the listener at accounts-actions.ts:1839 immediately writes "stopped" back into the hook. The repository mock does exactly this synchronously in src/lib/pkc-js/pkc-js-mock.ts:596-601, so the final hook state is "stopped" rather than the cleared/ready state promised by this method; clear after stopping or ignore callbacks from abandoned sessions.

Useful? React with 👍 / 👎.

if (!publishVoteOptions.commentCid) return;
await accountsActions.abandonVote(publishVoteOptions.commentCid, accountName);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
};
Comment thread
cursor[bot] marked this conversation as resolved.

return useMemo(
() => ({
challenge,
challengeVerification,
publishVote,
abandonPublish,
publishChallengeAnswers: publishChallengeAnswers || publishChallengeAnswersNotReady,
state: publishingState || initialState,
error: errors[errors.length - 1],
Expand Down
Loading
Loading