Skip to content

Chat Delete

Metadata

  • System type: flow

System Intent

  • What this is: Long-press gesture on conversation items in the side-menu that opens an in-component action sheet, wired to a React Query mutation and a backend Lambda that batch-deletes messages then the chat row.

Mermaid Diagram

flowchart TD
  User["User (long-press 400ms)"] -->|GestureDetector LongPress| SideMenu["side-menu.tsx\npendingDeleteItem state"]
  SideMenu -->|renders overlay| ActionSheet["In-component ActionSheet\n(Delete / Cancel)"]
  ActionSheet -->|onDeleteConversation item| IndexHandler["index.tsx\nhandleDeleteConversation"]
  IndexHandler -->|deleteChat item.id| useDeleteChat["useDeleteChat\n(useChatsApi.ts)"]
  useDeleteChat -->|POST /chats/delete| DeleteLambda["ChatsDeleteFunction\n(app.py)"]
  DeleteLambda -->|chat_owned_by check| DynamoDB["DynamoDB: Chats + Messages tables"]
  DeleteLambda -->|batch delete messages then chat row| DynamoDB
  DeleteLambda -->|deleted true/false| useDeleteChat
  useDeleteChat -->|onSuccess invalidateQueries| ChatsFeed["useChatsFeed\n(chats feed cache)"]
  ChatsFeed -->|re-fetch| SideMenu

Flows

Flow: longPressOpenActionSheet

  • Test files: main/app/__tests__/side-menu-delete.test.tsx
  • Core files: main/app/components/side-menu.tsx

Types

LongPressInput {
  item: ChatSummary (the conversation row that was held)
}

ChatSummary {
  id: string
  title: string
  last_message: string (relative label: "Today", "Yesterday", weekday, or "Mon D")
  updated_at: string (ISO timestamp)
}

ActionSheetState {
  pendingDeleteItem: ChatSummary | null
}

Paths

path input output path-type notes
longPressOpenActionSheet.opens LongPressInput (400ms hold) ActionSheetState pendingDeleteItem=item happy path GestureDetector fires onEnd, runOnJS marshals to JS thread
longPressOpenActionSheet.dismiss tap Cancel ActionSheetState pendingDeleteItem=null happy path overlay hidden, no mutation fired
longPressOpenActionSheet.shortTap tap < 400ms no state change happy path LongPress gesture does not fire; Pressable onPress fires normally

Pseudocode

// side-menu.tsx — per-item render
longPressGesture = Gesture.LongPress()
  .minDuration(400)
  .onEnd(() => {
    runOnJS(setPendingDeleteItem)(item)  // runOnJS: worklet → JS thread
  })

return (
  <GestureDetector gesture={longPressGesture}>
    <Pressable onPress={() => onConversationPress(item)}>
      ...
    </Pressable>
  </GestureDetector>
)

// GestureHandlerRootView wraps the entire Modal so RNGH can activate
// on Android (bypasses FlatList ScrollView touch-steal)

// Action sheet confirm (side-menu.tsx)
onPress(() => {
  const item = pendingDeleteItem
  setPendingDeleteItem(null)       // dismiss overlay immediately
  onDeleteConversation(item)       // delegate up to index.tsx
})

Flow: deleteChat

  • Test files: main/app/__tests__/delete-chat-api.test.ts, main/app/__tests__/side-menu-delete.test.tsx
  • Core files: main/app/lib/api/chats/deleteChat.ts, main/app/lib/api/chats/useChatsApi.ts, main/app/components/side-menu.tsx, main/app/app/index.tsx, main/server/api/chats/delete/app.py, main/server/layers/shared/python/shared/chat/repository.py

Types

DeleteChatRequest {
  chat_id: string (required, non-empty)
}

DeleteChatResponse {
  deleted: boolean   // false if chat not found or not owned by user
  chat_id: string
}

StandardError {
  message: string (human-readable description of what went wrong)
}

Paths

path input output path-type notes
deleteChat.success {chat_id} owned by user {deleted: true, chat_id} + cache invalidation happy path Messages batch-deleted in groups of 25, then chat row deleted; feed refetches
deleteChat.not_owned {chat_id} not owned or not found {deleted: false, chat_id} HTTP 200 graceful degradation chat_owned_by check fails silently; no deletion; UI still dismisses
deleteChat.missing_chat_id empty payload HTTP 400 {message: "chat_id is required"} error Validated before DB access
deleteChat.missing_user_id missing auth context HTTP 400 {message: "user_id is required"} error Validated in implementation()
deleteChat.networkError API unreachable onError logged; no UI feedback shown error No user-facing error toast; accessible via mutation.isError and mutation.error

Pseudocode

// index.tsx — handleDeleteConversation
handleDeleteConversation(item):
  logger({ step: "conversation_delete_requested", chat_id: item.id })
  deleteChat(item.id)              // useDeleteChat().mutate

