# Notifications System

## Status: ✅ COMPLETED (March 2026)

The full in-app notification system is live and working.

---

## What's Implemented

### Backend
- **Entity**: `src/Entity/UserWebNotification.php` — table `user_web_notification` with fields: `user`, `message`, `link`, `datetime`, `seen`, `clicked`, `isDeleted`
- **Service**: `src/Service/UserNotification.php` — `notifyUser(User, message, link)`, `getNotification()`, `getNotSeenNotificationCount()`, `setNotificationsAsShown()`
- **Repository**: `src/Repository/NotificationRepository.php` — full CRUD with pagination, unseen count
- **Controller**: `src/Controller/NotificationController.php` — 5 routes

### UI Components
- **Bell icon in header**: `templates/header.html.twig` lines 91-116
  - FontAwesome bell icon (`fa-bell`)
  - Red badge showing unseen count
  - Visible only for authenticated users
- **Dropdown menu**: Shows 10 most recent notifications
  - Green header with "Mark all as read" button
  - Notification items with seen/unseen styling
  - "See all notifications" link to full page
- **Full notifications page**: `templates/notifications/index.html.twig`
  - Paginated list at `/notifications`
  - Marks all as seen when viewed

### API Endpoints
| Route | Method | Purpose |
|-------|--------|---------|
| `/notifications` | GET | Full paginated notifications page |
| `/api/notifications/count` | GET | Unseen count (for badge polling) |
| `/api/notifications/recent` | GET | Latest 10 for dropdown |
| `/api/notifications/mark-seen` | POST | Mark all as seen |
| `/api/notifications/{id}/click` | POST | Track click + mark as read |

### JavaScript Features
- **Polling**: `/api/notifications/count` every 60 seconds
- **Lazy loading**: Notifications fetched on dropdown open
- **Click tracking**: Via `sendBeacon` API
- **Mark all read**: AJAX with instant UI update

### Translations
- `translations/Notifications.ar.yml`
- `translations/Notifications.en.yml`

---

## Active Notification Triggers (11 places)

| Event | Location |
|-------|----------|
| New question on profile | `QuestionController::newQuestionAction` |
| Answer to question | `QuestionController` (lines 378, 540) |
| New follower | `FollowController::follow` |
| Comment on profile post | `ProfilePostCommentController` (lines 105, 222) |
| Like on profile post | `ProfilePostController` (line 377) |
| New post by followed user | `ProfilePostController` (line 560) |
| New community question | `CommunityQuestionController` |
| New community discussion | `CommunityDiscussionController` |
| Comment on research | `ResearchController` |
| Chat message | `SendChatNotificationHandler` |

---

## Database Hygiene (Implemented March 2026)

### Problem
Notifications were being created for users who hadn't logged in for years, leading to database bloat with notifications that would never be seen.

### Solution 1: Skip Inactive Users
`UserNotification::notifyUser()` now checks `last_login` before creating a notification:
- Users inactive > 90 days do NOT receive in-app notifications
- Prevents future bloat
- Config: `UserNotification::INACTIVE_THRESHOLD_DAYS = 90`

### Solution 2: Cleanup Command
New command to delete stale notifications:

```bash
# Dry run (show what would be deleted)
php bin/console app:cleanup-notifications --dry-run

# Actually delete
php bin/console app:cleanup-notifications

# Custom thresholds
php bin/console app:cleanup-notifications --read-days=60 --unread-days=120 --inactive-days=90
```

**Default thresholds:**
| Type | Days | Rationale |
|------|------|-----------|
| Read notifications | 90 | User saw it, no need to keep |
| Unread notifications | 180 | User abandoned, won't return |
| Inactive user notifications | 180 | User hasn't logged in |

**Recommended cron** (weekly):
```bash
0 3 * * 0 cd /var/www/html/academia_v2 && php bin/console app:cleanup-notifications --env=prod >> var/log/cleanup-notifications.log 2>&1
```

---

## Future Enhancements

### Email Notification Preferences (Not Yet Implemented)
- Add `emailNotificationsEnabled` boolean to User entity
- Toggle in settings page
- Send lightweight email for high-value events (answers, upvotes)

### New Notification Events (Consider Adding)
- Profile visit notifications
- New paper in followed field
- Weekly digest of missed notifications

---

## Integration with Push Notifications

> See [10-push-notifications-revamp.md](10-push-notifications-revamp.md) for the full push notifications plan.

When `notifyUser()` is called in Step 4 above, it should also trigger a **push notification** for the same event if:
- The user has `pushNotificationsEnabled = true`
- The user has active device tokens in `academy_push_notification_device`
- The notification type is in the user's `pushNotificationTypes` preferences

This creates a dual-channel system:
- **On-site** → Bell notification (this plan)
- **Off-site** → Push notification (Plan 10)

The `UserNotification::notifyUser()` method is the single entry point for both channels.

---

## Decisions

- **Polling over WebSocket**: WebSocket adds infrastructure complexity. 60s polling is fine for <100K users.
- **Dual channel (bell + push)**: Web bell for on-site users, push notification for off-site. Both triggered from the same `notifyUser()` call. See Plan 10 for push details.
- **Notification message in user's locale**: Use `user.preferredLocale` to choose ar/en message text.

---

## Follow-Up: Keyword Follow Improvements

The existing "follow keyword" system (`FollowersTags` entity, `TagController::tagFollowAction`) notifies users when new research matches their followed tags — but discovery is poor. Three improvements, in priority order:

### A. Follow buttons on Research Detail Page (HIGH PRIORITY — implementing now)
Add a small `+ متابعة` (follow) button next to each tag on the research show page (`Research/show.html.twig`). This is the highest-impact touchpoint — users are reading about a topic they're interested in. AJAX call to existing `/tag-follow` route. Show ✓ if already following.

### B. "Followed Keywords" Tab on User Profile
Add a 4th tab to the profile page (`profile_tab.html.twig`) showing all tags the user follows with unfollow controls. Currently this info is buried in the separate library/follow page which most users never find.

### C. "Suggested Keywords" Section on Homepage
Show popular/trending keywords the user doesn't yet follow. Could be a sidebar widget or a dedicated section. Helps new users discover what's available and increases follow rates. Requires a new query: top tags by follower count or research count, filtered to exclude tags the user already follows.
