# Chat Technical Design

This document describes how chat is implemented in this Laravel application. It is based on the current code paths in `app/Http/Controllers/ChatController.php`, `app/Support/ChatWidgetDataProvider.php`, `app/Services/Chat/*`, `resources/views/dashboard/layout/partials/chat-widget.blade.php`, `resources/js/chat.js`, and `resources/css/chat-widget.css`.

## Scope

The application chat is an admin/manager block-chat widget. It is not a standalone chat page and it is not implemented with Livewire.

Current production-facing chat behavior is centered on block conversations with this conversation data:

```php
[
    'title' => '...',
    'chat_type' => 'block_auto',
    'block_id' => 123,
]
```

Only users with `Admin` or `Manager` roles can use chat endpoints. Regular users are not allowed to access chat.

## Main Components

### Backend

- `app/Http/Controllers/ChatController.php` handles chat creation, message CRUD, message polling, seen state, presence, attachments, and broadcast notifications.
- `app/Support/ChatWidgetDataProvider.php` prepares all server-rendered data for the chat widget view composer.
- `app/Services/Chat/BlockConversationResolver.php` finds block-backed chat conversations by `data.chat_type = block_auto` and `data.block_id`.
- `app/Services/Chat/BlockChatParticipantSyncService.php` keeps block chat participants synchronized when block manager assignments change.
- `app/Services/Chat/ChatPresenceService.php` tracks active site/chat presence in cache.
- `app/Services/Chat/ChatPushNotificationService.php` sends Pusher Beams push notifications for offline recipients.
- `app/Events/ChatMessageNotification.php` broadcasts real-time in-browser chat notifications to private user channels.
- `app/Events/ChatMessagesSeen.php` broadcasts real-time read receipts to message senders.
- `app/Http/Requests/Chat/*` contains validation and role-level authorization for chat actions.

### Frontend

- `resources/views/dashboard/layout/partials/chat-widget.blade.php` renders the widget shell, conversation list, creation form, current thread, participants, message search, and composer.
- `resources/js/chat.js` owns widget state, message polling, Echo subscriptions, Pusher Beams initialization, unread badges, presence heartbeats, attachments, replies, edit/delete actions, and chat-list search.
- `resources/css/chat-widget.css` contains all chat widget styling.

### Third-Party Package

Chat persistence uses the `musonza/chat` package through `Musonza\Chat\Facades\ChatFacade` and package models:

- `Musonza\Chat\Models\Conversation`
- `Musonza\Chat\Models\Message`
- `Musonza\Chat\Models\Participation`

The package table names come from `Musonza\Chat\ConfigurationManager`.

## Database Model

The chat package migration creates these core tables:

- `chat_conversations`: one row per conversation. The application stores chat metadata in the `data` JSON/text column.
- `chat_participation`: one row per user in a conversation. This is the primary access-control table.
- `chat_messages`: one row per message. Each message belongs to a conversation and has a `participation_id` for the sender.
- `chat_message_notifications`: one row per message recipient/participant. This table controls per-user message visibility, unread counts, and seen state.

Important application-added columns:

- `chat_messages.edited_at`: marks edited text messages.
- `chat_messages.deleted_at`: soft-deletes messages.
- `chat_conversations.name`: exists from a later migration, but current block-chat title is stored in `data.title`.

## Access Model

There are two layers of access control.

### Role Gate

Every controller action calls or relies on request authorization that only allows admins and managers. `ChatController::ensureChatRole()` rejects everyone else.

### Conversation Membership

`ChatController::resolveConversation()` loads the conversation and verifies the current user exists in `chat_participation` for that conversation. If not, the request gets `403`.

This means conversation membership is not only a UI concern. A removed manager cannot fetch messages, send messages, mark seen, update presence for that conversation, or download attachments because all of those routes resolve the conversation through participant membership.

## Block Chat Creation

Only admins can create block chats. This is enforced by `CreateBlockChatRequest::authorize()`.

Creation route:

```text
POST /chats -> chats.storeConversation
```

Creation flow:

1. The admin submits a `block_id` and optional `title`.
2. `ChatController::storeConversation()` checks whether a block chat already exists through `BlockConversationResolver::findForBlock()`.
3. Participants are resolved by `ChatController::blockParticipants()`.
4. The conversation is created through `Chat::createConversation($participants)->makePrivate()`.
5. Conversation metadata is saved in `data` with `chat_type = block_auto` and `block_id`.

Participant resolution includes:

