-
-
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?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { StreamableValue, useUIState } from 'ai/rsc' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { AI, UIState } from '@/app/actions' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { CollapsibleMessage } from './collapsible-message' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { SwipeableMessage } from './swipeable-message' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| interface ChatMessagesProps { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| messages: UIState | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -50,19 +51,24 @@ export function ChatMessages({ messages }: ChatMessagesProps) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| index | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) => ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+54
to
+71
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,30 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| '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> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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
🤖 Prompt for AI Agents