# Notifications System Design

**Date:** 2026-02-22
**Status:** Approved

## Overview

In-app notification system for the HBCU Connect mobile app. Notifies users when someone likes their post, comments on their post, sends/accepts a friend request, or follows them. Uses Socket.IO for real-time delivery with database persistence.

## Notification Types (5)

| Type | Trigger | Message | Deep-link |
|------|---------|---------|-----------|
| `like` | Someone likes your post | "John S. liked your post" | PostDetailScreen (media_id) |
| `comment` | Someone comments on your post | "John S. commented on your post" | PostDetailScreen (media_id) |
| `friend_request` | Someone sends you a friend request | "John S. sent you a friend request" | ViewProfileScreen (registry_id) |
| `friend_accept` | Someone accepts your friend request | "John S. accepted your friend request" | ViewProfileScreen (registry_id) |
| `follow` | Someone follows you | "John S. started following you" | ViewProfileScreen (registry_id) |

**Rules:**
- No self-notifications (liking your own post, etc.)
- No notifications if recipient has blocked the actor
- Deduplication for likes (no duplicate unread notification for same actor + same post)

## Database Schema

```sql
CREATE TABLE app_notifications (
    notification_id  INT AUTO_INCREMENT PRIMARY KEY,
    registry_id      INT NOT NULL,
    type             VARCHAR(30) NOT NULL,
    actor_id         INT NOT NULL,
    target_id        INT DEFAULT NULL,
    target_type      VARCHAR(20) DEFAULT NULL,
    message          VARCHAR(255) NOT NULL,
    is_read          TINYINT(1) NOT NULL DEFAULT 0,
    created_at       DATETIME NOT NULL DEFAULT NOW(),
    INDEX idx_recipient (registry_id, is_read, created_at),
    INDEX idx_actor (actor_id)
);
```

- `registry_id` = notification recipient
- `actor_id` = who triggered it
- `target_id` = media_id (for like/comment) or registry_id (for friend/follow)
- `target_type` = 'media' or 'user'

## API Endpoints

**New file:** `hbcu-app/api/routes/notifications.js`

| Method | Endpoint | Purpose |
|--------|----------|---------|
| `GET` | `/api/notifications` | Paginated list (20/page), newest first |
| `GET` | `/api/notifications/unread-count` | Count of unread (for badge) |
| `PUT` | `/api/notifications/mark-read` | Body: `{ notification_ids: [...] }` — batch mark as read |

### GET /api/notifications

Query params: `?page=1` (default 1, 20 per page)

Returns:
```json
{
  "notifications": [
    {
      "notification_id": 123,
      "type": "like",
      "actor_id": 456,
      "actor_name": "John Smith",
      "actor_thumb": "thumbs/456.jpg",
      "target_id": 789,
      "target_type": "media",
      "message": "John S. liked your post",
      "is_read": 0,
      "created_at": "2026-02-22 14:30:00"
    }
  ],
  "total": 50,
  "page": 1,
  "pages": 3
}
```

### GET /api/notifications/unread-count

Returns: `{ "count": 5 }`

### PUT /api/notifications/mark-read

Body: `{ "notification_ids": [1, 2, 3] }`
Returns: `{ "success": true }`

Only marks notifications belonging to the authenticated user.

## Notification Creation Helper

Shared function used by existing route files:

```javascript
// services/notifications.js
async function createNotification(pool, io, { recipientId, type, actorId, targetId, targetType, message }) {
    // 1. Skip self-notifications
    if (recipientId === actorId) return;

    // 2. Check blocked
    const [blocked] = await pool.execute(
        'SELECT 1 FROM registry_block WHERE registry_id = ? AND blocked_id = ?',
        [recipientId, actorId]
    );
    if (blocked.length > 0) return;

    // 3. Deduplicate likes (no duplicate unread for same actor + target)
    if (type === 'like') {
        const [existing] = await pool.execute(
            'SELECT 1 FROM app_notifications WHERE registry_id = ? AND actor_id = ? AND target_id = ? AND type = ? AND is_read = 0',
            [recipientId, actorId, targetId, type]
        );
        if (existing.length > 0) return;
    }

    // 4. Insert
    await pool.execute(
        'INSERT INTO app_notifications (registry_id, type, actor_id, target_id, target_type, message) VALUES (?, ?, ?, ?, ?, ?)',
        [recipientId, type, actorId, targetId, targetType, message]
    );

    // 5. Emit real-time via Socket.IO
    if (io) {
        io.to(`user_${recipientId}`).emit('new_notification', { type, actor_id: actorId, message });
    }
}
```

