Skip to content

More SDK Mutations, mute, subscribe, unsubscribe, account update, delete comment#653

Open
feruzm wants to merge 1 commit intodevelopfrom
sdkmutation
Open

More SDK Mutations, mute, subscribe, unsubscribe, account update, delete comment#653
feruzm wants to merge 1 commit intodevelopfrom
sdkmutation

Conversation

@feruzm
Copy link
Member

@feruzm feruzm commented Feb 12, 2026

Summary by CodeRabbit

  • Refactor
    • Migrated comment deletion, post muting, community subscription, and profile update operations to SDK-based mutations for improved stability and consistency.
    • Added optimistic UI updates for comment deletion and profile modifications to provide faster user feedback.
    • Enhanced error messaging and success notifications with localization support across all updated operations.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 12, 2026

📝 Walkthrough

Walkthrough

This PR introduces SDK-backed mutation hooks across web and SDK layers. Web-specific mutation wrappers are created that delegate to new SDK mutations via broadcast adapters. SDK mutations are added for comment deletion, post muting, and community subscription/unsubscription with post-broadcast cache invalidation. Export barrels are extended to expose these mutations.

Changes

Cohort / File(s) Summary
Web Mutations → SDK
apps/web/src/api/mutations/delete-comment.ts, mute-post.ts, subscribe-to-community.ts, update-profile.ts
Refactored to use SDK mutations via wrappers instead of direct operations. Added optimistic cache updates (delete-comment), removed broadcast/invalidation logic (now SDK-handled), simplified mutation flow. Updated mutation keys and callbacks.
Web SDK Mutation Wrappers
apps/web/src/api/sdk-mutations/use-delete-comment-mutation.ts, use-mute-post-mutation.ts, use-subscribe-community-mutation.ts, use-unsubscribe-community-mutation.ts, use-update-profile-mutation.ts
New web-specific hooks that wrap SDK mutations, fetch active user context, create broadcast adapters, and delegate mutation execution. Provide integration between web auth flows and SDK mutations.
Web SDK Mutations Export
apps/web/src/api/sdk-mutations/index.ts
Added exports for five new mutation hooks to extend public API surface.
SDK Community Mutations
packages/sdk/src/modules/communities/mutations/use-subscribe-community.ts, use-unsubscribe-community.ts, use-mute-post.ts, index.ts
packages/sdk/src/modules/communities/index.ts
New mutation hooks with payload types for community operations. Include post-broadcast cache invalidation for user subscriptions and community data. Updated module exports to expose mutations.
SDK Post Mutations
packages/sdk/src/modules/posts/mutations/use-delete-comment.ts, index.ts
New hook for deleting posts/comments with payload type including optional parent/root metadata. Implements cache invalidation for feeds, blogs, parent entries, and discussions using auth adapter. Updated module exports.
SDK Account Mutations
packages/sdk/src/modules/accounts/mutations/use-account-update.ts
Updated to accept AuthContextV2 instead of AuthContext. Added optimistic cache updates for account data and post-broadcast cache invalidation via auth adapter for account/full queries.

Sequence Diagram(s)

sequenceDiagram
    participant WebComponent
    participant WebMutationWrapper as Web Mutation<br/>(useDeleteCommentMutation)
    participant SDKMutation as SDK Mutation<br/>(useDeleteComment)
    participant BroadcastAdapter
    participant QueryCache

    WebComponent->>WebMutationWrapper: mutate(payload)
    WebMutationWrapper->>WebMutationWrapper: getActiveUser()
    WebMutationWrapper->>WebMutationWrapper: createWebBroadcastAdapter()
    WebMutationWrapper->>SDKMutation: mutate(username, auth)
    SDKMutation->>BroadcastAdapter: buildDeleteCommentOp()
    SDKMutation->>BroadcastAdapter: broadcast(operation)
    BroadcastAdapter-->>SDKMutation: success
    SDKMutation->>QueryCache: invalidateQueries(feeds)
    SDKMutation->>QueryCache: invalidateQueries(discussions)
    SDKMutation->>QueryCache: invalidateQueries(parent)
    SDKMutation-->>WebMutationWrapper: result
    WebMutationWrapper->>WebComponent: onSuccess(callback)
