# Phase 8 — Advanced profile, contacts and screen sharing

Branch: `feature/phase-8-advanced`

## Migrations (additive only)

| File | Purpose |
| --- | --- |
| `2026_11_02_000001_add_phase8_fields_to_users_table.php` | `username`, `bio`, `cover_image`, `cover_image_updated_at`, three contact-discovery flags, the business columns (`is_business`, name, category, description, address, lat/lng, email, website, `business_hours` JSON) and the admin-only `is_verified` / `business_verified_at`. |
| `2026_11_02_000002_create_contacts_table.php` | `contacts` with `status` (`pending` / `accepted` / `blocked`), `nickname`, `added_at`, `accepted_at`, unique `(user_id, contact_id)`. |
| `2026_11_02_000003_create_reports_table.php` | `reports` for the profile report modal, with `status` for a future moderation queue. |

All three have a working `down()`.

```bash
php artisan migrate
```

## Feature 1 — Screen sharing during calls

- `resources/js/call.js`: keeps the camera track and the video `RTCRtpSender`, calls
  `sender.replaceTrack(screenTrack)` (no renegotiation), reverts on the browser's own
  "Stop sharing" via `track.onended`, and re-acquires the camera if the original track died.
- `resources/views/chat/partials/call-overlay.blade.php`: **Share Screen** button (video calls only)
  plus a shared banner and a toast.
- `CallController@screen` + `POST /calls/{call}/screen`; `ScreenShareStarted` /
  `ScreenShareStopped` broadcast on the same private `user.{id}` channels as the rest of the
  call signalling (decision taken with the client: reuse the existing channel).

## Feature 2 — Contacts and add-contact flow

- `ContactController` (`index`, `search`, `store`, `accept`, `reject`, `nickname`, `destroy`,
  `block`, `unblock`, `addViaLink`) and `ContactPolicy`.
- Live search (300 ms debounce) across phone, `@username` and exact email, honouring each
  user's `allow_search_by_phone` / `allow_search_by_username` flags; phone numbers come back
  masked, existing contacts and blocked pairs are excluded.
- Sending a request to somebody who already invited you auto-accepts instead of creating a
  duplicate. Accepting creates the mirrored row so both sides have a contact.
- Blocking hides the private conversation from the sidebar, shows a read-only banner and
  returns 403 from `MessageController@store`; unblocking restores the thread with its history.
- QR code (`qrcode` npm package) renders a **signed** `contacts.add` URL; scanning decodes an
  uploaded image through `BarcodeDetector` where the browser supports it.
- Sidebar header gained a Contacts entry with a pending-request badge.

## Feature 3 — Profile viewer

- `GET /profile/{user}` → `ProfileController@show`, full page or the card partial via
  `?partial=1` (used by the in-chat modal in `chat/index.blade.php`).
- Shows cover, avatar, display name plus private nickname, `@handle`, bio, last seen,
  phone (full for self/accepted contacts, masked otherwise), groups in common, business block,
  and actions: Message, Voice, Video, Add contact, Block/Unblock, Report.
- Blocked in either direction renders only the name and "This user is unavailable."
- Reports are stored in `reports`; the admin review UI is still a TODO.
- Avatars in group member rows and the chat header open the same profile in a modal.

## Feature 4 — Cover image

- `PATCH /profile/cover` validates `jpg/jpeg/png/webp`, ≤ 5 MB and 600×200–3000×1500 px,
  stores under `covers/{user_id}/`, deletes the previous file and bumps
  `cover_image_updated_at` so the URL carries `?v=`.
- `profile/partials/update-cover.blade.php` previews the chosen file with `URL.createObjectURL`
  before upload (CSS `object-fit`, no cropper dependency).

## Feature 5 — Business profile

- `PATCH /profile/business` → `BusinessProfileController@update` with `BusinessProfileRequest`.
- Category is restricted to the fixed list; hours arrive as `business_hours[mon][open|close|closed]`
  and are normalised to `{"mon":["09:00","18:00"], …, "sun":null}`.
- All business text is `strip_tags`-ed on save and escaped again by Blade; profile updates are
  throttled to 10 per minute.
- `is_verified` / `business_verified_at` are excluded from `$fillable` and from the request, so
  only `php artisan business:verify {id|email} [--revoke]` can set them.
- The public block shows business name with a verified badge, category chip, description,
  client-side **Open now / Closed**, collapsible hours, address with a Google Maps link,
  website and `mailto:` email.
- `ProfileUpdated` broadcasts on the `online` presence channel (payload is just the user id).

## Verification performed

- `php artisan test` → **79 passed / 241 assertions** (suite unchanged; broadcasts pinned to the
  `null` driver in `phpunit.xml` so tests never call a real Reverb server).
- `./vendor/bin/pint --dirty` clean, `npm run build` clean.
- Browser checks: contact request → accept (both rows `accepted`, events processed by the queue),
  profile viewer with masked/full phone and common groups, cover upload (file on disk + `?v=`),
  business save (`sat`/`sun` null, Sunday shows **Closed**), block → sidebar hidden + 403 →
  unblock restores, `POST /calls/{id}/screen` → `ScreenShareStarted`/`Stopped` queued and sent,
  in-chat profile modal, contacts page at 375 px with zero horizontal overflow.

## Manual QA checklist

1. `php artisan migrate`, `npm install`, `npm run build`, restart `queue:work` and
   `reverb:start` (broadcasts are queued, so a stale worker won't deliver them).
2. Alice → `/contacts` → Add contact → search `bob` → Add. Bob sees the badge, opens
   Pending requests → Accept.
3. Bob → `/profile` → Business tab → enable, fill name/category/hours → Save.
   Alice opens `/profile/3` and checks the chip reads **Closed** outside hours.
4. Bob uploads a cover; Alice reloads the profile and sees the banner.
5. Alice blocks Bob: the chat disappears from the sidebar, the thread shows the banner and
   sending fails; unblock restores it.
6. Two browsers, video call: **Share Screen** → pick a window → the other side shows
   "… is sharing their screen"; stop from the browser bar or the button → camera returns.
7. Regression: send text/photo/voice, delete for everyone, typing, unread badges, group add/remove.

## Known limitations

- Screen sharing is video-call only and needs a secure context (`localhost` counts); Firefox and
  Chrome both work, the remote banner relies on Reverb being up.
- QR scanning uses `BarcodeDetector`, which Chromium supports and Firefox does not — the signed
  link can always be copied instead.
- No admin moderation UI yet (reports are stored only), and `business_hours` cannot express
  overnight ranges such as 22:00–02:00.
- No TURN server: calls still rely on STUN, as in Phase 6.
