Repository: Southclaws/storyden
Pull request #385: [#216] Add thread likes on the feed
Author: login: humeman
Created: 2025-04-09T14:19:10Z | Merged: 2025-04-21T09:52:20Z

To resolve issue #216.

This PR adds a like button to threads on the feed, to the left of the collections menu. To accomplish this, it adds `likePost` and `unlikePost` to feed mutations, which mutates the current state and sends the request to like/unlike before eventually revalidating. 

I assume some eventual work would also be to put this in the thread view itself somewhere -- we can also add that now if it makes sense to do it all at once.

Some related questions that might also be worth considering: 
- Is useFeedMutations a good place for the like/unlike methods or is it cleaner as its own file? This does mutate the feed thread list's `liked` flag, but doesn't seem totally related other than the fact that it takes place on the feed view. 
- Is there a better approach than the mutation doing a full thread list, given that it will only update one thread, or is that insignificant here?

Happy to hear any feedback or improvements you would want made, or if you want to go another direction with how these work as a whole. Appreciate it :+1: 


Diff:
diff --git a/web/src/components/post/LikeButton/LikeButton.tsx b/web/src/components/post/LikeButton/LikeButton.tsx
new file mode 100644
index 000000000..0e23cdaba
--- /dev/null
+++ b/web/src/components/post/LikeButton/LikeButton.tsx
@@ -0,0 +1,22 @@
+import { Box } from "@/styled-system/jsx";
+
+import { Props, useLikeButton } from "./useLikeButton";
+import { LikeAction } from "@/components/site/Action/Like";
+
+export function LikeButton(props: Props) {
+
+  const { handleClick } = useLikeButton(
+    {thread: props.thread},
+  );
+
+  return (
+    <Box>
+      <LikeAction
+        variant="subtle"
+        size="xs"
+        liked={props.thread.likes.liked}
+        onClick={handleClick}
+      />
+    </Box>
+  );
+}
\ No newline at end of file
diff --git a/web/src/components/post/LikeButton/useLikeButton.ts b/web/src/components/post/LikeButton/useLikeButton.ts
new file mode 100644
index 000000000..8fb1cbf46
--- /dev/null
+++ b/web/src/components/post/LikeButton/useLikeButton.ts
@@ -0,0 +1,35 @@
+import { PostReference } from "src/api/openapi-schema";
+
+import { handle } from "@/api/client";
+import { useFeedMutations } from "@/lib/feed/mutation";
+
+export type Props = {
+ thread: PostReference;
+};
+
+export function useLikeButton({ thread }: Props) {
+  const { likePost, unlikePost, revalidate } = useFeedMutations();
+
+  const handleClick = async () => {
+    await handle(
+      async () => {
+        // This is a toggle button, so we'll either like or unlike depending on what they have now.
+        if (thread.likes.liked) {
+          await unlikePost(thread.id);
+        } else {
+          await likePost(thread.id);
+        }
+      },
+      {
+        async cleanup() {
+          await revalidate();
+        },
+      },
+    );
+  }
+
+ return {
+   ready: true as const,
+   handleClick,
+ };
+}
diff --git a/web/src/components/post/ThreadCard.tsx b/web/src/components/post/ThreadCard.tsx
index c7464d092..2a1c5adae 100644
--- a/web/src/components/post/ThreadCard.tsx
+++ b/web/src/components/post/ThreadCard.tsx
@@ -11,6 +11,7 @@ import { HStack, styled } from "@/styled-system/jsx";
 import { getAssetURL } from "@/utils/asset";
 
 import { ThreadMenu } from "../thread/ThreadMenu/ThreadMenu";