// useDeleteChat (useChatsApi.ts)
mutationFn(chatId):
  logger({ step: "chat_delete_requested", chat_id: chatId })
  return deleteChat(chatId)        // POST /chats/delete { chat_id }
onSuccess(_data, chatId):
  logger({ step: "chat_delete_succeeded", chat_id: chatId })
  queryClient.invalidateQueries(["chats", "feed"])
onError(_error, chatId):
  logger({ step: "chat_delete_failed", chat_id: chatId })

// deleteChat.ts
POST /chats/delete { chat_id: chatId }  timeout: 30_000ms
// response body ignored; void return

// Backend — ChatsDeleteFunction (app.py)
implementation(payload, auth_context):
  user_id = auth_context.user_id           // raises ValueError if empty
  chat_id = payload.chat_id.strip()        // raises ValueError if empty
  log_event(FLOW, "delete_chat_start", { user_id, chat_id })
  repo = ChatRepository(MESSAGES_TABLE_NAME, CHATS_TABLE_NAME)
  deleted = repo.delete_chat(user_id, chat_id)
  if not deleted:
    log_event(FLOW, "delete_chat_not_found", ...)
    return { deleted: False, chat_id }
  log_event(FLOW, "delete_chat_complete", ...)
  return { deleted: True, chat_id }

// ChatRepository.delete_chat(user_id, chat_id):
  if not chat_owned_by(user_id, chat_id): return False
  loop:
    items = messages_table.query(chat_id, Limit=25)
    if not items: break
    batch_writer.delete_item for each item (key: chat_id + message_id)
    if not LastEvaluatedKey: break
  chats_table.delete_item(Key: { user_id, chat_id })
  return True

Logs

Source Location
Delete requested (frontend) step: "conversation_delete_requested" via useLogging in main/app/app/index.tsx
Delete mutation start step: "chat_delete_requested" via useLogging in useDeleteChat
Delete mutation success step: "chat_delete_succeeded" via useLogging in useDeleteChat
Delete mutation failure step: "chat_delete_failed" via useLogging in useDeleteChat
Delete Lambda start step: "delete_chat_start" via log_event in ChatsDeleteFunction
Delete Lambda not found step: "delete_chat_not_found" via log_event in ChatsDeleteFunction
Delete Lambda complete step: "delete_chat_complete" via log_event in ChatsDeleteFunction

Deployment

  • Mechanism: SAM
  • Deploy command:
    cd main/server
    sam build
    sam deploy --template .aws-sam/build/template.yaml
    
  • Notes:
  • ChatsDeleteFunction: Python 3.12, default 30s timeout. Env vars: CHATS_TABLE_NAME (default Chats), MESSAGES_TABLE_NAME (default Messages). Uses ChatDynamoDBPolicy (read + write on both tables). Route: POST /chats/delete behind Cognito JWT authorizer.
  • Frontend changes (gesture detection, action sheet, hook wiring) are bundled with the app build; no separate infra deploy needed beyond the Lambda.
  • Jest mocks required: react-native-reanimated module aliased to its mock in jest.config.js (moduleNameMapper); runOnJS stubbed as pass-through in jest.setup.js.

Key Files

File Role
main/app/components/side-menu.tsx Long-press gesture (GestureDetector + RNGH), pendingDeleteItem state, in-component action sheet overlay
main/app/app/index.tsx handleDeleteConversation — logs delete request and calls deleteChat mutate
main/app/lib/api/chats/deleteChat.ts deleteChat(chatId) — POST /chats/delete API client, 30s timeout
main/app/lib/api/chats/useChatsApi.ts useDeleteChat — React Query mutation with logging and ["chats", "feed"] cache invalidation on success
main/server/api/chats/delete/app.py ChatsDeleteFunction Lambda — validates input, calls repo.delete_chat, logs events
main/server/layers/shared/python/shared/chat/repository.py ChatRepository.delete_chat — ownership check, batch message deletion, chat row deletion

Architecture Notes

In-Component Action Sheet

The delete confirmation uses an in-component overlay (View with StyleSheet.absoluteFillObject) rather than Alert.alert(). On Android, Alert.alert() targets the Activity window, not the Modal's Dialog window, so it never appears on top of an open Modal. The overlay renders inside the Modal's GestureHandlerRootView at zIndex: 10, guaranteeing correct stacking on both platforms.

GestureHandlerRootView Wrapping

GestureHandlerRootView wraps the entire Modal contents in side-menu.tsx. This is required on Android so RNGH gesture recognizers can activate inside a Modal; without it the FlatList's underlying ScrollView intercepts all touch events and the long-press never fires.

Ownership Check Returns 200

ChatsDeleteFunction returns {deleted: false} with HTTP 200 (not 403) when the chat is not found or not owned by the requesting user. This is a silent no-op design: the UI dismisses the overlay regardless of the deleted flag, and no toast is shown. The backend still logs delete_chat_not_found for observability.