## Injection Points

| File | Location | Action |
|------|----------|--------|
| `routes/media.js` | Like endpoint (~line 617) | `createNotification({ type: 'like', recipientId: postOwnerId, ... })` |
| `routes/media.js` | Comment endpoint (~line 724) | `createNotification({ type: 'comment', recipientId: postOwnerId, ... })` |
| `routes/friends.js` | Send request (~line 87) | `createNotification({ type: 'friend_request', recipientId: targetUserId, ... })` |
| `routes/friends.js` | Accept request (~line 169) | `createNotification({ type: 'friend_accept', recipientId: requesterId, ... })` |
| `routes/follows.js` | Follow endpoint | `createNotification({ type: 'follow', recipientId: followedUserId, ... })` |

## Mobile App UI

### Header: NotificationBell Component

- Positioned left of GearMenu in `headerRight` area of `MainTabs.js`
- Bell icon: `Ionicons notifications-outline`
- Red badge with unread count (hidden when 0)
- Tap opens dropdown modal (same pattern as GearMenu)

### Dropdown (10 Recent)

- Shows 10 most recent notifications
- Unread notifications have highlighted background (light blue/amber)
- Each row: type icon (left), message text, relative timestamp ("2m ago")
- Tapping a notification deep-links to content (post or profile)
- "View All" link at bottom navigates to NotificationsScreen
- On open: sends displayed notification IDs to `PUT /mark-read`, decrements badge

### NotificationsScreen (Full List)

- New screen pushed onto navigation stack
- Full scrollable list, paginated (20 per page)
- Infinite scroll (load more on reaching bottom)
- Pull-to-refresh
- Unread notifications highlighted; on load, sends IDs to mark-read
- Each row: avatar/type icon, message, relative time, blue dot or highlight for unread
- Deep-link on tap to relevant content

### Read Behavior

1. User has 15 unread (badge: 15)
2. Taps bell -> dropdown loads 10 most recent -> marks those 10 as read -> badge: 5
3. Taps "View All" -> NotificationsScreen loads first page -> marks visible unread as read -> badge: 0
4. Scrolls down -> older notifications already read, no highlighting
5. New notification arrives via Socket.IO -> badge increments, dropdown shows it highlighted

### Real-Time Updates

- Socket.IO listener for `new_notification` event (same pattern as messages)
- Increments badge count on receipt
- 30-second polling fallback for unread-count (same pattern as messages)

### Web Preview Adjustments

- Same components, dropdown uses absolute positioning instead of React Native Modal
- Bell icon uses web-compatible touch handling via Platform.select

## Files to Create

1. `hbcu-app/api/routes/notifications.js` — API endpoints
2. `hbcu-app/api/services/notifications.js` — createNotification helper
3. `hbcu-app/mobile/src/components/NotificationBell.js` — header bell + dropdown
4. `hbcu-app/mobile/src/screens/notifications/NotificationsScreen.js` — full list

## Files to Modify

1. `hbcu-app/api/server.js` — register notifications route
2. `hbcu-app/api/routes/media.js` — add like/comment notification triggers
3. `hbcu-app/api/routes/friends.js` — add friend request/accept notification triggers
4. `hbcu-app/api/routes/follows.js` — add follow notification trigger
5. `hbcu-app/mobile/src/navigation/MainTabs.js` — add NotificationBell to header, Socket listener
6. `hbcu-app/mobile/src/navigation/AppNavigator.js` — register NotificationsScreen

## Not In Scope (Phase 3)

- Firebase Cloud Messaging push notifications
- Device token management
- Notification preferences per type (currently just global on/off)
- Email notification digests
