# Solution Design Document

## Validation Checklist

- [x] All required sections are complete
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] All context sources are listed with relevance ratings
- [x] Project commands are discovered from actual project files
- [x] Constraints → Strategy → Design → Implementation path is logical
- [x] Architecture pattern is clearly stated with rationale
- [x] Every component in diagram has directory mapping
- [x] Every interface has specification
- [x] Error handling covers all error types
- [x] Quality requirements are specific and measurable
- [x] Every quality requirement has test coverage
- [x] **All architecture decisions confirmed by user**
- [x] Component names consistent across diagrams
- [x] A developer could implement from this design

---

## Constraints

**CON-1 Framework & Language:**
- Flutter 3.38.4 / Dart 3.10.3
- Riverpod 3.x with `Notifier` pattern (not `StateNotifier`)
- Freezed 3.x with `abstract class` keyword for models
- Equatable for domain entities

**CON-2 Architecture Patterns:**
- Clean Architecture: `core/` → `data/` → `domain/` → `presentation/`
- Repository pattern with interface in `domain/`, implementation in `data/`
- Sealed classes for type-safe state machines
- Extension mappers for model-to-entity conversion

**CON-3 API Specification:**
- Must consume `docs/api/staff-chat-mobile-openapi.yaml` (19 endpoints)
- JWT Bearer token authentication via existing `AuthInterceptor`
- Store-scoped endpoints: `/{typeNum}/channels/...`

**CON-4 Real-time Communication:**
- Ably Flutter SDK (`ably_flutter: ^1.2.35`) for real-time
- Token-based authentication via `/{typeNum}/ably-token`
- Channel naming: `chat:{typeNum}:{channelId}` for messages
- Presence channel: `chat:{typeNum}:{channelId}:presence` for typing

**CON-5 Shared Code Requirement:**
- PRD Decision: Create `buyerkiosk_chat` shared package
- Must work in both BuyerKiosk Team and BuyerKiosk Live apps
- Shared: models, entities, services, repositories, core logic
- App-specific: screens, widgets, theme integration

