-
-
Couldn't load subscription status.
- Fork 6
feat: Implement swipe-to-dismiss for chat messages #264
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
This feature allows users to hide chat messages by swiping them to the left. Key changes: - Added `react-swipeable` to handle swipe gestures. - Created a `SwipeableMessage` component to wrap chat messages and detect swipes. - Implemented a `hideMessage` action to update the message state and set an `isHidden` flag. - Updated the UI to filter out hidden messages.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughIntroduces swipe-to-hide functionality: adds a server action to mark messages as hidden, updates type definitions, filters hidden messages from UI state, wraps chat messages with a new SwipeableMessage component that triggers hide on left swipe, and adds react-swipeable dependency. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor U as User
participant SM as SwipeableMessage (client)
participant RS as react-swipeable
participant AA as AI Actions (client proxy)
participant SA as hideMessage (server action)
participant ST as AI State
participant CM as ChatMessages renderer
U->>SM: Swipe left on message
SM->>RS: Handle gesture (trackMouse: true)
RS-->>SM: onSwipedLeft callback
SM->>AA: actions.hideMessage(message.id)
AA->>SA: hideMessage(messageId)
SA->>ST: Set message.isHidden = true
SA-->>AA: Success
AA-->>SM: Resolved
CM->>ST: getUIStateFromAIState()
ST-->>CM: messages filtered (exclude isHidden)
CM-->>U: UI re-renders without hidden message
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Suggested labels
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
|
|
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
PR Code Suggestions ✨Explore these optional code suggestions:
|
||||||||||||||||
There was a problem hiding this 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
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
💡 Knowledge Base configuration:
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (1)
bun.lockbis excluded by!**/bun.lockb
📒 Files selected for processing (5)
app/actions.tsx(3 hunks)components/chat-messages.tsx(2 hunks)components/swipeable-message.tsx(1 hunks)lib/types/index.ts(1 hunks)package.json(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
lib/types/index.ts (2)
lib/actions/chat.ts (1)
msg(119-127)lib/actions/chat-db.ts (1)
msg(117-121)
components/swipeable-message.tsx (1)
components/collapsible-message.tsx (2)
data(24-75)CollapsibleMessageProps(15-22)
components/chat-messages.tsx (2)
components/swipeable-message.tsx (1)
SwipeableMessage(15-30)components/collapsible-message.tsx (2)
CollapsibleMessage(24-75)CollapsibleMessageProps(15-22)
app/actions.tsx (2)
lib/actions/chat.ts (1)
msg(119-127)lib/actions/chat-db.ts (1)
msg(117-121)
🔇 Additional comments (5)
package.json (1)
74-74: react-swipeable import scope verified
Confirmed usage only incomponents/swipeable-message.tsxwhich begins with'use client'; no server-side imports detected.lib/types/index.ts (1)
74-75: PersistisHiddenin your message save/load mapping
You’ve added the flag in the type and toggle it inapp/actions.tsx, but I couldn’t find it in your persistence layer (e.g. noisHiddeninlib/actions/chat.ts), so hidden messages will reappear after reload. Extend your save/load logic to includeisHiddenor document it as a UI‐only field.app/actions.tsx (2)
292-295: Wiring action into AI.actions — LGTM
367-368: Filtering hidden messages before UI mapping — LGTMThis ensures hidden groups are excluded consistently.
components/chat-messages.tsx (1)
6-6: Import of SwipeableMessage — LGTM
| async function hideMessage(messageId: string) { | ||
| 'use server' | ||
|
|
||
| const aiState = getMutableAIState<typeof AI>() | ||
|
|
||
| aiState.update({ | ||
| ...aiState.get(), | ||
| messages: aiState.get().messages.map(msg => { | ||
| if (msg.id === messageId) { | ||
| return { | ||
| ...msg, | ||
| isHidden: true | ||
| } | ||
| } | ||
| return msg | ||
| }) | ||
| }) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
hideMessage should be idempotent and avoid no-op updates
Avoid updating state when nothing changes; also read aiState once.
async function hideMessage(messageId: string) {
'use server'
- const aiState = getMutableAIState<typeof AI>()
-
- aiState.update({
- ...aiState.get(),
- messages: aiState.get().messages.map(msg => {
- if (msg.id === messageId) {
- return {
- ...msg,
- isHidden: true
- }
- }
- return msg
- })
- })
+ const aiState = getMutableAIState<typeof AI>()
+ const current = aiState.get()
+ let changed = false
+ const messages = current.messages.map(msg => {
+ if (msg.id === messageId && msg.isHidden !== true) {
+ changed = true
+ return { ...msg, isHidden: true }
+ }
+ return msg
+ })
+ if (!changed) return
+ aiState.update({ ...current, messages })
}📝 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.
| async function hideMessage(messageId: string) { | |
| 'use server' | |
| const aiState = getMutableAIState<typeof AI>() | |
| aiState.update({ | |
| ...aiState.get(), | |
| messages: aiState.get().messages.map(msg => { | |
| if (msg.id === messageId) { | |
| return { | |
| ...msg, | |
| isHidden: true | |
| } | |
| } | |
| return msg | |
| }) | |
| }) | |
| } | |
| async function hideMessage(messageId: string) { | |
| 'use server' | |
| const aiState = getMutableAIState<typeof AI>() | |
| const current = aiState.get() | |
| let changed = false | |
| const messages = current.messages.map(msg => { | |
| if (msg.id === messageId && msg.isHidden !== true) { | |
| changed = true | |
| return { ...msg, isHidden: true } | |
| } | |
| return msg | |
| }) | |
| if (!changed) return | |
| aiState.update({ ...current, messages }) | |
| } |
🤖 Prompt for AI Agents
In app/actions.tsx around lines 35 to 52, the hideMessage function repeatedly
reads aiState and always triggers an update even when nothing changes; refactor
to read aiState.get() once into a local variable, find the target message and if
it doesn't exist or already has isHidden true return early (no-op), otherwise
create a new messages array that only changes the matched message's isHidden to
true and call aiState.update once with the new state; this makes the operation
idempotent and avoids unnecessary state updates.
| <SwipeableMessage | ||
| key={`${groupedMessage.id}-swipeable`} | ||
| message={groupedMessage} | ||
| > | ||
| <CollapsibleMessage | ||
| key={`${groupedMessage.id}`} | ||
| message={{ | ||
| id: groupedMessage.id, | ||
| component: groupedMessage.components.map((component, i) => ( | ||
| <div key={`${groupedMessage.id}-${i}`}>{component}</div> | ||
| )), | ||
| isCollapsed: groupedMessage.isCollapsed | ||
| }} | ||
| isLastMessage={ | ||
| groupedMessage.id === messages[messages.length - 1].id | ||
| } | ||
| /> | ||
| </SwipeableMessage> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick (assertive)
Wrap with SwipeableMessage — good; remove redundant key on single child
CollapsibleMessage isn’t part of a list here; the extra key is unnecessary.
<SwipeableMessage
key={`${groupedMessage.id}-swipeable`}
message={groupedMessage}
>
- <CollapsibleMessage
- key={`${groupedMessage.id}`}
+ <CollapsibleMessage
message={{
id: groupedMessage.id,
component: groupedMessage.components.map((component, i) => (
<div key={`${groupedMessage.id}-${i}`}>{component}</div>
)),
isCollapsed: groupedMessage.isCollapsed
}}
isLastMessage={
groupedMessage.id === messages[messages.length - 1].id
}
/>
</SwipeableMessage>📝 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.
| <SwipeableMessage | |
| key={`${groupedMessage.id}-swipeable`} | |
| message={groupedMessage} | |
| > | |
| <CollapsibleMessage | |
| key={`${groupedMessage.id}`} | |
| message={{ | |
| id: groupedMessage.id, | |
| component: groupedMessage.components.map((component, i) => ( | |
| <div key={`${groupedMessage.id}-${i}`}>{component}</div> | |
| )), | |
| isCollapsed: groupedMessage.isCollapsed | |
| }} | |
| isLastMessage={ | |
| groupedMessage.id === messages[messages.length - 1].id | |
| } | |
| /> | |
| </SwipeableMessage> | |
| <SwipeableMessage | |
| key={`${groupedMessage.id}-swipeable`} | |
| message={groupedMessage} | |
| > | |
| <CollapsibleMessage | |
| message={{ | |
| id: groupedMessage.id, | |
| component: groupedMessage.components.map((component, i) => ( | |
| <div key={`${groupedMessage.id}-${i}`}>{component}</div> | |
| )), | |
| isCollapsed: groupedMessage.isCollapsed | |
| }} | |
| isLastMessage={ | |
| groupedMessage.id === messages[messages.length - 1].id | |
| } | |
| /> | |
| </SwipeableMessage> |
🤖 Prompt for AI Agents
In components/chat-messages.tsx around lines 54 to 71, remove the redundant key
prop from the CollapsibleMessage child since SwipeableMessage already has the
unique key; keep the key only on the SwipeableMessage wrapper, delete the key
attribute on the CollapsibleMessage element, and verify TypeScript/JSX props
remain unchanged so rendering and isLastMessage logic still work.
| 'use client' | ||
|
|
||
| import { useSwipeable } from 'react-swipeable' | ||
| import { StreamableValue, useActions } from 'ai/rsc' | ||
|
|
||
| interface SwipeableMessageProps { | ||
| message: { | ||
| id: string | ||
| components: React.ReactNode[] | ||
| isCollapsed?: StreamableValue<boolean> | ||
| } | ||
| children: React.ReactNode | ||
| } | ||
|
|
||
| export function SwipeableMessage({ | ||
| message, | ||
| children | ||
| }: SwipeableMessageProps) { | ||
| const { hideMessage } = useActions() | ||
|
|
||
| const handlers = useSwipeable({ | ||
| onSwipedLeft: () => { | ||
| console.log(`Swiped left on message ${message.id}`) | ||
| hideMessage(message.id) | ||
| }, | ||
| trackMouse: true | ||
| }) | ||
|
|
||
| return <div {...handlers}>{children}</div> | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Harden swipe UX and add keyboard/A11y fallback; drop console.log
Prevents accidental hides, supports keyboards/screen readers, and removes debug logging.
'use client'
import { useSwipeable } from 'react-swipeable'
import { StreamableValue, useActions } from 'ai/rsc'
interface SwipeableMessageProps {
message: {
id: string
components: React.ReactNode[]
isCollapsed?: StreamableValue<boolean>
}
children: React.ReactNode
}
export function SwipeableMessage({
message,
children
}: SwipeableMessageProps) {
const { hideMessage } = useActions()
const handlers = useSwipeable({
- onSwipedLeft: () => {
- console.log(`Swiped left on message ${message.id}`)
- hideMessage(message.id)
- },
- trackMouse: true
+ onSwipedLeft: () => {
+ void hideMessage(message.id)
+ },
+ trackMouse: true,
+ preventScrollOnSwipe: true,
+ delta: 50
})
- return <div {...handlers}>{children}</div>
+ return (
+ <div
+ {...handlers}
+ role="button"
+ tabIndex={0}
+ aria-label="Hide message"
+ onKeyDown={(e) => {
+ if (e.key === 'Backspace' || e.key === 'Delete') {
+ e.preventDefault()
+ void hideMessage(message.id)
+ }
+ }}
+ >
+ {children}
+ </div>
+ )
}📝 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.
| 'use client' | |
| import { useSwipeable } from 'react-swipeable' | |
| import { StreamableValue, useActions } from 'ai/rsc' | |
| interface SwipeableMessageProps { | |
| message: { | |
| id: string | |
| components: React.ReactNode[] | |
| isCollapsed?: StreamableValue<boolean> | |
| } | |
| children: React.ReactNode | |
| } | |
| export function SwipeableMessage({ | |
| message, | |
| children | |
| }: SwipeableMessageProps) { | |
| const { hideMessage } = useActions() | |
| const handlers = useSwipeable({ | |
| onSwipedLeft: () => { | |
| console.log(`Swiped left on message ${message.id}`) | |
| hideMessage(message.id) | |
| }, | |
| trackMouse: true | |
| }) | |
| return <div {...handlers}>{children}</div> | |
| } | |
| 'use client' | |
| import { useSwipeable } from 'react-swipeable' | |
| import { StreamableValue, useActions } from 'ai/rsc' | |
| interface SwipeableMessageProps { | |
| message: { | |
| id: string | |
| components: React.ReactNode[] | |
| isCollapsed?: StreamableValue<boolean> | |
| } | |
| children: React.ReactNode | |
| } | |
| export function SwipeableMessage({ | |
| message, | |
| children | |
| }: SwipeableMessageProps) { | |
| const { hideMessage } = useActions() | |
| const handlers = useSwipeable({ | |
| onSwipedLeft: () => { | |
| void hideMessage(message.id) | |
| }, | |
| trackMouse: true, | |
| preventScrollOnSwipe: true, | |
| delta: 50 | |
| }) | |
| return ( | |
| <div | |
| {...handlers} | |
| role="button" | |
| tabIndex={0} | |
| aria-label="Hide message" | |
| onKeyDown={(e) => { | |
| if (e.key === 'Backspace' || e.key === 'Delete') { | |
| e.preventDefault() | |
| void hideMessage(message.id) | |
| } | |
| }} | |
| > | |
| {children} | |
| </div> | |
| ) | |
| } |
User description
This feature allows users to hide chat messages by swiping them to the left.
Key changes:
react-swipeableto handle swipe gestures.SwipeableMessagecomponent to wrap chat messages and detect swipes.hideMessageaction to update the message state and set anisHiddenflag.PR Type
Enhancement
Description
Added swipe-to-dismiss functionality for chat messages
Created SwipeableMessage component with left swipe detection
Implemented hideMessage server action to update message state
Added isHidden flag to AIMessage type definition
Diagram Walkthrough
File Walkthrough
index.ts
Add isHidden property to AIMessage typelib/types/index.ts
isHiddenboolean property to AIMessage typeactions.tsx
Implement hideMessage action and message filteringapp/actions.tsx
hideMessageserver action to mark messages as hiddengetUIStateFromAIStateto filter out hidden messageschat-messages.tsx
Integrate SwipeableMessage wrapper for chat messagescomponents/chat-messages.tsx
swipeable-message.tsx
Create SwipeableMessage component with gesture detectioncomponents/swipeable-message.tsx
package.json
Add react-swipeable dependency for gesture handlingpackage.json
react-swipeabledependency version 7.0.2Summary by CodeRabbit
New Features
Chores