Loading
sequenceDiagram
    participant WebComponent
    participant WebMutationWrapper as Web Mutation<br/>(updateProfile)
    participant SDKMutation as SDK Mutation<br/>(useAccountUpdate)
    participant BroadcastAdapter
    participant QueryCache

    WebComponent->>WebMutationWrapper: mutate(profile)
    WebMutationWrapper->>WebMutationWrapper: getActiveUser()
    WebMutationWrapper->>WebMutationWrapper: createWebBroadcastAdapter()
    WebMutationWrapper->>SDKMutation: mutate(username, auth)
    SDKMutation->>QueryCache: optimisticUpdate(FullAccount)
    SDKMutation->>BroadcastAdapter: buildAccountUpdateOp()
    SDKMutation->>BroadcastAdapter: broadcast(operation)
    BroadcastAdapter-->>SDKMutation: success
    SDKMutation->>QueryCache: invalidateQueries(accounts/full)
    SDKMutation-->>WebMutationWrapper: result
    WebMutationWrapper->>WebComponent: onSuccess()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • SDK improvements #650 — Introduces SDK mutation and broadcast/auth infrastructure changes that directly enable the mutation hooks and adapters used throughout this PR.
  • SDK mutation hooks and adapters #648 — Adds and exports the same SDK mutation hooks (useDeleteComment, useMutePost, useSubscribeCommunity, useUpdateProfile) that are wrapped and integrated in this PR.
  • Convert op, optimistic commenting fixes #613 — Modifies apps/web/src/api/mutations/delete-comment.ts with related optimistic cache and discussions invalidation logic.

Suggested labels

patch:sdk

Poem

🐰 Hops through mutations, hare does dare,
SDK wraps each web affair,
From cache to broadcast, adapters glare,
Comments delete, profiles laid bare!
Web and SDK, a perfect pair! 🎯

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately lists the main SDK mutations added (mute, subscribe, unsubscribe, account update, delete comment), directly reflecting the primary changes across multiple files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sdkmutation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/src/api/mutations/update-profile.ts (1)

51-71: ⚠️ Potential issue | 🔴 Critical

Cache key collision causes shallow merge to overwrite SDK's thorough profile update.

The SDK's useAccountUpdate (lines 99–117 of use-account-update.ts) performs an optimistic cache update using buildProfileMetadata() which:

  • Deep merges the profile with R.mergeDeep()
  • Sanitizes the tokens array (removes privateKey, username)
  • Sets version = 2

This wrapper performs a second update on the same query key ["get-account-full", account.name] using a shallow spread (...data.profile, ...profile), which overwrites the SDK's carefully constructed profile and loses:

  • Deep merge behavior for nested profile properties
  • Token sanitization (sensitive fields remain exposed)
  • Version field

Since both mutations target identical query keys, the web wrapper's onSuccess overwrites the SDK's update after it completes. Consider removing the manual cache update here and relying on the SDK's cache management, or coordinate the merge strategies to use deep merging and token sanitization consistently.

🤖 Fix all issues with AI agents
In `@apps/web/src/api/mutations/subscribe-to-community.ts`:
- Around line 38-43: The code inconsistently handles a possibly nullish
community: mutationKey uses community?.name while mutationFn accesses
community.name directly; to fix, make the handling consistent by either (A)
guarding at the top of the function (e.g., return or throw if community is
null/undefined) so you can safely use community.name in mutationKey and inside
mutationFn, or (B) keep optional chaining everywhere and bail out of
subscribeMutation.mutateAsync / unsubscribeMutation.mutateAsync when
community?.name is undefined; update references to mutationKey, mutationFn,
subscribeMutation.mutateAsync and unsubscribeMutation.mutateAsync to follow the
chosen approach.
- Around line 37-46: The mutationKey uses optional chaining (`community?.name`)
while the mutationFn and its calls (`subscribeMutation.mutateAsync`,
`unsubscribeMutation.mutateAsync`) use `community.name`; since the parameter is
a non-nullable Community, make access consistent by removing the optional
chaining in the useMutation call—change the key to use `community.name` so
`mutationKey` and `mutationFn` both reference the same non-null property.

In `@packages/sdk/src/modules/communities/mutations/use-unsubscribe-community.ts`:
- Around line 58-65: The invalidation keys embed the possibly undefined variable
username (used in the onSuccess callback where auth.adapter.invalidateQueries is
called), which can produce keys like ["accounts","subscriptions",undefined] and
fail to match cache entries; update the invalidate logic in
use-unsubscribe-community (and mirror the same fix in use-subscribe-community)
to only include the ["accounts","subscriptions",username] key when username is a
defined non-empty string (e.g., build an array of keys conditionally or push
that key only if username) before calling auth.adapter.invalidateQueries,
leaving the ["communities", variables.community] key intact; ensure you
reference the auth.adapter.invalidateQueries call and the username variable when
making the change.
🧹 Nitpick comments (7)
apps/web/src/api/sdk-mutations/use-update-profile-mutation.ts (1)