- Managers assigned through `block_user`.
- The block main manager in `blocks.head_id`.
- Additional managers in `block_additional_managers`.
- An admin participant. If the creator is an admin, that admin is included; otherwise the first admin by id is used.
- The requesting user, deduplicated by id.

The widget hides blocks that already have a block chat by using `BlockConversationResolver::occupiedBlockIds()` in `ChatWidgetDataProvider::accessibleBlocks()`.

## Manager Assignment Sync

Block manager changes must update chat access. This is handled by `BlockChatParticipantSyncService`, called from `BlocksController::update()` after the block, user assignments, roles, and additional managers are saved.

Sync flow:

1. Find the existing `block_auto` conversation for the block.
2. Resolve the current manager set from:
   - `blocks.head_id`
   - `block_additional_managers.user_id`
   - managers assigned through `block_user`
3. Compare current managers with manager participants already in `chat_participation`.
4. Add new managers to the conversation using the Musonza chat participant API.
5. Remove managers who are no longer assigned.
6. Soft-delete removed managers' existing `chat_message_notifications` rows for that conversation.

New managers intentionally do not receive notification rows for historical messages. Because message visibility is based on `chat_message_notifications`, this means a new manager only sees messages sent after they join.

Removed managers lose access at both levels:

- Their `chat_participation` row is deleted, so `resolveConversation()` rejects them.
- Their existing notification rows are soft-deleted, so old message visibility and unread state are removed.

Admins are not removed by this service. It only synchronizes manager participants.

## Additional Managers in Chat Context

Additional managers are first-class chat managers for a block, not secondary UI-only labels.

They are stored in `block_additional_managers` and are included anywhere the chat system resolves block manager access.

Additional-manager handling exists in these places:

- `ChatController::blockParticipants()` includes additional managers when an admin creates a block chat.
- `ChatController::canAccessBlock()` treats additional managers as allowed managers for the block.
- `ChatWidgetDataProvider::assignedManagerBlockIds()` includes blocks where the current user is an additional manager.
- `BlockChatParticipantSyncService::currentManagers()` includes additional managers when syncing existing chat participants after block edits.

Practical behavior:

- If a user is added as `additional_head_1` or `additional_head_2`, they should gain access to the block chat after the block update sync runs.
- If a user is removed from `additional_head_1` or `additional_head_2` and is not otherwise assigned to the block, they should lose access to the block chat.
- If the same user is both in `block_user` and `block_additional_managers`, they are deduplicated at participant resolution/sync time.
- A new additional manager does not receive historical message notification rows, so they only see future messages.
- A removed additional manager has their participant row removed and their existing notification rows soft-deleted for that conversation.

Important distinction:

- `block_user` is not the only source of chat manager access.
- `blocks.head_id` and `block_additional_managers.user_id` are also authoritative for chat access.

This matters because the block edit form has separate controls for main/additional managers and the block users multi-select. A manager can be chat-authorized even if they are not selected in the users multi-select.

## Widget Data Provider

`AppServiceProvider` registers a view composer for:

- `dashboard.layout.layout`
- `dashboard.layout.partials.chat-widget`

The composer calls `ChatWidgetDataProvider::dataFor(auth()->user())`.

For non-admin/non-manager users, the provider disables the widget:

```php
['chatWidgetEnabled' => false]
```

For admins:

- The sidebar is visible.
- The admin can create block chats.
- Conversations are all participant conversations filtered to `block_auto` with a valid `block_id`.
- Available blocks exclude blocks that already have a chat.

For managers:

- The sidebar is hidden in thread-only mode.
- The manager cannot create conversations.
- Conversations are participant conversations filtered to `block_auto` for blocks assigned to that manager.
- Manager block assignment includes `block_user`, `blocks.head_id`, and `block_additional_managers`.
- The first available conversation is selected by default.

The provider also generates route templates for JavaScript:

- messages
- mark seen
- update presence
- send message
- edit message
- delete message
- unread counts
- Pusher Beams auth

## Routes

Chat routes are registered in `routes/web.php` under authenticated routes:

```text
POST   /chats                                      chats.storeConversation
DELETE /chats/{conversation}                       chats.destroyConversation
GET    /chats/unread-counts                        chats.unreadCounts
POST   /chats/{conversation}/messages              chats.storeMessage
PUT    /chats/{conversation}/messages/{message}    chats.updateMessage
DELETE /chats/{conversation}/messages/{message}    chats.destroyMessage
GET    /chats/{conversation}/messages              chats.messages
POST   /chats/{conversation}/seen                  chats.markSeen
POST   /chats/presence                             chats.updateSitePresence
POST   /chats/{conversation}/presence              chats.updatePresence
GET    /chats/{conversation}/messages/{message}/attachment chats.downloadAttachment
```

Read routes use `throttle:chat-read`. Write routes use `throttle:chat-write`.

Pusher Beams auth is exposed separately:

```text
GET /pusher/beams-auth pusher.beams-auth
```

## Message Sending

Message send route:

```text
POST /chats/{conversation}/messages
```

Validation is in `SendChatMessageRequest`:

- `content`: optional string, max 5000, required without attachment.
- `attachment`: optional file, max 12 MB, required without content.
- allowed extensions/MIME types: JPG, PNG, GIF, WEBP, PDF, MP4, MOV, WEBM.
- `reply_to_message_id`: optional integer.

Send flow:

1. User role is checked.
2. Conversation membership is checked through `resolveConversation()`.
3. Reply target is resolved if `reply_to_message_id` is present.
4. Attachments are stored on the `local` disk under `chat-attachments`.
5. Message type is set to `text`, `image`, or `attachment`.
6. Message metadata is saved in the package message `data` field.
7. The message is sent through `Chat::message(...)->from($user)->to($conversation)->send()`.
8. The fresh message is loaded with sender and participant data.
9. Push notifications are sent through `ChatPushNotificationService`.
10. Browser real-time notifications are dispatched through `ChatMessageNotification`.

## Message Visibility and Retrieval

Message retrieval route:

```text
GET /chats/{conversation}/messages
```

`ChatController::conversationMessages()` joins `chat_messages` to `chat_message_notifications` and filters by:

- current conversation id
- current user's `messageable_id`
- current user's `messageable_type`
- notification `deleted_at IS NULL`
- message `deleted_at IS NULL`

This is why new managers do not see historical messages: they do not have notification rows for messages sent before their participation row was created.

The route supports incremental polling with `after_id`. Without `after_id`, it returns the latest 100 visible messages sorted ascending by id. With `after_id`, it returns messages newer than that id.

## Editing and Deleting Messages

Edit route:

```text
PUT /chats/{conversation}/messages/{message}
```

Rules:

- Only the original sender can edit.
- Only text messages can be edited.
- Content is required and max 5000 characters.
- `edited_at` is set when saved.

Delete route:

```text
DELETE /chats/{conversation}/messages/{message}
```

Rules:

- Only the original sender can delete.
- Messages are soft-deleted by setting `chat_messages.deleted_at`.
- Attachment files are not deleted when a message is soft-deleted.

## Attachments

Attachments are stored on the `local` disk in `chat-attachments`.

Attachment metadata stored in message `data` includes:

- `file_name`
- `file_path`
- `file_disk`
- `mime`
- `size`

Download/view route:

```text
GET /chats/{conversation}/messages/{message}/attachment
```

Attachment access requires conversation membership. Inline viewing is allowed for images and PDFs. Other files are downloaded. The response sets `X-Content-Type-Options: nosniff`.

When an entire conversation is deleted by an admin, `ChatController::destroyConversation()` deletes participant rows, deletes the conversation, then deletes attachment files belonging to messages in that conversation.

## Unread Counts and Seen State

Unread count route:

```text
GET /chats/unread-counts
```

Unread counts are calculated from `chat_message_notifications` where:

- `messageable_id` and `messageable_type` match the current user.
- `is_seen = false`.
- `is_sender = false`.
- `deleted_at IS NULL`.

Mark seen route:

```text
POST /chats/{conversation}/seen
```

This updates the current user's unseen notification rows for the conversation to `is_seen = true`.

The frontend keeps conversation badges and the launcher badge synchronized with periodic polling and immediate updates after selecting a conversation.

## Presence

Presence is cache-backed and best-effort.

`ChatPresenceService` stores two kinds of state:

- Site-open presence: `chat:site-open:{userId}:{clientId}`
- Conversation-viewing presence: `chat:viewing:{conversationId}:{userId}`

Both have a 45-second TTL.

The frontend sends heartbeats roughly every 25 seconds. It also sends inactive state on blur, visibility changes, and `beforeunload` where possible.

Presence is used by `ChatPushNotificationService` to avoid sending push notifications to users who are currently on the site or actively viewing the conversation.

## Browser Real-Time Notifications and Read Receipts

`ChatMessageNotification` implements `ShouldBroadcast` and broadcasts to:

```text
private-App.Models.User.{recipientId}
```

Broadcast payload includes:

- conversation id
- message id
- sender id/name
- body/type/data
- created timestamp
- conversation title
- last message preview
- updated timestamp
- full message payload

