* feat: edit/revert sent messages via server revert API Wires the mobile client up to the opencode server's session.revert / session.unrevert endpoints (the same primitive the desktop TUI uses to edit the last message). Long-press a user message bubble -> "Edit message" reverts it server-side and prefills the composer with its text; a banner offers Undo while the revert is pending (it's only cleaned up server-side on the next prompt). Degrades gracefully with an alert on older servers that 404 the /revert route. Closes #56 * fix(revert): address code review findings on edit/revert message flow - Confirm before overwriting an in-progress composer draft when editing a sent message (F2) - Restore reverted message's file attachments into the composer, not just its text (F3) - Distinguish 401/403 from other revert failures with an accurate "Authentication failed" message instead of a generic one (F4) - Exclude optimistic "temp-" message IDs from the revert cutoff comparison so concurrently-sent messages aren't hidden (F9) --------- Co-authored-by: engineer <engineer@gray-knight-m1.local>
This commit is contained in:
@@ -16,12 +16,16 @@ interface Props {
|
||||
message: Message
|
||||
parts: Part[]
|
||||
isDark: boolean
|
||||
// Only wired up for user messages — long-press opens the "Edit message" /
|
||||
// revert action sheet. Identified by messageID (not a closure over parts)
|
||||
// so it stays correct even if the memo below bails on a stale render.
|
||||
onLongPress?: (messageID: string) => void
|
||||
}
|
||||
|
||||
// TODO: Replace with streamdown-rn once React 19 types PR lands - it has
|
||||
// built-in block-level memoization that eliminates re-renders for stable blocks
|
||||
export const MessageBubble = memo(
|
||||
function MessageBubble({ message, parts, isDark }: Props) {
|
||||
function MessageBubble({ message, parts, isDark, onLongPress }: Props) {
|
||||
const isUser = message.role === "user"
|
||||
|
||||
const textParts = parts.filter((p) => p.type === "text")
|
||||
@@ -32,7 +36,10 @@ export const MessageBubble = memo(
|
||||
const reasoning = reasoningParts.map((p) => p.text).join("\n") || ""
|
||||
|
||||
return (
|
||||
<View
|
||||
<TouchableOpacity
|
||||
activeOpacity={isUser && onLongPress ? 0.7 : 1}
|
||||
onLongPress={isUser && onLongPress ? () => onLongPress(message.id) : undefined}
|
||||
disabled={!isUser || !onLongPress}
|
||||
style={[
|
||||
s.bubble,
|
||||
isUser ? s.user : s.assistant,
|
||||
@@ -101,7 +108,7 @@ export const MessageBubble = memo(
|
||||
{message.cost ? ` · $${message.cost.toFixed(4)}` : ""}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
},
|
||||
(prev, next) => {
|
||||
@@ -109,6 +116,7 @@ export const MessageBubble = memo(
|
||||
// This prevents completed messages from re-rendering during streaming
|
||||
if (prev.message.id !== next.message.id) return false
|
||||
if (prev.isDark !== next.isDark) return false
|
||||
if (prev.onLongPress !== next.onLongPress) return false
|
||||
if (prev.parts.length !== next.parts.length) return false
|
||||
// Compare the last part's text content - this is what changes during streaming
|
||||
const prevLast = prev.parts[prev.parts.length - 1]
|
||||
|
||||
Reference in New Issue
Block a user