+import { LikeButton } from "./LikeButton/LikeButton";
 import {
   DiscussionIcon,
   DiscussionParticipatingIcon,
@@ -48,6 +49,7 @@ export const ThreadReferenceCard = memo(({ thread }: Props) => {
       controls={
         session && (
           <HStack>
+            <LikeButton thread={thread} />
             <CollectionMenu account={session} thread={thread} />
             <ThreadMenu thread={thread} />
           </HStack>
diff --git a/web/src/components/site/Action/Like.tsx b/web/src/components/site/Action/Like.tsx
new file mode 100644
index 000000000..a76150377
--- /dev/null
+++ b/web/src/components/site/Action/Like.tsx
@@ -0,0 +1,23 @@
+import { ButtonProps } from "@/components/ui/button";
+import { IconButton } from "@/components/ui/icon-button";
+import {
+  LikeIcon,
+  LikeSavedIcon,
+} from "@/components/ui/icons/Like";
+
+type Props = ButtonProps & { liked: boolean };
+
+export function LikeAction(props: Props) {
+  const { liked, ...rest } = props;
+  return (
+    <IconButton 
+      variant="subtle" 
+      size="xs" 
+      aria-pressed={liked} 
+      aria-label={liked ? "Unlike" : "Like"}
+      title={liked ? "Unlike" : "Like"}
+      {...rest}>
+      {liked ? <LikeSavedIcon /> : <LikeIcon />}
+    </IconButton>
+  );
+}
diff --git a/web/src/lib/feed/mutation.ts b/web/src/lib/feed/mutation.ts
index a747951ce..c95c27059 100644
--- a/web/src/lib/feed/mutation.ts
+++ b/web/src/lib/feed/mutation.ts
@@ -16,6 +16,7 @@ import {
   ThreadListParams,
   ThreadReference,
 } from "@/api/openapi-schema";
+import { likePostAdd, likePostRemove } from "@/api/openapi-client/likes";
 
 export function useFeedMutations(session?: Account, params?: ThreadListParams) {
   const { mutate } = useSWRConfig();
@@ -134,9 +135,71 @@ export function useFeedMutations(session?: Account, params?: ThreadListParams) {
     await threadDelete(id);
   }
 
+  async function likePost(id: Identifier) {
+    const mutator: MutatorCallback<ThreadListOKResponse> = (data) => {
+      if (!data) return;
+
+      const newThreads = data.threads.map((thread) => {
+        if (thread.id === id) {
+          return {
+            ...thread,
+            likes: {
+              likes: thread.likes.likes + 1,
+              liked: true,
+            },
+          };
+        }
+        return thread;
+      });
+
+      return {
+        ...data,
+        threads: newThreads,
+      };
+    }
+
+    await mutate(threadListKeyFilterFn, mutator, {
+      revalidate: false,
+    });
+
+    await likePostAdd(id);
+  }
+
+  async function unlikePost(id: Identifier) {
+    const mutator: MutatorCallback<ThreadListOKResponse> = (data) => {
+      if (!data) return;
+
+      const newThreads = data.threads.map((thread) => {
+        if (thread.id === id) {
+          return {
+            ...thread,
+            likes: {
+              likes: thread.likes.likes - 1,
+              liked: false,
+            },
+          };
+        }
+        return thread;
+      });
+
+      return {
+        ...data,
+        threads: newThreads,
+      };
+    }
+
+    await mutate(threadListKeyFilterFn, mutator, {
+      revalidate: false,
+    });
+
+    await likePostRemove(id);
+  }
+
   return {
     createThread,
     deleteThread,
+    likePost,
+    unlikePost,
     revalidate,
   };
 }


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (6)</summary><blockquote>

<details>
<summary>web/src/lib/feed/mutation.ts (1)</summary><blockquote>

`138-166`: **Good implementation of optimistic updates for like functionality.**

The `likePost` function follows the pattern of optimistically updating the local cache before making the API call, which provides a responsive user experience. The implementation handles the necessary state updates correctly.


Consider adding error handling to gracefully handle API failures and potentially revert the optimistic update if needed:

```diff
async function likePost(id: Identifier) {
  // ... existing code ...

-  await likePostAdd(id);
+  try {
+    await likePostAdd(id);
+  } catch (error) {
+    // Revert optimistic update on failure
+    await mutate(threadListKeyFilterFn);
+    throw error;
+  }
}
```

</blockquote></details>
<details>
<summary>web/src/components/site/Action/Like.tsx (1)</summary><blockquote>

`13-21`: **Consider enhancing with loading state support.**

Since the like/unlike actions involve asynchronous API calls, it might be helpful to support a loading state to prevent users from clicking multiple times while an action is in progress.


Enhance the component with loading state support:

```diff
type Props = ButtonProps & { 
  liked: boolean;
+ isLoading?: boolean;
};

export function LikeAction(props: Props) {
- const { liked, ...rest } = props;
+ const { liked, isLoading, ...rest } = props;
  return (
    <IconButton 
      variant="subtle" 
      size="xs" 
      aria-pressed={liked} 
      aria-label={liked ? "Unlike" : "Like"}
      title={liked ? "Unlike" : "Like"}
+     loading={isLoading}
      {...rest}>
      {liked ? <LikeSavedIcon /> : <LikeIcon />}
    </IconButton>
  );
}
```

</blockquote></details>
<details>
<summary>web/src/components/post/LikeButton/LikeButton.tsx (1)</summary><blockquote>

`8-10`: **Simplify the hook argument.**

The useLikeButton hook is being called with an object that contains a thread property, but the argument could be simplified by directly passing the thread.


```diff
- const { handleClick } = useLikeButton(
-   {thread: props.thread},
- );
+ const { handleClick } = useLikeButton(props.thread);
```

This would require updating the useLikeButton hook signature accordingly.

</blockquote></details>
<details>
<summary>web/src/components/post/LikeButton/useLikeButton.ts (3)</summary><blockquote>

`1-5`: **Import path consistency could be improved.**

I notice that you're using two different import path styles: `src/api/openapi-schema` and `@/api/client`. For better maintainability, consider standardizing to one approach throughout the codebase.

```diff
-import { PostReference } from "src/api/openapi-schema";
+import { PostReference } from "@/api/openapi-schema";
```

---

`13-29`: **Consider adding loading state management.**

While the `handle` utility likely manages errors, the hook doesn't expose any loading state that could be used to disable the button or show loading indicators during the API call. This could lead to a poor UX if users click multiple times while a request is in progress.

```diff
export function useLikeButton({ thread }: Props) {
  const { likePost, unlikePost, revalidate } = useFeedMutations();
+ const [isLoading, setIsLoading] = useState(false);

  const handleClick = async () => {
+   if (isLoading) return;
+   setIsLoading(true);
    await handle(
      async () => {
        // This is a toggle button, so we'll either like or unlike depending on what they have now.
        if (thread.likes.liked) {
          await unlikePost(thread.id);
        } else {
          await likePost(thread.id);
        }
      },
      {
        async cleanup() {
          await revalidate();
+         setIsLoading(false);
        },
      },
    );
  }

 return {
   ready: true as const,
   handleClick,
+  isLoading,
 };
}
```

---

`31-35`: **Consider simplifying the return object.**

The `ready: true as const` property seems unnecessary if it's always true. Unless it's part of a required interface pattern used throughout the codebase, consider removing it or explaining its purpose with a comment.

```diff
 return {
-  ready: true as const,
   handleClick,
 };
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 49a97cf761a9b6d092bba57528a385b0d8e2bfe2 and 3a4b89c0d148978a2e64e86c7847386625e850cb.

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `web/src/components/post/LikeButton/LikeButton.tsx` (1 hunks)
* `web/src/components/post/LikeButton/useLikeButton.ts` (1 hunks)
* `web/src/components/post/ThreadCard.tsx` (2 hunks)
* `web/src/components/site/Action/Like.tsx` (1 hunks)
* `web/src/lib/feed/mutation.ts` (2 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (2)</summary>

<details>
<summary>web/src/components/post/ThreadCard.tsx (1)</summary><blockquote>

<details>
<summary>web/src/components/post/LikeButton/LikeButton.tsx (1)</summary>

* `LikeButton` (6-22)

</details>

</blockquote></details>
<details>
<summary>web/src/components/site/Action/Like.tsx (2)</summary><blockquote>

<details>
<summary>web/src/components/ui/icon-button.tsx (1)</summary>

* `IconButton` (24-44)

</details>
<details>
<summary>web/src/components/ui/icons/Like.tsx (2)</summary>

* `LikeSavedIcon` (5-7)
* `LikeIcon` (8-8)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (4)</summary><blockquote>

<details>
<summary>web/src/components/post/ThreadCard.tsx (1)</summary>

`52-52`: **Good integration of the LikeButton component.**

The LikeButton is correctly integrated in the controls section and conditionally rendered when a user session exists, matching the pattern used for other control buttons.

</details>
<details>
<summary>web/src/components/site/Action/Like.tsx (1)</summary>

`8-23`: **Well-implemented accessible action button with appropriate toggle states.**

The LikeAction component appropriately uses aria attributes to enhance accessibility, and the conditional rendering of different icons based on the liked state provides clear visual feedback to users.

</details>
<details>
<summary>web/src/components/post/LikeButton/LikeButton.tsx (1)</summary>

`6-22`: **Clean implementation with good separation of concerns.**

The component uses a custom hook for interaction logic while keeping the presentation simple. This separation of concerns makes the code more maintainable.

</details>
<details>
<summary>web/src/components/post/LikeButton/useLikeButton.ts (1)</summary>

`10-12`: **The hook implementation looks good!**

The hook correctly destructures the necessary mutation functions from `useFeedMutations` to handle like/unlike operations. This provides a clean abstraction for the like button functionality.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-16T20:31:06Z
user: login: Southclaws
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-21T09:46:32Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-21T09:48:07Z

Review comments:
diff_hunk: @@ -134,9 +135,71 @@ export function useFeedMutations(session?: Account, params?: ThreadListParams) {
     await threadDelete(id);
   }
 
+  async function likePost(id: Identifier) {
+    const mutator: MutatorCallback<ThreadListOKResponse> = (data) => {
+      if (!data) return;
+
+      const newThreads = data.threads.map((thread) => {
+        if (thread.id === id) {
+          return {
+            ...thread,
+            likes: {
+              likes: thread.likes.likes + 1,
+              liked: true,
+            },
+          };
+        }
+        return thread;
+      });
+
+      return {
+        ...data,
+        threads: newThreads,
+      };
+    }
+
+    await mutate(threadListKeyFilterFn, mutator, {
+      revalidate: false,
+    });
+
+    await likePostAdd(id);
+  }
+
+  async function unlikePost(id: Identifier) {
+    const mutator: MutatorCallback<ThreadListOKResponse> = (data) => {
+      if (!data) return;
+
+      const newThreads = data.threads.map((thread) => {
+        if (thread.id === id) {
+          return {
+            ...thread,
+            likes: {
+              likes: thread.likes.likes - 1,
+              liked: false,
+            },
+          };
+        }
+        return thread;
+      });
+
+      return {
+        ...data,
+        threads: newThreads,
+      };
+    }
+
+    await mutate(threadListKeyFilterFn, mutator, {
+      revalidate: false,
+    });
+
+    await likePostRemove(id);
+  }
path: web/src/lib/feed/mutation.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Prevent potential negative like counts.**

The `unlikePost` function decrements the like count without checking if it's already at 0, which could theoretically lead to negative counts if the UI allows unliking a post that wasn't previously liked.


Add a check to prevent negative like counts:

```diff
const newThreads = data.threads.map((thread) => {
  if (thread.id === id) {
    return {
      ...thread,
      likes: {
-       likes: thread.likes.likes - 1,
+       likes: Math.max(0, thread.likes.likes - 1),
        liked: false,
      },
    };
  }
  return thread;
});
```

Also consider adding error handling similar to the suggestion for the `likePost` function.


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
  async function unlikePost(id: Identifier) {
    const mutator: MutatorCallback<ThreadListOKResponse> = (data) => {
      if (!data) return;

      const newThreads = data.threads.map((thread) => {
        if (thread.id === id) {
          return {
            ...thread,
            likes: {
-             likes: thread.likes.likes - 1,
+             likes: Math.max(0, thread.likes.likes - 1),
              liked: false,
            },
          };
        }
        return thread;
      });

      return {
        ...data,
        threads: newThreads,
      };
    }

    await mutate(threadListKeyFilterFn, mutator, {
      revalidate: false,
    });

    await likePostRemove(id);
  }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-16T20:31:04Z
author_association: NONE
diff_hunk: @@ -0,0 +1,35 @@
+import { PostReference } from "src/api/openapi-schema";
+
+import { handle } from "@/api/client";
+import { useFeedMutations } from "@/lib/feed/mutation";
+
+export type Props = {
+ thread: PostReference;
+};
+
+export function useLikeButton({ thread }: Props) {
+  const { likePost, unlikePost, revalidate } = useFeedMutations();
+
+  const handleClick = async () => {
+    await handle(
+      async () => {
+        // This is a toggle button, so we'll either like or unlike depending on what they have now.
+        if (thread.likes.liked) {
+          await unlikePost(thread.id);
+        } else {
+          await likePost(thread.id);
+        }
path: web/src/components/post/LikeButton/useLikeButton.ts
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Verify optimistic updates are happening.**

The code correctly toggles between like and unlike based on the current state, but there's no visible implementation of optimistic UI updates. Make sure that the `likePost` and `unlikePost` functions from `useFeedMutations` are handling optimistic updates to provide immediate feedback to users before the API response is received.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if likePost and unlikePost in useFeedMutations implement optimistic updates

echo "Checking useFeedMutations implementation..."
rg -A 20 'likePost|unlikePost' --glob '*.ts' web/src/lib/feed/mutation.ts
```

Length of output: 2122

---



</details>

**Missing optimistic UI updates in `likePost`/`unlikePost`**

The mutator callbacks are defined but never applied via `mutate()`, so no optimistic state change actually occurs. You’ll need to invoke `mutate(threadQueryMutationKey, mutator, false)` before (or immediately after) calling the API to reflect the like/unlike in the UI optimistically.

• Location  
  - `web/src/lib/feed/mutation.ts`  
    - Functions: `likePost(id: Identifier)` and `unlikePost(id: Identifier)`

• Suggested change  
```diff
async function likePost(id: Identifier) {
  const mutator: MutatorCallback<ThreadListOKResponse> = (data) => {
    if (!data) return
    const newThreads = data.threads.map((thread) =>
      thread.id === id
        ? { ...thread, likes: { likes: thread.likes.likes + 1, liked: true } }
        : thread
    )
    return { ...data, threads: newThreads }
  }
+ // Apply optimistic update
+ mutate(threadQueryMutationKey, mutator, false)
  await likePostAdd(id)
}

async function unlikePost(id: Identifier) {
  const mutator: MutatorCallback<ThreadListOKResponse> = (data) => {
    if (!data) return
    const newThreads = data.threads.map((thread) =>
      thread.id === id
        ? { ...thread, likes: { likes: thread.likes.likes - 1, liked: false } }
        : thread
    )
    return { ...data, threads: newThreads }
  }
+ // Apply optimistic update
+ mutate(threadQueryMutationKey, mutator, false)
  await likePostRemove(id)
}
```



> Committable suggestion skipped: line range outside the PR's diff.

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-16T20:31:04Z
author_association: NONE
diff_hunk: @@ -0,0 +1,35 @@
+import { PostReference } from "src/api/openapi-schema";
+
+import { handle } from "@/api/client";
+import { useFeedMutations } from "@/lib/feed/mutation";
+
+export type Props = {
+ thread: PostReference;
+};
+
+export function useLikeButton({ thread }: Props) {
+  const { likePost, unlikePost, revalidate } = useFeedMutations();
+
+  const handleClick = async () => {
+    await handle(
+      async () => {
+        // This is a toggle button, so we'll either like or unlike depending on what they have now.
+        if (thread.likes.liked) {
+          await unlikePost(thread.id);
+        } else {
+          await likePost(thread.id);
+        }
path: web/src/components/post/LikeButton/useLikeButton.ts
user: login: Southclaws
body: Already handled by likePost/unlikePost
created_at: 2025-04-21T09:46:32Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,35 @@
+import { PostReference } from "src/api/openapi-schema";
+
+import { handle } from "@/api/client";
+import { useFeedMutations } from "@/lib/feed/mutation";
+
+export type Props = {
+ thread: PostReference;
+};
+
+export function useLikeButton({ thread }: Props) {
+  const { likePost, unlikePost, revalidate } = useFeedMutations();
+
+  const handleClick = async () => {
+    await handle(
+      async () => {
+        // This is a toggle button, so we'll either like or unlike depending on what they have now.
+        if (thread.likes.liked) {
+          await unlikePost(thread.id);
+        } else {
+          await likePost(thread.id);
+        }
path: web/src/components/post/LikeButton/useLikeButton.ts
user: login: coderabbitai[bot]
body: <details>
<summary>🧩 Analysis chain</summary>

🏁 Script executed:
```
#!/bin/bash
# Check the implementation of likePost and unlikePost in the mutation.ts file
cat web/src/lib/feed/mutation.ts
```

Length of output: 4953

---



</details>

@Southclaws You're absolutely right! Looking at the implementation of `likePost` and `unlikePost` in `web/src/lib/feed/mutation.ts`, I can see that optimistic updates are indeed already properly handled:

```javascript
async function likePost(id: Identifier) {
  const mutator: MutatorCallback<ThreadListOKResponse> = (data) => {
    // ... Creates updated thread list with increased like count and liked=true
  }

  await mutate(threadListKeyFilterFn, mutator, {
    revalidate: false,
  });

  await likePostAdd(id);
}
```

The code already follows the optimistic update pattern by calling `mutate()` with the mutator function before making the API call, exactly as it should be. My apologies for the confusion in the previous review.

---

<details>
<summary>🧬 Code Graph Analysis Results</summary>

### Relevant snippet from `web/src/api/client.ts` lines 38-86:
```ts
export async function handle<T>(
  fn: () => Promise<T>,
  args?: HandleArgs<T>,
): Promise<T | undefined> {
  const {
    onError,
    cleanup,
    promiseToast,
    errorToast = true,
    action,
  } = args ?? {};

  if (promiseToast) {
    return new Promise((resolve, reject) => {
      toast.promise<T>(
        async () => {
          return await fn();
        },
        {
          loading: promiseToast.loading,
          success: (data: T) => {
            resolve(data);
            return promiseToast.success;
          },
          error: (error: unknown) => {
            reject(error);
            return deriveError(error);
          },
          finally: cleanup,
          action: action,
        },
      );
    });
  }

  try {
    return await fn();
  } catch (error) {
    await onError?.(error);

    if (errorToast) {
      toast.error(deriveError(error));
    }
  } finally {
    await cleanup?.();
  }

  return;
}
```
- This function wraps an async operation with error handling, optional toast notifications, and a cleanup callback. It is used in `useLikeButton` to handle the like/unlike toggle operation.

---

### Relevant snippet from `web/src/lib/feed/mutation.ts` lines 21-205 (summary):
```ts
export function useFeedMutations(session?: Account, params?: ThreadListParams) {
  // Provides mutation functions for thread feed operations with optimistic UI updates and cache mutation.

  // Revalidates the thread list cache.
  async function revalidate() { ... }

  // Creates a new thread with optimistic update.
  async function createThread(initial: ThreadInitialProps, preHydratedLink?: LinkReference) { ... }

  // Deletes a thread with optimistic update.
  async function deleteThread(id: Identifier) { ... }

  // Likes a post with optimistic UI update:
  // - Updates local cache to increment likes count and mark liked true.
  // - Calls likePostAdd API.
  async function likePost(id: Identifier) { ... }

  // Unlikes a post with optimistic UI update:
  // - Updates local cache to decrement likes count and mark liked false.
  // - Calls likePostRemove API.
  async function unlikePost(id: Identifier) { ... }

  return {
    createThread,
    deleteThread,
    likePost,
    unlikePost,
    revalidate,
  };
}
```
- This hook provides the `likePost` and `unlikePost` functions used in `useLikeButton`.
- Both functions perform optimistic UI updates by mutating the cached thread list before calling the respective API endpoints.
- The `revalidate` function refreshes the thread list cache after mutations.

---

### Relevant snippet from `app/transports/http/openapi/server_gen.go` lines 2170-2213 (summary):
```go
PostReference struct {
  Assets AssetList `json:"assets"`
  Author ProfileReference `json:"author"`
  Collections CollectionStatus `json:"collections"`
  CreatedAt time.Time `json:"createdAt"`
  DeletedAt *time.Time `json:"deletedAt,omitempty"`
  Description *PostDescription `json:"description,omitempty"`
  Id Identifier `json:"id"`
  Likes LikeData `json:"likes"`
  Meta *Metadata `json:"meta,omitempty"`
  Misc *map[string]interface{} `json:"misc,omitempty"`
  Reacts ReactList `json:"reacts"`
  Slug ThreadMark `json:"slug"`
  Title ThreadTitle `json:"title"`
  UpdatedAt time.Time `json:"updatedAt"`
}
```
- Defines the `PostReference` type used in `useLikeButton` props.
- Includes the `Likes` field which contains like count and liked status, relevant for toggling like state.

---

These snippets provide context on how the like/unlike toggle is implemented, how errors and UI updates are handled, and the data structure of the post object involved.

</details>



---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: Southclaws
PR: Southclaws/storyden#385
File: web/src/components/post/LikeButton/useLikeButton.ts:17-21
Timestamp: 2025-04-21T09:48:06.361Z
Learning: In Storyden, optimistic UI updates for like/unlike actions are handled by the `likePost` and `unlikePost` functions in `web/src/lib/feed/mutation.ts`, which update the local cache with `mutate()` before making the actual API calls.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>




<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer><details>
<summary>🧩 Analysis chain</summary>

🏁 Script executed:
```
#!/bin/bash
# Check the implementation of likePost and unlikePost in the mutation.ts file
cat web/src/lib/feed/mutation.ts
```

Length of output: 4953

---



</details>

@Southclaws You're absolutely right! Looking at the implementation of `likePost` and `unlikePost` in `web/src/lib/feed/mutation.ts`, I can see that optimistic updates are indeed already properly handled:

```javascript
async function likePost(id: Identifier) {
  const mutator: MutatorCallback<ThreadListOKResponse> = (data) => {
    // ... Creates updated thread list with increased like count and liked=true
  }

  await mutate(threadListKeyFilterFn, mutator, {
    revalidate: false,
  });

  await likePostAdd(id);
}
```

The code already follows the optimistic update pattern by calling `mutate()` with the mutator function before making the API call, exactly as it should be. My apologies for the confusion in the previous review.

---

<details>
<summary>🧬 Code Graph Analysis Results</summary>

### Relevant snippet from `web/src/api/client.ts` lines 38-86:
```ts
export async function handle<T>(
  fn: () => Promise<T>,
  args?: HandleArgs<T>,
): Promise<T | undefined> {
  const {
    onError,
    cleanup,
    promiseToast,
    errorToast = true,
    action,
  } = args ?? {};

  if (promiseToast) {
    return new Promise((resolve, reject) => {
      toast.promise<T>(
        async () => {
          return await fn();
        },
        {
          loading: promiseToast.loading,
          success: (data: T) => {
            resolve(data);
            return promiseToast.success;
          },
          error: (error: unknown) => {
            reject(error);
            return deriveError(error);
          },
          finally: cleanup,
          action: action,
        },
      );
    });
  }

  try {
    return await fn();
  } catch (error) {
    await onError?.(error);

    if (errorToast) {
      toast.error(deriveError(error));
    }
  } finally {
    await cleanup?.();
  }

  return;
}
```
- This function wraps an async operation with error handling, optional toast notifications, and a cleanup callback. It is used in `useLikeButton` to handle the like/unlike toggle operation.

---

### Relevant snippet from `web/src/lib/feed/mutation.ts` lines 21-205 (summary):
```ts
export function useFeedMutations(session?: Account, params?: ThreadListParams) {
  // Provides mutation functions for thread feed operations with optimistic UI updates and cache mutation.

  // Revalidates the thread list cache.
  async function revalidate() { ... }

  // Creates a new thread with optimistic update.
  async function createThread(initial: ThreadInitialProps, preHydratedLink?: LinkReference) { ... }

  // Deletes a thread with optimistic update.
  async function deleteThread(id: Identifier) { ... }

  // Likes a post with optimistic UI update:
  // - Updates local cache to increment likes count and mark liked true.
  // - Calls likePostAdd API.
  async function likePost(id: Identifier) { ... }

  // Unlikes a post with optimistic UI update:
  // - Updates local cache to decrement likes count and mark liked false.
  // - Calls likePostRemove API.
  async function unlikePost(id: Identifier) { ... }

  return {
    createThread,
    deleteThread,
    likePost,
    unlikePost,
    revalidate,
  };
}
```
- This hook provides the `likePost` and `unlikePost` functions used in `useLikeButton`.
- Both functions perform optimistic UI updates by mutating the cached thread list before calling the respective API endpoints.
- The `revalidate` function refreshes the thread list cache after mutations.

---

### Relevant snippet from `app/transports/http/openapi/server_gen.go` lines 2170-2213 (summary):
```go
PostReference struct {
  Assets AssetList `json:"assets"`
  Author ProfileReference `json:"author"`
  Collections CollectionStatus `json:"collections"`
  CreatedAt time.Time `json:"createdAt"`
  DeletedAt *time.Time `json:"deletedAt,omitempty"`
  Description *PostDescription `json:"description,omitempty"`
  Id Identifier `json:"id"`
  Likes LikeData `json:"likes"`
  Meta *Metadata `json:"meta,omitempty"`
  Misc *map[string]interface{} `json:"misc,omitempty"`
  Reacts ReactList `json:"reacts"`
  Slug ThreadMark `json:"slug"`
  Title ThreadTitle `json:"title"`
  UpdatedAt time.Time `json:"updatedAt"`
}
```
- Defines the `PostReference` type used in `useLikeButton` props.
- Includes the `Likes` field which contains like count and liked status, relevant for toggling like state.

---

These snippets provide context on how the like/unlike toggle is implemented, how errors and UI updates are handled, and the data structure of the post object involved.

</details>



---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: Southclaws
PR: Southclaws/storyden#385
File: web/src/components/post/LikeButton/useLikeButton.ts:17-21
Timestamp: 2025-04-21T09:48:06.361Z
Learning: In Storyden, optimistic UI updates for like/unlike actions are handled by the `likePost` and `unlikePost` functions in `web/src/lib/feed/mutation.ts`, which update the local cache with `mutate()` before making the actual API calls.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-21T09:48:07Z
author_association: NONE

PR comments:
user: login: vercel[bot]
created_at: 2025-04-09T14:19:17Z
body: [vc]: #HGZQGoUORvJ8nMNZG0OTC3NfG4eUQj+nl4tV7UxxVbI=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJzdG9yeWRlbi1ob21lcGFnZSIsInJvb3REaXJlY3RvcnkiOiJob21lIiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3NvdXRoY2xhd3MtcHJvamVjdHMvc3RvcnlkZW4taG9tZXBhZ2UvMnlRNUJRcW1GaTVhdGVHQnBDVTZKZzR3WUNraiIsInByZXZpZXdVcmwiOiJzdG9yeWRlbi1ob21lcGFnZS1naXQtZm9yay1odW1lbWFuLTItMDk5NjAxLXNvdXRoY2xhd3MtcHJvamVjdHMudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJJR05PUkVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6IiJ9fSx7Im5hbWUiOiJzdG9yeWRlbiIsInJvb3REaXJlY3RvcnkiOiJ3ZWIiLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vc291dGhjbGF3cy1wcm9qZWN0cy9zdG9yeWRlbi8ybTllYWpIdXlGSFF4aVI0cEh6ZFNyTnV3MzJxIiwicHJldmlld1VybCI6InN0b3J5ZGVuLWdpdC1mb3JrLWh1bWVtYW4tMjE2LXRocmVhZC1saWtlcy1zb3V0aGNsYXdzLXByb2plY3RzLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoic3RvcnlkZW4tZ2l0LWZvcmstaHVtZW1hbi0yMTYtdGhyZWFkLWxpa2VzLXNvdXRoY2xhd3MtcHJvamVjdHMudmVyY2VsLmFwcCJ9fV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **storyden** | ✅ Ready ([Inspect](https://vercel.com/southclaws-projects/storyden/2m9eajHuyFHQxiR4pHzdSrNuw32q)) | [Visit Preview](https://storyden-git-fork-humeman-216-thread-likes-southclaws-projects.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/storyden-git-fork-humeman-216-thread-likes-southclaws-projects.vercel.app?via=pr-comment-feedback-link) | Apr 17, 2025 0:58am |

<details><summary>1 Skipped Deployment</summary>

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **storyden-homepage** | ⬜️ Ignored ([Inspect](https://vercel.com/southclaws-projects/storyden-homepage/2yQ5BQqmFi5ateGBpCU6Jg4wYCkj)) | [Visit Preview](https://storyden-homepage-git-fork-humeman-2-099601-southclaws-projects.vercel.app) |  | Apr 17, 2025 0:58am |
</details>



author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-04-09T14:19:18Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

<details>
<summary>📝 Walkthrough</summary>

## Walkthrough

This change introduces a "like" feature for post threads in the application. It adds a new `LikeButton` component, a supporting custom hook `useLikeButton`, and a UI component `LikeAction` for the like button itself. The `LikeButton` is integrated into the thread card UI, allowing users to like or unlike posts. The backend logic for liking and unliking posts is handled by new functions in the feed mutations library, which optimistically update the UI and then persist changes to the server. All new components and logic are modular and reusable.

## Changes

| File(s)                                                                                      | Change Summary                                                                                                   |
|---------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------|
| web/src/components/post/LikeButton/LikeButton.tsx                                           | Added a new `LikeButton` React component that uses a custom hook and renders a styled like action button.        |
| web/src/components/post/LikeButton/useLikeButton.ts                                         | Introduced a custom hook `useLikeButton` to handle like/unlike logic and state management for threads.           |
| web/src/components/site/Action/Like.tsx                                                     | Added a new `LikeAction` UI component for rendering the like button with appropriate icons and accessibility.    |
| web/src/components/post/ThreadCard.tsx                                                      | Integrated the `LikeButton` into the thread card controls, shown when a user session exists.                     |
| web/src/lib/feed/mutation.ts                                                                | Added `likePost` and `unlikePost` functions to manage optimistic UI updates and server requests for likes.       |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant LikeButton
    participant useLikeButton
    participant useFeedMutations
    participant Server

    User->>LikeButton: Clicks LikeAction
    LikeButton->>useLikeButton: handleClick()
    useLikeButton->>useFeedMutations: likePost/unlikePost(thread.id)
    useFeedMutations->>useFeedMutations: Optimistically update cache
    useFeedMutations->>Server: likePostAdd/likePostRemove API call
    useFeedMutations->>useLikeButton: Return control
    useLikeButton->>LikeButton: Revalidate feed data
    LikeButton->>User: UI updates to reflect new like state
```

## Poem

> In the meadow of code, a new button appears,  
> For posts to be liked, and to gather some cheers.  
> A hop and a click, the heart turns bright,  
> With optimistic updates, the feed feels light.  
> Now rabbits and users, with joy can partake—  
> In threads full of stories, just one click to like!  
> 🐇💖

</details>

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡💬 Agentic Chat (Pro Plan, General Availability)</summary>
> 
> - We're introducing multi-step agentic chat in review comments and issue comments, within and outside of PR's. This feature enhances review and issue discussions with the CodeRabbit agentic chat by enabling advanced interactions, including the ability to create pull requests directly from comments and add commits to existing pull requests.
> 
> </details>

<!-- announcements_end -->
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

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.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=Southclaws/storyden&utm_content=385):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: vercel[bot]
created_at: 2025-04-09T14:19:18Z
body: @humeman is attempting to deploy a commit to the **Southclaws** Team on [Vercel](https://vercel.com).

A member of the Team first needs to [authorize it](https://vercel.com/git/authorize?team=Southclaws&type=github&job=%7B%22headInfo%22%3A%7B%22sha%22%3A%22414eb3e4ec74550a8930e3b5b6a52276933fa4e6%22%7D%2C%22id%22%3A%22QmahfvfgZxueC75Z3PDuyxw4o3ZBbBGfCGS5qnotZCCxaY%22%2C%22org%22%3A%22Southclaws%22%2C%22prId%22%3A385%2C%22repo%22%3A%22storyden%22%7D).


author_association: NONE
user: login: jbakke2
created_at: 2025-04-10T19:45:04Z
body: ![like](https://github.com/user-attachments/assets/e17104ed-9eaf-4e23-a3ac-39c4b593d822)


author_association: NONE
user: login: Southclaws
created_at: 2025-04-17T12:55:33Z
body: Apologies for the robot reviews, they're useful about 20% of the time 😂 
author_association: OWNER
user: login: Southclaws
created_at: 2025-04-21T09:52:41Z
body: Perfect, thank you for contributing this feature!
author_association: OWNER