**CON-6 UI Requirements:**
- Custom Flutter UI (not chatview package) per PRD decision
- WhatsApp-style bubbles: own messages right (purple #7c3aed), others left (gray)
- Dark mode required (follow system preference)
- Swipe gestures: right = react, left = reply

**CON-7 Rate Limits (Backend Enforced):**
- Messages: 30/minute per user/channel
- Reactions: 20/minute per user (global)
- File uploads: 10/minute per user

---

## Shared Package Plan (buyerkiosk_chat)

Per PRD requirement, chat must be shared between BuyerKiosk Team and BuyerKiosk Live apps.

### Package Structure

```
packages/
└── buyerkiosk_chat/
    ├── pubspec.yaml
    ├── lib/
    │   ├── buyerkiosk_chat.dart          # Barrel export
    │   ├── src/
    │   │   ├── models/                   # Freezed models (API shapes)
    │   │   │   ├── channel_model.dart
    │   │   │   ├── message_model.dart
    │   │   │   ├── reaction_model.dart
    │   │   │   ├── member_model.dart
    │   │   │   └── mention_model.dart
    │   │   ├── entities/                 # Equatable domain entities
    │   │   │   ├── channel.dart
    │   │   │   ├── message.dart
    │   │   │   ├── reaction.dart
    │   │   │   ├── member.dart
    │   │   │   └── chat_state.dart       # Sealed state classes
    │   │   ├── mappers/                  # Model ↔ Entity mappers
    │   │   │   └── chat_mappers.dart
    │   │   ├── services/                 # Core services
    │   │   │   └── ably_realtime_service.dart
    │   │   └── repositories/             # Repository interface + impl
    │   │       ├── chat_repository.dart
    │   │       └── chat_repository_impl.dart
    │   └── constants/
    │       └── chat_constants.dart
    └── test/
        └── ...
```

### What Lives in Shared Package vs App-Specific

| Component | Location | Rationale |
|-----------|----------|-----------|
| Data Models (Freezed) | `buyerkiosk_chat` | Same API contract |
| Domain Entities (Equatable) | `buyerkiosk_chat` | Same business logic |
| State Machines (Sealed) | `buyerkiosk_chat` | Same state transitions |
| Mappers | `buyerkiosk_chat` | Same conversions |
| AblyRealtimeService | `buyerkiosk_chat` | Same real-time logic |
| ChatRepository | `buyerkiosk_chat` | Same API calls |
| ChatNotifier (Provider) | **App-specific** | Different Ref context |
| Screens | **App-specific** | Different navigation |
| Widgets | **App-specific** | Different themes |
| Theme integration | **App-specific** | AppColors differs |

### Versioning Strategy

- Package uses `0.x.y` versioning until stable
- Both apps depend on local path during development:
  ```yaml
  # In app pubspec.yaml
  dependencies:
    buyerkiosk_chat:
      path: ../packages/buyerkiosk_chat
  ```
- For release, can publish to private pub server or git dependency

### Implementation Approach

**UPDATED (Implementation Plan Decision):** Create package scaffolding now instead of post-MVP.

**Phase 1 (This Implementation):**
- Create `packages/buyerkiosk_chat/` with full package structure
- Build models, entities, services, and repository in the package
- Keep Riverpod providers and screens in `buyerkiosk_team/lib/` (app-specific)
- Add path dependency to main app's pubspec.yaml

**Phase 2 (Post-MVP - Live App Integration):**
- Add same path dependency to `buyerkiosk_live/pubspec.yaml`
- Create Live-app-specific providers/screens in that app
- Share core chat logic from the package

---

## Implementation Context

### Required Context Sources

```yaml
# Internal documentation
- doc: docs/specs/008-team-chat/product-requirements.md
  relevance: CRITICAL
  why: "Source of truth for all feature requirements, UI specs, and acceptance criteria"

- doc: docs/api/staff-chat-mobile-openapi.yaml
  relevance: CRITICAL
  why: "API contract defining all 19 endpoints, request/response schemas, error codes"

- doc: CLAUDE.md
  relevance: HIGH
  why: "Project architecture patterns, file structure, naming conventions"

# Source code patterns
- file: lib/core/network/api_client.dart
  relevance: HIGH
  why: "HTTP client pattern with error handling to replicate"

- file: lib/core/network/api_interceptors.dart
  relevance: HIGH
  why: "Auth interceptor pattern for token refresh handling"

- file: lib/core/services/push_notification_service.dart
  relevance: HIGH
  why: "FCM integration pattern for chat notifications"

- file: lib/core/constants/ably_constants.dart
  relevance: HIGH
  why: "Existing Ably channel naming and event type patterns"

- file: lib/presentation/providers/shift_requests_provider.dart
  relevance: HIGH
  why: "Complex Riverpod Notifier pattern with multiple state types"

- file: lib/domain/entities/avatar_state.dart
  relevance: MEDIUM
  why: "Sealed class state machine pattern"

- file: lib/data/mappers/shift_request_mappers.dart
  relevance: MEDIUM
  why: "Extension mapper pattern for model-to-entity conversion"

# External documentation
- url: https://pub.dev/packages/ably_flutter
  relevance: HIGH
  sections: [realtime, presence, token_auth]
  why: "Ably Flutter SDK API for real-time messaging"

- url: https://ably.com/docs/presence-occupancy/presence
  relevance: MEDIUM
  why: "Presence API for typing indicators"
```

### Implementation Boundaries

- **Must Preserve**:
  - Existing `AuthInterceptor` token refresh logic
  - `ApiClient` error handling patterns
  - `StorageService` for secure token storage
  - `PushNotificationService` FCM integration
  - App-wide theme (AppColors, AppTheme)

- **Can Modify**:
  - `lib/core/constants/ably_constants.dart` - Add chat-specific events
  - `lib/core/constants/notification_constants.dart` - Add chat notification types
  - `lib/core/services/notification_navigation_service.dart` - Add chat deep links
  - `lib/router/app_router.dart` - Add chat routes
  - `lib/presentation/screens/chat/chat_screen.dart` - Replace placeholder

- **Must Not Touch**:
  - `lib/core/network/api_interceptors.dart` - Auth logic is stable
  - `lib/firebase_options.dart` - Auto-generated
  - Any existing feature screens (home, schedule, settings, requests)

### External Interfaces

#### System Context Diagram

```mermaid
graph TB
    subgraph Mobile["Mobile App (Flutter)"]
        ChatUI[Chat UI]
        ChatProvider[ChatProvider]
        ChatRepo[ChatRepository]
        AblyService[AblyRealtimeService]
    end

    User[Team Member] --> ChatUI
    Manager[Manager] --> ChatUI

    ChatUI --> ChatProvider
    ChatProvider --> ChatRepo
    ChatProvider --> AblyService

    ChatRepo --> BackendAPI[Backend API<br/>staff-chat-mobile]
    AblyService --> AblyCloud[Ably Realtime Cloud]

    BackendAPI --> PostgresDB[(PostgreSQL)]
    BackendAPI --> AblyCloud

    FCM[Firebase Cloud Messaging] --> ChatUI
    BackendAPI -.-> FCM
```

#### Interface Specifications

```yaml
# Inbound Interfaces
inbound:
  - name: "Staff Chat Mobile API"
    type: HTTPS
    format: REST
    authentication: JWT Bearer Token
    doc: docs/api/staff-chat-mobile-openapi.yaml
    base_path: /api/mobile/staff-chat/{typeNum}
    endpoints: 19
    data_flow: "Channels, messages, reactions, members, mentions"

  - name: "Ably Realtime"
    type: WebSocket
    format: Ably Protocol
    authentication: Token (via POST /{typeNum}/ably-token)
    doc: https://ably.com/docs
    data_flow: "Real-time message delivery, typing indicators, reactions"
    channels:
      - pattern: "chat:{typeNum}:{channelId}"
        purpose: "Message events"
      - pattern: "chat:{typeNum}:{channelId}:presence"
        purpose: "Typing indicators"

  - name: "Firebase Cloud Messaging"
    type: Push Notification
    format: FCM Data Message
    authentication: Device Token
    doc: lib/core/services/push_notification_service.dart
    data_flow: "New message notifications, mention notifications"
    notification_types:
      - chat_message
      - chat_mention

# Outbound Interfaces (same as inbound - bidirectional)
outbound:
  - name: "Staff Chat Mobile API"
    type: HTTPS
    format: REST
    doc: docs/api/staff-chat-mobile-openapi.yaml
    operations:
      - Send message (POST)
      - Edit/delete message (PATCH/DELETE)
      - Add/remove reaction (POST/DELETE)
      - Mark as read (POST)
      - Mute/unmute channel (POST)

  - name: "Ably Presence"
    type: WebSocket
    format: Ably Protocol
    doc: https://ably.com/docs/presence-occupancy/presence
    data_flow: "Publish typing status (enter/leave)"
```

### Project Commands

```bash
# Environment Setup
Install Dependencies: flutter pub get
Environment Variables: .env file with API_BASE_URL (defaults to try.buyerkiosk.com)
Start Development: flutter run

# Code Generation (CRITICAL - run after model changes)
Generate Freezed/JSON: dart run build_runner build --delete-conflicting-outputs

# Testing Commands
Unit Tests: flutter test
Widget Tests: flutter test test/widget_test.dart
Integration Tests: flutter test integration_test/

# Code Quality
Linting: flutter analyze
Type Checking: (included in flutter analyze)
Formatting: dart format lib/

# Build
Build APK (debug): flutter build apk --debug
Build iOS (debug): flutter build ios --debug --no-codesign
Build APK (release): flutter build apk --release
Build iOS (release): flutter build ios --release
```

---

## Solution Strategy

**Architecture Pattern:** Clean Architecture with Riverpod + Real-time Layer

The solution adds a **real-time service layer** between presentation and data layers to handle Ably WebSocket connections while maintaining the existing Clean Architecture pattern.

```
┌────────────────────────────────────────────────────────────────┐
│                     PRESENTATION LAYER                         │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────────┐ │
│  │ ChatScreen  │  │ ChannelList │  │ MessageComposer         │ │
│  │ (screens)   │  │ (widgets)   │  │ (widgets)               │ │
│  └──────┬──────┘  └──────┬──────┘  └───────────┬─────────────┘ │
│         │                │                      │               │
│         └────────────────┴──────────────────────┘               │
│                          │                                      │
│  ┌───────────────────────▼───────────────────────────────────┐ │
│  │              ChatNotifier (Riverpod 3.x)                   │ │
│  │  - channelsState, messagesState, mentionsState             │ │
│  │  - sendMessage(), markAsRead(), addReaction()              │ │
│  └───────────────────────┬───────────────────────────────────┘ │
└──────────────────────────┼──────────────────────────────────────┘
                           │
┌──────────────────────────┼──────────────────────────────────────┐
│                    REAL-TIME LAYER (NEW)                        │
│  ┌───────────────────────▼───────────────────────────────────┐ │
│  │           AblyRealtimeService (core/services)              │ │
│  │  - connect(), subscribe(), publishTyping()                 │ │
│  │  - Message stream, typing indicator stream                 │ │
│  └───────────────────────┬───────────────────────────────────┘ │
└──────────────────────────┼──────────────────────────────────────┘
                           │
┌──────────────────────────┼──────────────────────────────────────┐
│                      DATA LAYER                                 │
│  ┌───────────────────────▼───────────────────────────────────┐ │
│  │              ChatRepositoryImpl (data/repositories)        │ │
│  │  - getChannels(), sendMessage(), getMessages()             │ │
│  │  - Uses ApiClient for HTTP, handles error mapping          │ │
│  └───────────────────────┬───────────────────────────────────┘ │
│                          │                                      │
│  ┌───────────────────────▼───────────────────────────────────┐ │
│  │              ChatMappers (data/mappers)                    │ │
│  │  - ChannelModel → Channel, MessageModel → Message          │ │
│  └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
                           │
┌──────────────────────────┼──────────────────────────────────────┐
│                     DOMAIN LAYER                                │
│  ┌───────────────────────▼───────────────────────────────────┐ │
│  │              Entities (domain/entities)                    │ │
│  │  - Channel, Message, Reaction, Member                      │ │
│  │  - ChatState, ChannelState (sealed classes)                │ │
│  └───────────────────────────────────────────────────────────┘ │
│                                                                 │
│  ┌───────────────────────────────────────────────────────────┐ │
│  │              ChatRepository (interface)                    │ │
│  │  - Abstract contract for data operations                   │ │
│  └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```

**Integration Approach:**
1. Chat feature follows existing patterns (Notifier, Repository, sealed states)
2. New `AblyRealtimeService` handles WebSocket connections and streams
3. `ChatNotifier` coordinates between HTTP (CRUD) and WebSocket (real-time updates)
4. Existing `PushNotificationService` extended for chat notifications

**Justification:**
- Maintains consistency with existing codebase patterns
- Separates concerns: HTTP for persistence, WebSocket for real-time
- Enables future extraction to shared package
- Testable: real-time service can be mocked

**Key Decisions:**
- ADR-1: Separate AblyRealtimeService vs embedding in repository
- ADR-2: Stream-based updates vs callback pattern
- ADR-3: Local-first with sync vs remote-first architecture

---

## Building Block View

### Components

```mermaid
graph LR
    subgraph Presentation["Presentation Layer"]
        CS[ChatScreen]
        CLS[ChannelListScreen]
        MIS[MentionsInboxScreen]

        subgraph Widgets
            CLW[ChannelListWidget]
            MLW[MessageListWidget]
            MCW[MessageComposerWidget]
            MBW[MessageBubbleWidget]
            RPW[ReactionPickerWidget]
            TIW[TypingIndicatorWidget]
        end

        subgraph Providers
            CP[ChatNotifier]
            CHP[ChannelProvider]
            MSP[MessageSubmissionProvider]
        end
    end

    subgraph Services["Core Services"]
        ARS[AblyRealtimeService]
        PNS[PushNotificationService]
    end

    subgraph Data["Data Layer"]
        CRI[ChatRepositoryImpl]
        CM[ChatMappers]
        subgraph Models
            CHM[ChannelModel]
            MGM[MessageModel]
            RCM[ReactionModel]
            MBM[MemberModel]
        end
    end

    subgraph Domain["Domain Layer"]
        CR[ChatRepository<br/>interface]
        subgraph Entities
            CHE[Channel]
            MGE[Message]
            RCE[Reaction]
            MBE[Member]
            CHS[ChatState<br/>sealed]
        end
    end

    CS --> CP
    CLS --> CP
    MIS --> CP

    CP --> CRI
    CP --> ARS

    CRI --> CM
    CRI --> CR
    CM --> Models
    CM --> Entities

    ARS --> CP
    PNS --> CP
```

### Directory Map

```
lib/
├── core/
│   ├── constants/
│   │   ├── chat_constants.dart          # NEW: Rate limits, timeouts, channel naming
│   │   ├── ably_constants.dart          # MODIFY: Add chat event types
│   │   └── notification_constants.dart  # MODIFY: Add chat notification types
│   └── services/
│       ├── ably_realtime_service.dart   # NEW: Real-time messaging via Ably
│       └── notification_navigation_service.dart  # MODIFY: Add chat deep links
│
├── data/
│   ├── models/
│   │   ├── channel_model.dart           # NEW: Freezed - ChannelModel, ChannelSummaryModel
│   │   ├── message_model.dart           # NEW: Freezed - MessageModel
│   │   ├── reaction_model.dart          # NEW: Freezed - ReactionModel
│   │   ├── member_model.dart            # NEW: Freezed - ChannelMemberModel
│   │   └── mention_model.dart           # NEW: Freezed - MentionItemModel
│   ├── mappers/
│   │   └── chat_mappers.dart            # NEW: Extension mappers for all chat models
│   └── repositories/
│       └── chat_repository_impl.dart    # NEW: ChatRepository implementation
│
├── domain/
│   ├── entities/
│   │   ├── channel.dart                 # NEW: Equatable - Channel entity
│   │   ├── message.dart                 # NEW: Equatable - Message entity
│   │   ├── reaction.dart                # NEW: Equatable - Reaction entity
│   │   ├── member.dart                  # NEW: Equatable - Member entity
│   │   ├── mention.dart                 # NEW: Equatable - MentionItem entity
│   │   └── chat_state.dart              # NEW: Sealed - ChatState, ChannelState, etc.
│   └── repositories/
│       └── chat_repository.dart         # NEW: ChatRepository interface
│
├── presentation/
│   ├── providers/
│   │   ├── chat_provider.dart           # NEW: ChatNotifier + convenience providers
│   │   └── message_submission_provider.dart  # NEW: Separate submission state
│   ├── screens/
│   │   └── chat/
│   │       ├── chat_screen.dart             # REPLACE: Main chat screen with tabs
│   │       ├── channel_screen.dart          # NEW: Individual channel view
│   │       ├── channel_search_screen.dart   # NEW: Search within channel
│   │       ├── create_channel_screen.dart   # NEW: Create channel (manager+ only)
│   │       ├── channel_settings_screen.dart # NEW: Channel settings + members
│   │       └── member_profile_sheet.dart    # NEW: Profile card bottom sheet
│   └── widgets/
│       └── chat/
│           ├── channel_list_widget.dart     # NEW: Channels tab content
│           ├── channel_tile.dart            # NEW: Single channel in list
│           ├── mentions_list_widget.dart    # NEW: Mentions tab content
│           ├── message_list_widget.dart     # NEW: Scrollable message list
│           ├── message_bubble.dart          # NEW: Individual message bubble
│           ├── message_composer.dart        # NEW: Input bar with mentions
│           ├── reaction_picker.dart         # NEW: Emoji picker popover
│           ├── reaction_chips.dart          # NEW: Reaction display
│           ├── typing_indicator.dart        # NEW: "John is typing..."
│           ├── read_receipts.dart           # NEW: "Read by N" display
│           ├── mention_autocomplete.dart    # NEW: @mention dropdown
│           ├── member_list_widget.dart      # NEW: Channel member list
│           └── empty_state.dart             # NEW: Empty channel illustration
│
└── router/
    └── app_router.dart                  # MODIFY: Add chat routes
```

### Interface Specifications

#### API Reference

The complete API specification is in `docs/api/staff-chat-mobile-openapi.yaml`. Key endpoints:

| Method | Path | Purpose |
|--------|------|---------|
| GET | `/{typeNum}/channels` | List accessible channels with unread counts |
| POST | `/{typeNum}/channels` | Create channel (manager+ only) |
| GET | `/{typeNum}/channels/{id}` | Get channel details + members |
| GET | `/{typeNum}/channels/{id}/messages` | Paginated messages (before, limit) |
| POST | `/{typeNum}/channels/{id}/messages` | Send message with @mentions |
| PATCH | `/{typeNum}/messages/{id}` | Edit own message (24hr window) |
| DELETE | `/{typeNum}/messages/{id}` | Soft-delete own message |
| POST | `/{typeNum}/channels/{id}/read` | Mark messages as read |
| GET | `/{typeNum}/channels/{id}/search` | Search messages in channel |
| POST | `/{typeNum}/channels/{id}/mute` | Mute/unmute channel |
| GET | `/{typeNum}/channels/{id}/members` | List members + eligible employees |
| POST | `/{typeNum}/channels/{id}/members` | Add member (manager+ only) |
| DELETE | `/{typeNum}/channels/{id}/members/{empId}` | Remove member |
| POST | `/{typeNum}/messages/{id}/reactions` | Add emoji reaction |
| DELETE | `/{typeNum}/messages/{id}/reactions` | Remove own reaction |
| GET | `/{typeNum}/mentions` | Get mentions inbox |
| POST | `/{typeNum}/ably-token` | Get Ably token for real-time |
| GET | `/{typeNum}/attachments/{id}` | Get signed download URL |

#### Application Data Models

```pseudocode
# Freezed Models (lib/data/models/)

MODEL: ChannelSummaryModel (for channel list)
  FIELDS:
    id: int
    name: String
    description: String?
    accessLevel: String (public|manager|owner)
    isDefault: bool
    unreadCount: int
    lastMessageAt: String? (ISO-8601)
    lastMessagePreview: String?
    isMuted: bool

MODEL: ChannelModel (for channel details)
  FIELDS:
    id: int
    name: String
    description: String?
    accessLevel: String
    isDefault: bool
    retentionDays: int?
    createdAt: String (ISO-8601)

MODEL: MessageModel
  FIELDS:
    id: int
    channelId: int
    content: String (max 4000)
    senderEmployeeId: int?
    senderName: String
    senderAvatar: String?
    senderType: String (employee|system)
    createdAt: String (ISO-8601)
    editedAt: String?
    isEdited: bool
    isDeleted: bool
    reactions: List<ReactionModel>
    attachments: List<AttachmentModel>
    mentions: List<MentionReferenceModel>
    systemSourceType: String? (task|note)
    systemSourceId: int?

MODEL: ReactionModel
  FIELDS:
    emoji: String
    count: int
    users: List<ReactorModel>
    hasReacted: bool

MODEL: ChannelMemberModel
  FIELDS:
    employeeId: int
    employeeName: String
    avatarUrl: String?
    isOnline: bool

MODEL: MentionItemModel
  FIELDS:
    messageId: int
    channelId: int
    channelName: String
    content: String
    senderName: String
    senderAvatar: String?
    createdAt: String (ISO-8601)

MODEL: AttachmentModel (referenced by MessageModel)
  FIELDS:
    id: int
    fileName: String
    mimeType: String (e.g., "image/jpeg")
    fileSize: int (bytes)
    thumbnailUrl: String? (signed URL, expires 15 min)

MODEL: ReactorModel (referenced by ReactionModel)
  FIELDS:
    employeeId: int
    employeeName: String

MODEL: MentionReferenceModel (referenced by MessageModel)
  FIELDS:
    employeeId: int
    employeeName: String

MODEL: CreateChannelRequest
  FIELDS:
    name: String (1-100 chars)
    description: String? (max 500 chars)
    accessLevel: String (public|manager|owner, default: public)

MODEL: SendMessageRequest
  FIELDS:
    content: String (max 4000 chars)
    clientMessageId: String? (UUID for idempotency)

MODEL: AddReactionRequest
  FIELDS:
    emoji: String (single emoji character)

MODEL: MarkReadRequest
  FIELDS:
    lastReadMessageId: int (high-water mark)
```

```pseudocode
# Equatable Entities (lib/domain/entities/)

ENTITY: Channel
  FIELDS:
    id: int
    name: String
    description: String?
    accessLevel: ChannelAccessLevel (enum)
    isDefault: bool
    unreadCount: int
    lastMessageAt: DateTime?
    lastMessagePreview: String?
    isMuted: bool
    isPinned: bool (local-only, stored in SharedPreferences)
  COMPUTED:
    hasUnread: bool → unreadCount > 0

ENTITY: Message
  FIELDS:
    id: int
    channelId: int
    content: String
    sender: MessageSender?
    createdAt: DateTime
    editedAt: DateTime?
    isEdited: bool
    isDeleted: bool
    reactions: List<Reaction>
    attachments: List<Attachment>
    mentions: List<MentionReference>
    replyToId: int?
    isPending: bool (local state for optimistic updates)
    sendFailed: bool (local state)
  COMPUTED:
    isSystemMessage: bool → sender == null

ENTITY: Reaction
  FIELDS:
    emoji: String
    count: int
    users: List<Reactor>
    hasReacted: bool

ENTITY: Member
  FIELDS:
    employeeId: int
    name: String
    avatarUrl: String?
    isOnline: bool
    membershipType: MembershipType? (auto|manual)

ENTITY: MessageSender (referenced by Message)
  FIELDS:
    employeeId: int
    name: String
    avatarUrl: String?

ENTITY: Reactor (referenced by Reaction)
  FIELDS:
    employeeId: int
    name: String

ENTITY: Attachment (referenced by Message)
  FIELDS:
    id: int
    fileName: String
    mimeType: String
    fileSize: int
    thumbnailUrl: String?

ENTITY: MentionReference (referenced by Message)
  FIELDS:
    employeeId: int
    name: String

ENTITY: MentionItem (for mentions inbox)
  FIELDS:
    messageId: int
    channelId: int
    channelName: String
    content: String
    senderName: String
    senderAvatar: String?
    createdAt: DateTime
```

```pseudocode
# Sealed State Classes (lib/domain/entities/chat_state.dart)

SEALED: ChannelsState
  ├── ChannelsInitial
  ├── ChannelsLoading
  ├── ChannelsLoaded(channels: List<Channel>, lastUpdated: DateTime)
  └── ChannelsError(message: String, canRetry: bool)

SEALED: ChannelMessagesState
  ├── ChannelMessagesInitial
  ├── ChannelMessagesLoading
  ├── ChannelMessagesLoaded(
        messages: List<Message>,
        hasMore: bool,
        readReceipts: Map<int, List<Member>>,
        typingUsers: List<Member>
      )
  └── ChannelMessagesError(message: String, canRetry: bool)

SEALED: MentionsState
  ├── MentionsInitial
  ├── MentionsLoading
  ├── MentionsLoaded(mentions: List<MentionItem>, hasMore: bool)
  └── MentionsError(message: String)

SEALED: MessageSubmissionState
  ├── MessageSubmissionIdle
  ├── MessageSubmissionSending
  ├── MessageSubmissionSuccess(message: Message)
  └── MessageSubmissionError(message: String, content: String, canRetry: bool)

ENUM: ChannelAccessLevel { public, manager, owner }
ENUM: MembershipType { auto, manual }
```

#### Integration Points

```yaml
# Real-time via Ably
ably_realtime:
  token_endpoint: POST /{typeNum}/ably-token
  channels:
    - pattern: "chat:{typeNum}:{channelId}"
      events:
        - name: message.created
          data: MessageModel JSON
        - name: message.updated
          data: { messageId, content, editedAt }
        - name: message.deleted
          data: { messageId }
        - name: reaction.added
          data: { messageId, emoji, userId, userName }
        - name: reaction.removed
          data: { messageId, emoji, userId }
        - name: read.updated
          data: { messageId, userId, userName }
    - pattern: "chat:{typeNum}:{channelId}:presence"
      events:
        - enter: User started typing
        - leave: User stopped typing
        - member_list: Current typing users

# Push Notifications (FCM)
push_notifications:
  new_types:
    - type: chat_message
      deep_link: /chat/{channelId}?messageId={messageId}
      payload: { channelId, channelName, senderName, preview }
    - type: chat_mention
      deep_link: /chat/{channelId}?messageId={messageId}
      payload: { channelId, channelName, senderName, preview }
      priority: high (bypasses mute)

# Internal Integration
internal:
  - from: ChatNotifier
    to: AblyRealtimeService
    protocol: Dart Streams
    data_flow: "Subscribe to message/typing streams, publish typing status"

  - from: ChatNotifier
    to: ChatRepository
    protocol: Dart async/await
    data_flow: "CRUD operations for channels, messages, reactions"

  - from: PushNotificationService
    to: ChatNotifier
    protocol: Riverpod ref.read
    data_flow: "Trigger refresh on chat notifications"
```

### Detailed Behavior Specifications

#### Ably Presence Mitigation (100-member limit)

Ably presence has a 100-member limit per channel. For channels exceeding this:

**Mitigation Strategy:**
1. **Typing indicators via presence** for channels with ≤100 members (most channels)
2. **Fallback for large channels (>100 members):**
   - Disable presence-based typing (don't attempt enter/leave)
   - Show static message: "Typing indicators unavailable for large channels"
   - Alternative: Use regular channel messages for typing events (less real-time, but works)
3. **Detection:** Check member count from channel details API before enabling presence

```pseudocode
METHOD: shouldEnableTypingPresence(channelMemberCount: int) -> bool
  RETURN channelMemberCount <= 100
```

#### Pin/Unpin Channel UX

**Interaction Flow:**
1. Long-press channel tile in list → Show context menu
2. Context menu options: "Pin to top" / "Unpin"
3. On tap: Update local SharedPreferences, reorder list immediately

**Storage:**
```dart
// Key: 'pinned_channels_{typeNum}'
// Value: JSON array of channel IDs in pin order
['12', '45', '3']  // First = top of pinned section
```

**Display Logic:**
```pseudocode
FUNCTION: sortChannels(channels: List<Channel>) -> List<Channel>
  pinnedIds = loadPinnedChannelIds()
  pinned = channels.filter(c => pinnedIds.contains(c.id))
                   .sortBy(c => pinnedIds.indexOf(c.id))
  unpinned = channels.filter(c => !pinnedIds.contains(c.id))
                     .sortBy(c => c.lastMessageAt DESC)
  RETURN [...pinned, ...unpinned]
```

#### Read Receipt Semantics

**High-water Mark Approach:**
- Client tracks `lastReadMessageId` per channel per user
- On viewport visibility (≥500ms): POST `/channels/{id}/read` with highest visible message ID
- Backend calculates read count: count of users whose `lastReadMessageId >= message.id`

**Display Rules:**
| Message Position | Display | Tap Action |
|------------------|---------|------------|
| Own message, read by ≥1 | "Read by N" | Show reader list modal |
| Own message, unread | "Sent" or "Delivered" | None |
| Others' message | No receipt shown | N/A |

**Multi-device Sync:**
- Reading on device A updates `lastReadMessageId` on server
- Device B receives real-time `read.updated` event via Ably
- Device B marks messages as read locally without re-posting

#### Notification Grouping (Backend Responsibility)

Per PRD, notification grouping is handled by backend:
- Backend batches messages from same channel within 30s window
- Sends single FCM with: `"3 new messages in #General"`
- Client does NOT perform grouping (receives pre-grouped notifications)

**Client Responsibilities:**
1. Display notification as received
2. Handle tap → deep link to channel
3. Clear badge on channel open

#### Analytics Event Mapping (PRD → Implementation)

| PRD Event | Implementation | Properties |
|-----------|---------------|------------|
| `channel_opened` | `AnalyticsService.logChannelOpened()` | `channelId`, `source` (list/notification/deeplink) |
| `message_sent` | `AnalyticsService.logMessageSent()` | `channelId`, `hasMentions`, `hasAttachment` |
| `message_received` | `AnalyticsService.logMessageReceived()` | `channelId`, `isMention`, `latencyMs` |
| `reaction_added` | `AnalyticsService.logReactionAdded()` | `messageId`, `emoji` |
| `mention_tapped` | `AnalyticsService.logMentionTapped()` | `source` (inbox/inline) |
| `role_mention_sent` | `AnalyticsService.logRoleMentionSent()` | `role` (managers/staff), `channelId` |
| `channel_muted` | `AnalyticsService.logChannelMuted()` | `channelId` |
| `channel_pinned` | `AnalyticsService.logChannelPinned()` | `channelId`, `pinned` (true/false) |
| `search_performed` | `AnalyticsService.logSearchPerformed()` | `queryLength`, `resultCount` |

**Integration:** Add methods to existing `AnalyticsService` class.

#### Profile Card Popup

**Trigger:** Tap on avatar/name in message or tap on @mention

**Display:** Bottom sheet modal with:
- Large avatar (80px)
- Full name + role badge
- "Message" button (future: opens DM when DMs are implemented)
- "Close" button

**Implementation:** `MemberProfileSheet` widget shown via `showModalBottomSheet()`

#### Token Capability Scope & Refresh

**Token Scoping:**
- Ably token is scoped to channels user can access at token issue time
- Token includes capabilities like: `"chat:ou00:*": ["subscribe"]`
- If user role changes mid-session, old subscriptions remain until reconnect

**Access Revocation Handling:**
1. Backend sends `channel.access_revoked` event via existing subscription
2. Client receives event, shows: "You no longer have access to #ChannelName"
3. Client unsubscribes from that channel, removes from local state
4. Redirect to channel list if viewing revoked channel

**Token Refresh:**
- Ably SDK handles refresh via `authCallback` in `ClientOptions`
- On refresh, backend issues token with current access rights
- Subscriptions to revoked channels will fail silently (expected)

### Implementation Examples

#### Example: AblyRealtimeService Connection Pattern

**Why this example**: Demonstrates the token refresh, connection management, and stream pattern for real-time updates.

```dart
// lib/core/services/ably_realtime_service.dart

class AblyRealtimeService {
  final Ref _ref;
  ably.Realtime? _realtime;
  final Map<String, StreamController<ChatEvent>> _channelStreams = {};

  AblyRealtimeService(this._ref);

  /// Connect to Ably with token auth
  Future<void> connect(String typeNum) async {
    final tokenResponse = await _getAblyToken(typeNum);

    _realtime = ably.Realtime(
      options: ably.ClientOptions(
        tokenDetails: ably.TokenDetails(tokenResponse.token),
        autoConnect: true,
        // Token refresh callback
        authCallback: (params) async {
          final newToken = await _getAblyToken(typeNum);
          return ably.TokenDetails(newToken.token);
        },
      ),
    );

    // Monitor connection state
    _realtime!.connection.on().listen((event) {
      if (event.current == ably.ConnectionState.disconnected) {
        _handleDisconnect();
      } else if (event.current == ably.ConnectionState.connected) {
        _handleReconnect();
      }
    });
  }

  /// Subscribe to channel messages - returns stream
  Stream<ChatEvent> subscribeToChannel(int channelId, String typeNum) {
    final channelName = 'chat:$typeNum:$channelId';

    if (!_channelStreams.containsKey(channelName)) {
      final controller = StreamController<ChatEvent>.broadcast();
      _channelStreams[channelName] = controller;

      final channel = _realtime!.channels.get(channelName);
      channel.subscribe().listen((message) {
        final event = _parseEvent(message);
        controller.add(event);
      });
    }

    return _channelStreams[channelName]!.stream;
  }

  /// Publish typing indicator via presence
  Future<void> enterTyping(int channelId, String typeNum, String userName) async {
    final presenceChannel = 'chat:$typeNum:$channelId:presence';
    final channel = _realtime!.channels.get(presenceChannel);
    await channel.presence.enter({'name': userName});
  }

  Future<void> leaveTyping(int channelId, String typeNum) async {
    final presenceChannel = 'chat:$typeNum:$channelId:presence';
    final channel = _realtime!.channels.get(presenceChannel);
    await channel.presence.leave();
  }
}
```

#### Example: Optimistic Message Sending

**Why this example**: Shows the critical optimistic update pattern for responsive UX.

```dart
// In ChatNotifier

Future<void> sendMessage(int channelId, String content, {String? clientMessageId}) async {
  final messageId = clientMessageId ?? const Uuid().v4();
  final currentUser = _ref.read(currentUserProvider)!;

  // 1. Create optimistic message
  final optimisticMessage = Message(
    id: -1, // Temporary ID
    channelId: channelId,
    content: content,
    sender: MessageSender(
      employeeId: currentUser.id,
      name: currentUser.fullName,
      avatarUrl: currentUser.avatarUrl,
    ),
    createdAt: DateTime.now(),
    isPending: true,
    sendFailed: false,
  );

  // 2. Add to state immediately (optimistic)
  _addMessageToList(channelId, optimisticMessage, messageId);

  try {
    // 3. Send via API
    final sentMessage = await _chatRepo.sendMessage(
      channelId: channelId,
      content: content,
      clientMessageId: messageId,
    );

    // 4. Replace optimistic with real message
    _updateMessage(channelId, messageId, sentMessage.copyWith(isPending: false));

  } catch (e) {
    // 5. Mark as failed (keep in list for retry)
    _updateMessageFailed(channelId, messageId, content);

    _ref.read(messageSubmissionStateProvider.notifier).setError(
      e.toString(),
      content,
    );
  }
}
```

#### Example: Swipe Gesture Handler

**Why this example**: Swipe gestures (react right, reply left) are core UX per PRD.

```dart
// In MessageBubble widget

GestureDetector(
  onHorizontalDragEnd: (details) {
    final velocity = details.primaryVelocity ?? 0;

    if (velocity > 300) {
      // Swipe right → React
      _showReactionPicker(context, message);
    } else if (velocity < -300) {
      // Swipe left → Reply
      onReply?.call(message);
    }
  },
  child: _buildBubbleContent(message),
)
```

---

## Runtime View

### Primary Flow: Send Message

1. User types message in `MessageComposer`
2. User taps send button
3. `ChatNotifier.sendMessage()` called
4. Optimistic message added to state (UI updates immediately)
5. API call: `POST /{typeNum}/channels/{id}/messages`
6. On success: Replace optimistic with real message
7. On failure: Mark message as failed, show retry button
8. Real-time: Other users receive via Ably subscription
9. Read receipt: Auto-sent after 500ms viewport visibility

```mermaid
sequenceDiagram
    actor User
    participant Composer as MessageComposer
    participant Notifier as ChatNotifier
    participant Repo as ChatRepository
    participant API as Backend API
    participant Ably as Ably Realtime
    participant Others as Other Users

    User->>Composer: Type message + tap send
    Composer->>Notifier: sendMessage(content)

    Note over Notifier: Optimistic Update
    Notifier->>Notifier: Add pending message to state
    Notifier-->>Composer: UI shows message immediately

    Notifier->>Repo: sendMessage(channelId, content, clientMessageId)
    Repo->>API: POST /channels/{id}/messages
    API-->>Repo: 201 Created + Message
    Repo-->>Notifier: Message entity

    Note over Notifier: Confirm Update
    Notifier->>Notifier: Replace pending with confirmed
    Notifier-->>Composer: Remove sending indicator

    Note over API: Broadcast via Ably
    API->>Ably: Publish message.created
    Ably->>Others: Real-time delivery
```

### Secondary Flow: Receive Real-time Message

1. Ably channel receives `message.created` event
2. `AblyRealtimeService` emits event on stream
3. `ChatNotifier` receives stream event
4. Message parsed and added to state
5. If channel in view: Scroll to new message
6. If scrolled up: Show "Jump to new" button
7. If channel not in view: Update unread count

```mermaid
sequenceDiagram
    participant Ably as Ably Realtime
    participant Service as AblyRealtimeService
    participant Notifier as ChatNotifier
    participant UI as MessageListWidget

    Ably->>Service: message.created event
    Service->>Service: Parse to ChatEvent
    Service->>Notifier: Stream emission

    Notifier->>Notifier: Add message to state
    Notifier->>Notifier: Update channel unread count

    Notifier-->>UI: State change triggers rebuild

    alt Scrolled to bottom
        UI->>UI: Auto-scroll to new message
    else Scrolled up
        UI->>UI: Show "Jump to new" FAB
    end
```

### Error Handling

```yaml
# Network Errors
network_unavailable:
  detection: DioException.type == connectionError
  ui_message: "Unable to connect. Please check your internet connection."
  recovery: Show retry button, enable offline mode (read-only)

request_timeout:
  detection: DioException.type == receiveTimeout
  ui_message: "Request timed out. Please try again."
  recovery: Show retry button on affected action

# API Errors
rate_limited:
  detection: HTTP 429
  ui_message: "Slow down! You can send another message in {retryAfter} seconds."
  recovery: Disable send button until retryAfter expires

edit_window_expired:
  detection: HTTP 422 with error_code == "edit_window_expired"
  ui_message: "This message can no longer be edited. You can send a new message instead."
  recovery: Dismiss edit mode, offer to send as new message

session_expired:
  detection: HTTP 401 after token refresh attempt
  ui_message: "Your session has expired. Please sign in again."
  recovery: Redirect to login via AuthNotifier.handleAccessRevoked()

# Real-time Errors
ably_disconnect:
  detection: ConnectionState.disconnected
  ui_message: "Reconnecting..." (toast/banner)
  recovery: Auto-reconnect with exponential backoff

ably_reconnect_after_long_disconnect:
  detection: Reconnected after >5 minutes
  recovery: Fetch missed messages via API before resuming real-time

# Message Send Failures
send_failed:
  detection: API error during sendMessage
  ui_message: Inline error below message + "Tap to retry"
  recovery: Keep message in list with sendFailed=true, allow retry
```

---

## Deployment View

**No change to existing deployment.** Chat feature is a client-side addition that:
- Uses existing API client infrastructure
- Integrates with existing FCM push notification handling
- Adds new Ably real-time connection (client-side only)

---

## Cross-Cutting Concepts

### Pattern Documentation

```yaml
# Existing patterns used
- pattern: Clean Architecture (core → data → domain → presentation)
  relevance: CRITICAL
  why: "Maintain consistency with existing codebase structure"

- pattern: Riverpod 3.x Notifier Pattern
  relevance: CRITICAL
  why: "State management pattern used throughout app"

- pattern: Sealed Classes for State Machines
  relevance: HIGH
  why: "Type-safe state handling (AuthState, AvatarState pattern)"

- pattern: Extension Mappers
  relevance: HIGH
  why: "Model-to-entity conversion (shift_request_mappers.dart pattern)"

- pattern: Repository Pattern
  relevance: HIGH
  why: "Data access abstraction with interface in domain layer"

# New patterns
- pattern: Real-time Stream Integration
  relevance: CRITICAL
  why: "New pattern for Ably WebSocket + Riverpod integration"
  doc: Implementation example in AblyRealtimeService section
```

### System-Wide Patterns

**Security:**
- JWT Bearer token authentication (existing)
- Ably token-based auth (scoped to accessible channels)
- No client-side message encryption (backend handles at rest)

**Error Handling:**
- Follow existing `AppException` hierarchy
- Add `ChatException` subclass with factory constructors:
  - `ChatException.rateLimited()`
  - `ChatException.editWindowExpired()`
  - `ChatException.channelNotFound()`

**Performance:**
- Cache last 100 messages per channel in memory
- Pagination with `before` cursor (load 50 messages per request)
- Image thumbnails lazy-loaded with `CachedNetworkImage`
- Typing indicator debounce (300ms before enter, 3s auto-leave)

**Logging/Auditing:**
- Use existing `LoggingInterceptor` for API calls
- Add analytics events per PRD tracking requirements

### Implementation Patterns

#### State Management Pattern

```pseudocode
# ChatNotifier manages multiple sub-states
CLASS: ChatNotifier extends Notifier<ChatCompositeState>

  STATE: ChatCompositeState
    channelsState: ChannelsState
    activeChannelId: int?
    messagesState: ChannelMessagesState
    mentionsState: MentionsState

  METHOD: loadChannels()
    state = state.copyWith(channelsState: ChannelsLoading())
    TRY:
      channels = await _chatRepo.getChannels(typeNum)
      state = state.copyWith(channelsState: ChannelsLoaded(channels))
    CATCH NetworkException:
      state = state.copyWith(channelsState: ChannelsError(message, canRetry: true))

  METHOD: enterChannel(channelId)
    state = state.copyWith(
      activeChannelId: channelId,
      messagesState: ChannelMessagesLoading()
    )

    # Start real-time subscription
    _ablyService.subscribeToChannel(channelId, typeNum).listen(_onRealtimeEvent)

    # Load initial messages
    messages = await _chatRepo.getMessages(channelId)
    state = state.copyWith(messagesState: ChannelMessagesLoaded(messages))
```

#### Error Handling Pattern

```pseudocode
# Add ChatException to lib/core/errors/app_exceptions.dart

CLASS: ChatException extends AppException

  FACTORY: rateLimited(retryAfterSeconds)
    message: "Rate limited. Try again in $retryAfterSeconds seconds."
    code: "RATE_LIMITED"

  FACTORY: editWindowExpired()
    message: "Edit window has expired (24 hours)."
    code: "EDIT_WINDOW_EXPIRED"

  FACTORY: channelNotFound()
    message: "Channel not found or no longer accessible."
    code: "CHANNEL_NOT_FOUND"
```

---

## Architecture Decisions

- [x] **ADR-1: Separate AblyRealtimeService vs Repository Integration**
  - **Choice:** Separate `AblyRealtimeService` in `core/services/`
  - **Rationale:**
    - Separation of concerns: HTTP (CRUD) vs WebSocket (real-time)
    - Testability: Can mock Ably service independently
    - Reusability: Same service works for both Team and Live apps
  - **Trade-offs:**
    - Additional complexity in `ChatNotifier` coordinating two data sources
    - Need to handle reconnection and sync logic
  - **User confirmed:** ✅ 2026-01-02

- [x] **ADR-2: Stream-based Real-time Updates vs Callback Pattern**
  - **Choice:** Dart Streams for Ably events
  - **Rationale:**
    - Natural fit with Riverpod's reactive paradigm
    - Easy to combine/transform with other streams
    - Proper lifecycle management with StreamSubscription
  - **Trade-offs:**
    - More boilerplate for stream controllers
    - Need careful subscription cleanup
  - **User confirmed:** ✅ 2026-01-02

- [x] **ADR-3: Optimistic Updates with Local ID Tracking**
  - **Choice:** Use `clientMessageId` (UUID) for optimistic updates
  - **Rationale:**
    - Immediate UI feedback for responsive UX
    - Backend supports idempotency via clientMessageId
    - Clean replacement when server confirms
  - **Trade-offs:**
    - Need to handle failed sends (keep in list with retry)
    - Temporary IDs must not conflict with real IDs
  - **User confirmed:** ✅ 2026-01-02

- [x] **ADR-4: Composite State vs Multiple Providers**
  - **Choice:** Single `ChatNotifier` with composite state
  - **Rationale:**
    - Follows pattern established in `AuthNotifier`
    - Easier to coordinate related state changes
    - Reduces provider count and dependency complexity
  - **Trade-offs:**
    - Larger state object
    - All subscribers notified on any state change (mitigate with `select`)
  - **User confirmed:** ✅ 2026-01-02 (implicit - follows existing pattern)

- [x] **ADR-5: In-memory Message Cache vs Local Database**
  - **Choice:** In-memory cache only (no SQLite/Hive)
  - **Rationale:**
    - Simpler implementation for v1
    - Messages always fresh from server on re-enter
    - PRD specifies read-only offline mode (no compose)
  - **Trade-offs:**
    - No true offline message history
    - Re-fetch on every channel enter
    - May revisit for v2 with full offline support
  - **User confirmed:** ✅ 2026-01-02

---

## Quality Requirements

**Performance:**
- Message list scroll: 60fps with 500+ messages in view
- Message send: Optimistic update visible <100ms from tap
- Channel list load: <500ms on 4G network
- Real-time message delivery: p95 <500ms (Ably SLA)

**Usability:**
- All interactive elements meet 44pt minimum tap target
- Keyboard avoidance for message composer
- Pull-to-refresh on channel list and message list
- Swipe gestures work with 300+ velocity threshold

**Reliability:**
- Auto-reconnect on network restoration
- Read-only offline mode (no message queue in v1 per PRD decision)
- Graceful degradation when Ably unavailable (polling fallback at 30s)

**Accessibility:**
- All images have semantic labels
- Screen reader announces new messages
- High contrast mode support via system theme

---

## Risks and Technical Debt

### Known Technical Issues

- **No existing Ably service:** Need to build connection management, token refresh, and stream handling from scratch
- **Typing indicator performance:** Presence updates can be chatty; need debouncing

### Technical Debt

- **Placeholder chat screen:** Must be completely replaced
- **Ably constants incomplete:** Only has schedule/clock events, needs chat events

### Implementation Gotchas

- **Ably token expiration:** Tokens expire; must handle `authCallback` for seamless refresh
- **Message ordering:** Ably may deliver out-of-order on reconnect; sort by server timestamp
- **Presence limitations:** Ably presence has 100-member limit per channel for presence tracking
- **iOS keyboard:** Must handle safe area insets for message composer
- **Android back button:** Need to handle back from channel to list correctly
- **Deep link race:** If app opens from notification before chat loads, need to queue navigation

---

## Test Specifications

### Critical Test Scenarios

**Scenario 1: Send Message Happy Path**
```gherkin
Given: User is authenticated and viewing a channel
And: Network is available
When: User types "Hello team" and taps send
Then: Message appears immediately in list with pending indicator
And: After API response, pending indicator is removed
And: Message has correct timestamp and sender info
```

**Scenario 2: Send Message Offline**
```gherkin
Given: User is authenticated and viewing a channel
And: Network is unavailable
When: User types "Hello team" and taps send
Then: Error message is displayed "Unable to send. Check your connection."
And: Message is NOT added to list (read-only offline for v1)
```

**Scenario 3: Real-time Message Reception**
```gherkin
Given: User A and User B are viewing the same channel
When: User A sends a message
Then: User B sees the message appear within 2 seconds
And: User B sees the correct sender name and avatar
```

**Scenario 4: Rate Limit Handling**
```gherkin
Given: User has sent 30 messages in the last minute
When: User attempts to send another message
Then: Error is displayed "Slow down! You can send another message in X seconds"
And: Send button is disabled until cooldown expires
```

**Scenario 5: Edit Message Within Window**
```gherkin
Given: User sent a message 1 hour ago
When: User long-presses and selects "Edit"
And: User modifies the content and confirms
Then: Message is updated with "(edited)" indicator
And: Other users see the updated content
```

**Scenario 6: Edit Message After Window Expired**
```gherkin
Given: User sent a message 25 hours ago
When: User long-presses and selects "Edit"
Then: Error is displayed "This message can no longer be edited"
And: Edit mode is dismissed
```

### Test Coverage Requirements

- **Business Logic:**
  - All message state transitions (pending → confirmed → failed)
  - Reaction add/remove logic
  - Read receipt marking (500ms viewport rule)
  - @mention parsing and autocomplete

- **User Interface:**
  - Channel list with unread badges
  - Message bubble alignment (own vs others)
  - Swipe gestures (left = reply, right = react)
  - Typing indicator animation
  - Empty state display

- **Integration Points:**
  - API calls with mock responses
  - Ably stream event handling
  - Push notification deep links
  - Auth interceptor token refresh

- **Edge Cases:**
  - Very long messages (4000 chars)
  - Rapid message sending (rate limit)
  - Reconnect after long disconnect
  - Channel access revoked mid-session

---

## Glossary

### Domain Terms

| Term | Definition | Context |
|------|------------|---------|
| Channel | A chat room/conversation within a store | Users see channels in the channel list; channels have access levels |
| Mention | An @reference to a user (e.g., @john.smith) | Triggers notification; appears in mentions inbox |
| Role Mention | @managers or @staff group mention | Notifies all users with that role in the channel |
| Read Receipt | Acknowledgment that a user has seen a message | Displayed as "Read by N" below messages |
| Mute | Suppress push notifications for a channel | Mentions still notify; badge still updates |
| Pin | User-favorited channel shown at top of list | Local preference stored in SharedPreferences |

### Technical Terms

| Term | Definition | Context |
|------|------------|---------|
| Optimistic Update | UI shows change before server confirms | Used for message sending for instant feedback |
| Client Message ID | UUID sent with message for deduplication | Enables idempotent retries; links optimistic to confirmed |
| Presence | Ably feature for real-time user status | Used for typing indicators |
| High-water Mark | Last read message ID for a user in a channel | Sent with read receipt; determines unread count |

### API/Interface Terms

| Term | Definition | Context |
|------|------------|---------|
| typeNum | Store identifier (e.g., "ou00", "pa00") | Required path parameter for all store-scoped endpoints |
| Access Level | Channel visibility (public, manager, owner) | Determines who can see and join the channel |
| Ably Token | Short-lived auth token for real-time connection | Obtained via POST /ably-token; auto-refreshed via callback |

---

## PRD Traceability Matrix

### Must Have Features

| PRD Feature | SDD Section | Status |
|-------------|-------------|--------|
| F1: Channel List View | Directory Map (screens, widgets), Entities (Channel) | ✅ Designed |
| F2: Message Viewing | Directory Map (MessageBubble, MessageList), Runtime View | ✅ Designed |
| F3: Send Messages | Runtime View (Send Message flow), Optimistic Updates | ✅ Designed |
| F4: Emoji Reactions | Data Models (ReactionModel), Widgets (reaction_picker) | ✅ Designed |
| F5: Push Notifications | Integration Points (FCM), Notification Types | ✅ Designed |
| F6: Real-time Updates | AblyRealtimeService, Stream pattern, ADR-2 | ✅ Designed |

### Should Have Features

| PRD Feature | SDD Section | Status |
|-------------|-------------|--------|
| F7: Message Search | Screens (channel_search_screen) | ✅ Designed |
| F8: Edit/Delete Messages | Error Handling (edit window), API endpoints | ✅ Designed |
| F9: Mentions Inbox | Entities (MentionItem), Widgets (mentions_list) | ✅ Designed |
| F10: Channel Muting | API endpoints, Notification priority rules | ✅ Designed |

### Could Have Features (Scope Notes)

| PRD Feature | SDD Status | Implementation Note |
|-------------|------------|---------------------|
| F11: Image Attachments | ⏳ Deferred | Models defined (AttachmentModel), UI not designed. Implement post-MVP. |
| F12: Reply Threading | ⏳ Deferred | `replyToId` field in Message entity. Quote UI not fully designed. |
| F13: Link Previews | ⏳ Deferred | Not designed. Backend would provide preview metadata. |

### Channel Management (PRD Lifecycle)

| PRD Requirement | SDD Section | Status |
|-----------------|-------------|--------|
| Channel Creation (manager+) | Screens (create_channel_screen), Models (CreateChannelRequest) | ✅ Designed |
| Member Management | Screens (channel_settings_screen, member_list_widget) | ✅ Designed |
| Channel Archive/Unarchive | API endpoints listed | ✅ API mapped, UI minimal |
| Permissions Matrix | Not explicitly modeled | ⚠️ Backend enforces; client respects API errors |

### Cross-Cutting Concerns

| PRD Requirement | SDD Section | Status |
|-----------------|-------------|--------|
| Dark Mode | Constraints (CON-6) | ✅ Required at launch |
| Read-only Offline | ADR-5, Quality Requirements | ✅ Designed |
| Rate Limits | Constraints (CON-7), Error Handling | ✅ Designed |
| Analytics Events | Detailed Behavior Specs (Analytics Event Mapping) | ✅ Mapped |
| Shared Package | Shared Package Plan section | ✅ Designed |

---

## Appendix: Deferred Features Detail

### Image Attachments (F11)
**PRD Location:** `product-requirements.md:191-199`

**Designed:**
- `AttachmentModel` with id, fileName, mimeType, fileSize, thumbnailUrl
- API endpoint: GET `/attachments/{id}` for signed download URL
- Message entity includes `attachments: List<Attachment>`

**Not Designed (Post-MVP):**
- Camera/gallery picker in composer
- Upload progress UI
- Thumbnail rendering in message bubble
- Full-screen gallery view

### Reply Threading (F12)
**PRD Location:** `product-requirements.md:200-207`

**Designed:**
- `replyToId: int?` field in Message entity
- Swipe-left gesture triggers reply

**Not Designed (Post-MVP):**
- Quote preview bar widget
- "Jump to original" navigation
- Reply count/thread indicators

### Link Previews (F13)
**PRD Location:** `product-requirements.md:208-215`

**Not Designed:** Full implementation deferred to post-MVP.
- Backend would extract metadata (og:title, og:image, og:description)
- Client would render preview card below message
- Tap opens URL in external browser