The frontend initializes Laravel Echo with the configured Pusher key/cluster and listens for user-level chat notifications. When a notification arrives, it updates the conversation list, unread badge, and active thread if relevant.

`ChatMessagesSeen` also broadcasts on the private user channel:

```text
private-App.Models.User.{senderId}
```

This event is dispatched by `ChatController::markSeen()` after unseen notification rows are changed to seen. It is sent only to users who originally sent messages that were newly marked seen.

Read receipt payload includes:

- conversation id
- message ids that were newly seen
- user id/name of the participant who saw them

The frontend listens for `.ChatMessagesSeen` and immediately updates the sender's rendered messages to show `ნანახია`, without waiting for the periodic receipt refresh or a page reload.

## Push Notifications

`ChatPushNotificationService` sends Pusher Beams web push notifications when Beams is configured:

- `services.pusher_beams.instance_id`
- `services.pusher_beams.secret_key`

Recipients are conversation participants except the sender. Recipients are skipped if `ChatPresenceService` says they are site-open or viewing the conversation.

Notifications include:

- title: conversation title
- body: sender name plus message preview
- deep link: same-host referer when available, otherwise `home.index`
- icon and badge URLs
- tag: `chat-message`
- `hide_notification_if_site_has_focus = true`

## Frontend Widget Behavior

The widget is rendered globally in the dashboard layout when enabled.

Key browser state:

- Open/closed state is stored in `localStorage` under `chat_widget_open`.
- Expanded state is stored in `localStorage` under `chat_widget_expanded`.
- Site presence client id is stored in `sessionStorage`.
- Conversation messages are cached in memory per conversation while the page is open.

Admin widget behavior:

- Shows a sidebar.
- Allows creating a new block chat.
- Shows existing block chats.
- Lets admin delete entire conversations.

Manager widget behavior:

- Uses thread-only mode.
- Automatically selects the first assigned block chat.
- Does not expose chat creation or conversation deletion.

## Chat List Search

The sidebar has a simple chat-list search input.

Search behavior:

- Implemented in `resources/js/chat.js` by `filterConversationsByTitle()`.
- Filters only by `data-conversation-title`.
- Does not search participants, block id, preview text, or messages.
- Pressing Escape clears the search.
- If no visible conversations match, the empty state says `ჩატი ვერ მოიძებნა`.

This is separate from the existing in-thread message search.

## In-Thread Message Search

The opened thread has its own message search bar.

Search behavior:

- Filters currently rendered `.chat-message` elements.
- Searches message body text only.
- Does not query the server.
- Does not search across all conversations.

## Replies

When replying to a message, the reply metadata is embedded in the new message `data.reply` value.

Reply data includes:

- original message id
- sender name
- truncated body
- original type
- file name, if any

The frontend renders this as a reply preview and supports jumping/highlighting the referenced message when present in the current rendered message list.

## Conversation Deletion

Only admins can delete conversations.

Delete flow:

1. Resolve conversation and verify membership.
2. Verify current user is admin.
3. Collect all attachment files referenced by messages.
4. Delete all participant rows.
5. Delete the conversation.
6. Delete attachment files from storage.
7. Return deleted conversation metadata, including block id/name, so the frontend can restore the block option in the create form.

## Legacy / Currently Unused Code

`ManagerInboxConversationResolver` exists and supports a `manager_admin_inbox` conversation type. Current widget data intentionally filters to `block_auto` conversations only, and no active code references this resolver. Treat it as legacy unless a future change explicitly reintroduces manager-admin inbox conversations.

## Potential Improvements

These are possible future improvements. They are not required for the current feature set.

### Authorization Structure

- Move conversation authorization from private controller helpers into a dedicated policy or gate-backed service.
- Keep the current participant-table check as the final source of truth, but centralize it so attachment, message, presence, and seen endpoints cannot drift.
- Add explicit tests for `head_id`, `additional_head_1`, and `additional_head_2` access paths separately.

### Manager Sync Robustness

- Run `BlockChatParticipantSyncService::sync()` inside the same transaction as the block manager update if the block update method is later refactored into a transaction.
- Add coverage for a user who remains assigned through one path while removed from another path, for example removed from `additional_head_1` but still present in `block_user`.
- Consider emitting a system message or audit record when chat participants change, if the business wants chat history to show manager handoffs.

### Data Modeling

- Store `chat_type` and `block_id` as first-class indexed columns if chat volume grows. Current code scans conversations and filters decoded `data`, which is acceptable at small scale but not ideal for large data sets.
- Add database indexes for frequent notification lookups if they are not already covered well enough by existing package indexes.
- Consider a dedicated application-level `block_chats` table that maps `block_id` to `conversation_id`, instead of discovering block chats through conversation `data`.

### Frontend Behavior

- Add an explicit empty/error state when a manager has no assigned block chat, instead of only showing the current generic unavailable text.
- Add a small label in the participant list that distinguishes main manager, additional manager, and admin participants.
- Consider server-side chat-list search only if the conversation count becomes large. Current chat-name search is intentionally client-side and simple.

### Notifications and Presence

- Track participant-join timestamps explicitly if future requirements need stronger guarantees around visibility from join time.
- Add observability for Pusher Beams failures beyond warning logs if push delivery becomes business-critical.
- Consider making presence TTL and heartbeat intervals configurable.

### Code Organization

- Extract message formatting from `ChatController` into a dedicated formatter/resource if more endpoints need the same message shape.
- Extract attachment storage/response logic into a service if attachment behavior grows.
- Split `resources/js/chat.js` into modules if future chat UI changes continue to expand the file.

## Deletion and Refactor Candidates

These are candidates to review before deletion. Do not remove them without confirming no hidden usage exists.

### `ManagerInboxConversationResolver`

`app/Services/Chat/ManagerInboxConversationResolver.php` appears unused by active application code. It supports `manager_admin_inbox`, while current widget behavior intentionally filters to `block_auto` only.

Candidate action:

- Delete it if product confirms manager-admin inbox chats are no longer needed.
- Before deletion, search for references in tests, config, queued jobs, and historical deployment branches.

### Legacy Chat Types

Existing tests mention legacy/manual conversation data such as `chat_type = users`. Current widget excludes non-`block_auto` chats.

Candidate action:

- Keep the exclusion tests if old data may still exist in production.
- Consider a one-time cleanup/migration only if production no longer needs legacy conversations.

### Unused Participant Picker Code

The Blade create form currently creates block chats only. `resources/js/chat.js` still contains participant-picker logic such as `chat-participant-search`, `chat-participant-dropdown`, and `chat-participant-tags` handling.

Candidate action:

- Verify whether any hidden DOM still uses those ids.
- If not, remove the unused picker JavaScript and related CSS after confirming no manual participant chat creation will return.

### `participant_ids` Validation on Block Chat Creation

`CreateBlockChatRequest` still validates `participant_ids`, but current creation flow requires `block_id` and resolves participants from the selected block.

Candidate action:

- Remove `participant_ids` validation if manual chat creation is permanently retired.
- Keep it if a future admin-created custom chat feature is planned.

### Compiled/Public Asset Drift

The source of truth for widget behavior is `resources/js/chat.js` and `resources/css/chat-widget.css`. Public compiled assets may change after build commands.

Candidate action:

- Confirm the project's deployment process compiles assets from `resources/`.
- Avoid manually editing compiled `public/js` or `public/css` files unless the deployment strategy requires committed build artifacts.

## Tests Covering Chat

Important test files:

- `tests/Feature/ChatAccessTest.php`
- `tests/Feature/ChatMessagingTest.php`
- `tests/Feature/ChatPresencePushSuppressionTest.php`
- `tests/Unit/ChatPushNotificationServiceTest.php`
- `tests/Unit/ChatNotificationAudioSelectionTest.php`
- `tests/Unit/ManagerChatWidgetInitializationTest.php`

Core coverage includes:

- Admin can create block chats.
- Managers can only access assigned block chats.
- Regular users cannot access chat.
- Duplicate block chats are rejected.
- Admin can delete conversations.
- Chat widget hides blocks that already have chats.
- Manager widget only selects block chats.
- Manager access syncs when block manager changes.
- New manager does not see old messages but sees future messages.
- Attachments require conversation membership.
- Message ordering and incremental polling.
- Seen/unread behavior.
- Edit/delete authorization.
- Push notification suppression when users are present.

Focused commands used for chat verification:

```shell
ddev artisan test --compact --filter=ChatAccessTest
ddev artisan test --compact --filter=ChatMessagingTest
```

## Operational Notes

- Frontend assets are not rebuilt automatically in this project. If browser changes do not appear, run the appropriate local asset command separately.
- Chat depends on the user role helpers `isAdmin()` and `isManager()` and Spatie roles named `Admin` and `Manager`.
- The package-level notification table is part of access semantics. Do not treat it as unread-count-only data.
- Adding participants without creating historical notification rows is intentional for manager reassignment.
- Removing manager access must delete `chat_participation` and clear that manager's notification rows for the conversation.
- Do not introduce Livewire for this widget; Livewire is present for Pulse only.