63-74: Passing username ?? "" triggers a wasteful query for a non-existent account.

When no user is logged in, username is undefined, and the fallback "" is passed to useAccountUpdate. Inside the SDK hook (Line 69 of use-account-update.ts), this fires useQuery(getAccountFullQueryOptions("")), sending a network request for an empty username. The mutation itself is guarded (throws if data is missing), but the query is still issued unnecessarily.

Consider passing undefined (and updating useAccountUpdate to accept string | undefined) or disabling the query when username is empty — consistent with how the other SDK mutations (useMutePost, useSubscribeCommunity, etc.) accept string | undefined.

packages/sdk/src/modules/accounts/mutations/use-account-update.ts (1)

63-65: username parameter type is inconsistent with sibling mutations.

useAccountUpdate takes username: string, while useMutePost, useSubscribeCommunity, and useUnsubscribeCommunity all accept string | undefined. This forces the web wrapper (use-update-profile-mutation.ts) to use the username ?? "" fallback, which triggers a wasteful query for an empty string (see Line 69).

Aligning the signature to username: string | undefined (and disabling the query when undefined) would make the API consistent and avoid the unnecessary network request.

Proposed signature change
 export function useAccountUpdate(
-  username: string,
+  username: string | undefined,
   auth?: AuthContextV2
 ) {
packages/sdk/src/modules/posts/mutations/use-delete-comment.ts (1)

82-117: Consider tightening the type of queriesToInvalidate.

The array mixes plain query key arrays with predicate objects ({ predicate: ... }). Using any[] works but loses type safety. A union type (e.g., Array<string[] | { predicate: (query: any) => boolean }>) would make the intent clearer and catch accidental misuse.

apps/web/src/api/sdk-mutations/use-unsubscribe-community-mutation.ts (1)

3-3: Unused type import: UnsubscribeCommunityPayload.

UnsubscribeCommunityPayload is imported but never referenced in this file. The same applies to SubscribeCommunityPayload in the subscribe wrapper and MutePostPayload/DeleteCommentPayload in their respective wrappers.

Proposed fix
-import { useUnsubscribeCommunity, type UnsubscribeCommunityPayload } from "@ecency/sdk";
+import { useUnsubscribeCommunity } from "@ecency/sdk";
apps/web/src/api/sdk-mutations/use-subscribe-community-mutation.ts (1)

3-3: Unused type import: SubscribeCommunityPayload.

Same as the unsubscribe wrapper — the type is imported but not used.

Proposed fix
-import { useSubscribeCommunity, type SubscribeCommunityPayload } from "@ecency/sdk";
+import { useSubscribeCommunity } from "@ecency/sdk";
apps/web/src/api/sdk-mutations/use-mute-post-mutation.ts (1)

3-3: Unused type import: MutePostPayload.

Proposed fix
-import { useMutePost, type MutePostPayload } from "@ecency/sdk";
+import { useMutePost } from "@ecency/sdk";
apps/web/src/api/sdk-mutations/use-delete-comment-mutation.ts (1)

3-3: Unused type import: DeleteCommentPayload.

Proposed fix
-import { useDeleteComment, type DeleteCommentPayload } from "@ecency/sdk";
+import { useDeleteComment } from "@ecency/sdk";

Comment on lines 37 to 46
return useMutation({
mutationKey: ["subscribeToCommunity", activeUser?.username, community?.name],
mutationKey: ["subscribeToCommunity", community?.name],
mutationFn: async ({ isSubscribe }: { isSubscribe: boolean }) => {
if (!activeUser) {
throw new Error("Can`t subscribe w/o active user");
}

if (isSubscribe) {
return [
isSubscribe,
await broadcastPostingJSON(activeUser?.username, "community", [
"subscribe",
{ community: community.name }
])
] as const;
await subscribeMutation.mutateAsync({ community: community.name });
} else {
return [
isSubscribe,
await broadcastPostingJSON(activeUser?.username, "community", [
"unsubscribe",
{ community: community.name }
])
] as const;
await unsubscribeMutation.mutateAsync({ community: community.name });
}
},
onSuccess: ([isSubscribe]) => {
queryClient.setQueryData<Subscription[] | undefined>(
["accounts", "subscriptions", activeUser?.username],
(data) => {
if (!data) {
return isSubscribe
? [[community.name, community.title, "guest", ""]]
: data;
}

return isSubscribe
? [...data, [community.name, community.title, "guest", ""]]
: data.filter(([u]) => u !== community.name);
}
);
queryClient.invalidateQueries({
queryKey: [QueryIdentifiers.COMMUNITY, community.name]
});
},
onError: (err) => error(...formatError(err))
}
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's confirm the file exists and examine its full content
cat -n apps/web/src/api/mutations/subscribe-to-community.ts

Repository: ecency/vision-next

Length of output: 1999


🏁 Script executed:

# Check how useSubscribeToCommunity is consumed
rg -n --type=ts --type=tsx -C3 'useSubscribeToCommunity' -g '!**/mutations/subscribe-to-community.ts'

Repository: ecency/vision-next

Length of output: 89


🏁 Script executed:

# Check React Query version to understand mutation patterns
fd package.json -x grep -l 'react-query\|@tanstack/react-query' {} \;

Repository: ecency/vision-next

Length of output: 177


🏁 Script executed:

# Search for callers without file type restriction
rg -n 'useSubscribeToCommunity' --type ts -C3 | grep -v 'subscribe-to-community.ts'

Repository: ecency/vision-next

Length of output: 1647


🏁 Script executed:

# Also search in all files more broadly
rg -n 'useSubscribeToCommunity' -C3 | head -100

Repository: ecency/vision-next

Length of output: 2858


🏁 Script executed:

# Check the React Query version in the main package.json
cat apps/web/package.json | grep -A2 -B2 '@tanstack/react-query'

Repository: ecency/vision-next

Length of output: 294


🏁 Script executed:

# Examine the caller component more thoroughly
cat -n apps/web/src/app/communities/_components/subscription-btn/index.tsx

Repository: ecency/vision-next

Length of output: 2357


🏁 Script executed:

# Check if Community type allows undefined/null
rg -n 'type Community|interface Community' --type ts -A5

Repository: ecency/vision-next

Length of output: 17134


🏁 Script executed:

# Search for any other potential callers more broadly
find . -name '*.ts' -o -name '*.tsx' | xargs grep -l 'useSubscribeToCommunity' 2>/dev/null | grep -v node_modules | head -20

Repository: ecency/vision-next

Length of output: 186


🏁 Script executed:

# Verify that Community cannot be undefined in actual usage
cat -n apps/web/src/entities/communities.ts | head -40

Repository: ecency/vision-next

Length of output: 1215


🏁 Script executed:

# Double-check if there are any other patterns where useSubscribeToCommunity receives undefined community
rg -B5 -A5 'useSubscribeToCommunity' apps/web/src/app/communities/_components/subscription-btn/index.tsx

Repository: ecency/vision-next

Length of output: 850


Fix optional chaining inconsistency in mutationKey and mutationFn.

Line 38 uses community?.name with optional chaining, but lines 41 and 43 access community.name directly. Since the community parameter is required (type Community is non-nullable), the optional chaining is unnecessary. Make the access consistent by removing the optional chaining operator on line 38: ["subscribeToCommunity", community.name].

🤖 Prompt for AI Agents
In `@apps/web/src/api/mutations/subscribe-to-community.ts` around lines 37 - 46,
The mutationKey uses optional chaining (`community?.name`) while the mutationFn
and its calls (`subscribeMutation.mutateAsync`,
`unsubscribeMutation.mutateAsync`) use `community.name`; since the parameter is
a non-nullable Community, make access consistent by removing the optional
chaining in the useMutation call—change the key to use `community.name` so
`mutationKey` and `mutationFn` both reference the same non-null property.

Comment on lines +38 to +43
mutationKey: ["subscribeToCommunity", community?.name],
mutationFn: async ({ isSubscribe }: { isSubscribe: boolean }) => {
if (!activeUser) {
throw new Error("Can`t subscribe w/o active user");
}

if (isSubscribe) {
return [
isSubscribe,
await broadcastPostingJSON(activeUser?.username, "community", [
"subscribe",
{ community: community.name }
])
] as const;
await subscribeMutation.mutateAsync({ community: community.name });
} else {
return [
isSubscribe,
await broadcastPostingJSON(activeUser?.username, "community", [
"unsubscribe",
{ community: community.name }
])
] as const;
await unsubscribeMutation.mutateAsync({ community: community.name });
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Inconsistent nullish handling of community.

Line 38 uses community?.name (optional chaining), implying community could be nullish. But lines 41 and 43 access community.name without optional chaining, which would throw a TypeError if community is indeed nullish.

Either guard consistently or drop the optional chaining on line 38 if community is guaranteed by the parameter type.

Proposed fix (drop optional chaining)
-    mutationKey: ["subscribeToCommunity", community?.name],
+    mutationKey: ["subscribeToCommunity", community.name],
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
mutationKey: ["subscribeToCommunity", community?.name],
mutationFn: async ({ isSubscribe }: { isSubscribe: boolean }) => {
if (!activeUser) {
throw new Error("Can`t subscribe w/o active user");
}
if (isSubscribe) {
return [
isSubscribe,
await broadcastPostingJSON(activeUser?.username, "community", [
"subscribe",
{ community: community.name }
])
] as const;
await subscribeMutation.mutateAsync({ community: community.name });
} else {
return [
isSubscribe,
await broadcastPostingJSON(activeUser?.username, "community", [
"unsubscribe",
{ community: community.name }
])
] as const;
await unsubscribeMutation.mutateAsync({ community: community.name });
mutationKey: ["subscribeToCommunity", community.name],
mutationFn: async ({ isSubscribe }: { isSubscribe: boolean }) => {
if (isSubscribe) {
await subscribeMutation.mutateAsync({ community: community.name });
} else {
await unsubscribeMutation.mutateAsync({ community: community.name });
🤖 Prompt for AI Agents
In `@apps/web/src/api/mutations/subscribe-to-community.ts` around lines 38 - 43,
The code inconsistently handles a possibly nullish community: mutationKey uses
community?.name while mutationFn accesses community.name directly; to fix, make
the handling consistent by either (A) guarding at the top of the function (e.g.,
return or throw if community is null/undefined) so you can safely use
community.name in mutationKey and inside mutationFn, or (B) keep optional
chaining everywhere and bail out of subscribeMutation.mutateAsync /
unsubscribeMutation.mutateAsync when community?.name is undefined; update
references to mutationKey, mutationFn, subscribeMutation.mutateAsync and
unsubscribeMutation.mutateAsync to follow the chosen approach.

Comment on lines +58 to +65
async (_result: any, variables) => {
// Cache invalidation
if (auth?.adapter?.invalidateQueries) {
await auth.adapter.invalidateQueries([
["accounts", "subscriptions", username],
["communities", variables.community]
]);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Cache invalidation key includes potentially undefined username.

On Line 62, username (typed string | undefined) is embedded directly in the invalidation key: ["accounts", "subscriptions", username]. If username is undefined at the time of the callback, the key becomes ["accounts", "subscriptions", undefined], which won't match the cache entry set with the actual username string, silently skipping invalidation.

The same issue exists in use-subscribe-community.ts (Line 62) and use-mute-post.ts does not have this issue since it doesn't embed username in its invalidation keys.

While broadcast likely requires an authenticated user (so username is defined in practice), adding a guard would be safer:

Proposed guard
     async (_result: any, variables) => {
       // Cache invalidation
-      if (auth?.adapter?.invalidateQueries) {
+      if (auth?.adapter?.invalidateQueries && username) {
         await auth.adapter.invalidateQueries([
           ["accounts", "subscriptions", username],
           ["communities", variables.community]
         ]);
       }
     },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async (_result: any, variables) => {
// Cache invalidation
if (auth?.adapter?.invalidateQueries) {
await auth.adapter.invalidateQueries([
["accounts", "subscriptions", username],
["communities", variables.community]
]);
}
async (_result: any, variables) => {
// Cache invalidation
if (auth?.adapter?.invalidateQueries && username) {
await auth.adapter.invalidateQueries([
["accounts", "subscriptions", username],
["communities", variables.community]
]);
}
🤖 Prompt for AI Agents
In `@packages/sdk/src/modules/communities/mutations/use-unsubscribe-community.ts`
around lines 58 - 65, The invalidation keys embed the possibly undefined
variable username (used in the onSuccess callback where
auth.adapter.invalidateQueries is called), which can produce keys like
["accounts","subscriptions",undefined] and fail to match cache entries; update
the invalidate logic in use-unsubscribe-community (and mirror the same fix in
use-subscribe-community) to only include the
["accounts","subscriptions",username] key when username is a defined non-empty
string (e.g., build an array of keys conditionally or push that key only if
username) before calling auth.adapter.invalidateQueries, leaving the
["communities", variables.community] key intact; ensure you reference the
auth.adapter.invalidateQueries call and the username variable when making the
change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant