# Notifications System Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Add an in-app notification system that alerts users when someone likes/comments on their post, sends/accepts a friend request, or follows them — with real-time delivery via Socket.IO and a bell icon + dropdown in the app header.

**Architecture:** New `app_notifications` DB table stores all notifications. A shared `createNotification()` helper is called from existing route files (media, friends, follows) to insert + emit via Socket.IO. Mobile app gets a `NotificationBell` component in the header with dropdown, plus a full `NotificationsScreen`. Badge count uses Socket.IO + 30s polling fallback (same pattern as Messages).

**Tech Stack:** Node.js/Express API, MySQL (hbcu_central), Socket.IO, React Native (Expo), React Navigation

**Design doc:** `docs/plans/2026-02-22-notifications-system-design.md`

---

### Task 1: Create Database Table

**Files:**
- Run SQL on: `newdb.hbcuconnect.com` → `hbcu_central`

**Step 1: Create the app_notifications table**

```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)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```

Run:
```bash
mysql -h newdb.hbcuconnect.com -u <user> -p hbcu_central < /tmp/create_notifications.sql
```

**Step 2: Verify table exists**

```bash
mysql -h newdb.hbcuconnect.com -u <user> -p hbcu_central -e "DESCRIBE app_notifications"
```

Expected: 9 columns listed (notification_id through created_at).

**Step 3: Commit**

```bash
# No file to commit — SQL was run directly. Move to next task.
```

---

### Task 2: Create Notification Service Helper

**Files:**
- Create: `hbcu-app/api/services/notifications.js`

**Step 1: Create the notification service**

```javascript
const { mediaPool } = require('../config/database');

/**
 * Create a notification and emit via Socket.IO.
 * Handles self-notification prevention, block checking, and like deduplication.
 */
async function createNotification(io, { recipientId, type, actorId, targetId, targetType, message }) {
    // No self-notifications
    if (recipientId === actorId) return;

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

    // Deduplicate likes — no duplicate unread notification for same actor + target
    if (type === 'like') {
        const [existing] = await mediaPool.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;
    }

    // Insert notification
    const [result] = await mediaPool.execute(
        'INSERT INTO app_notifications (registry_id, type, actor_id, target_id, target_type, message) VALUES (?, ?, ?, ?, ?, ?)',
        [recipientId, type, actorId, targetId, targetType, message]
    );

    // Emit real-time event
    if (io) {
        const [countRows] = await mediaPool.execute(
            'SELECT COUNT(*) AS cnt FROM app_notifications WHERE registry_id = ? AND is_read = 0',
            [recipientId]
        );
        io.to(`user_${recipientId}`).emit('new_notification', {
            notification_id: result.insertId,
            type,
            actor_id: actorId,
            message,
            unread_count: countRows[0].cnt,
        });
    }
}

module.exports = { createNotification };
```

**Step 2: Verify syntax**

```bash
cd /var/www/vhosts/hbcuconnect.com/httpdocs/hbcu-app/api
node -e "require('./services/notifications')"
```

Expected: No errors.

**Step 3: Commit**

```bash
git add hbcu-app/api/services/notifications.js
git commit -m "feat(notifications): add createNotification service helper"
```

---

### Task 3: Create Notifications API Route

**Files:**
- Create: `hbcu-app/api/routes/notifications.js`
- Modify: `hbcu-app/api/server.js:73` (add route registration after marketplace)

**Step 1: Create the notifications route file**

```javascript
const express = require('express');
const router = express.Router();
const { authenticateToken } = require('../middleware/auth');
const { mediaPool } = require('../config/database');

router.use(authenticateToken);

// GET /api/notifications — paginated list, newest first
router.get('/', async (req, res) => {
    try {
        const page = Math.max(1, parseInt(req.query.page) || 1);
        const perPage = 20;
        const offset = (page - 1) * perPage;

        const [countRows] = await mediaPool.execute(
            'SELECT COUNT(*) AS total FROM app_notifications WHERE registry_id = ?',
            [req.userId]
        );
        const total = countRows[0].total;

        const [rows] = await mediaPool.execute(
            `SELECT n.notification_id, n.type, n.actor_id, n.target_id, n.target_type,
                    n.message, n.is_read, n.created_at,
                    r.first_name AS actor_first_name, r.last_name AS actor_last_name,
                    r.photo_link AS actor_photo_link
             FROM app_notifications n
             LEFT JOIN registry_data r ON r.registry_id = n.actor_id
             WHERE n.registry_id = ?
             ORDER BY n.created_at DESC
             LIMIT ? OFFSET ?`,
            [req.userId, perPage, offset]
        );

        const notifications = rows.map(r => ({
            notification_id: r.notification_id,
            type: r.type,
            actor_id: r.actor_id,
            actor_name: ((r.actor_first_name || '') + ' ' + (r.actor_last_name || '')).trim(),
            actor_thumb: r.actor_photo_link
                ? `https://hbcuconnect.com/uploads/thumbs/${r.actor_photo_link}`
                : null,
            target_id: r.target_id,
            target_type: r.target_type,
            message: r.message,
            is_read: r.is_read,
            created_at: r.created_at,
        }));

        res.json({
            notifications,
            total,
            page,
            pages: Math.ceil(total / perPage),
        });
    } catch (err) {
        console.error('Notifications list error:', err);
        res.status(500).json({ error: 'Failed to load notifications' });
    }
});

// GET /api/notifications/unread-count
router.get('/unread-count', async (req, res) => {
    try {
        const [rows] = await mediaPool.execute(
            'SELECT COUNT(*) AS count FROM app_notifications WHERE registry_id = ? AND is_read = 0',
            [req.userId]
        );
        res.json({ count: rows[0].count });
    } catch (err) {
        console.error('Unread count error:', err);
        res.status(500).json({ error: 'Failed to get unread count' });
    }
});

// PUT /api/notifications/mark-read — batch mark specific IDs as read
router.put('/mark-read', async (req, res) => {
    try {
        const ids = req.body.notification_ids;
        if (!Array.isArray(ids) || ids.length === 0) {
            return res.status(400).json({ error: 'notification_ids array required' });
        }

        // Only mark notifications belonging to this user
        const placeholders = ids.map(() => '?').join(',');
        await mediaPool.execute(
            `UPDATE app_notifications SET is_read = 1
             WHERE notification_id IN (${placeholders}) AND registry_id = ?`,
            [...ids, req.userId]
        );

        res.json({ success: true });
    } catch (err) {
        console.error('Mark read error:', err);
        res.status(500).json({ error: 'Failed to mark notifications as read' });
    }
});

module.exports = router;
```

**Step 2: Register route in server.js**

In `hbcu-app/api/server.js`, after line 73 (`app.use('/api/marketplace', ...)`), add:

```javascript
app.use('/api/notifications', require('./routes/notifications'));
```

**Step 3: Verify server starts**

```bash
cd /var/www/vhosts/hbcuconnect.com/httpdocs/hbcu-app/api
node -e "require('./routes/notifications')"
```

Expected: No errors.

**Step 4: Commit**

```bash
git add hbcu-app/api/routes/notifications.js hbcu-app/api/server.js
git commit -m "feat(notifications): add notifications API endpoints"
```

---

### Task 4: Add Notification Triggers to Existing Routes

**Files:**
- Modify: `hbcu-app/api/routes/media.js:617-645` (like endpoint)
- Modify: `hbcu-app/api/routes/media.js:724-768` (comment endpoint)
- Modify: `hbcu-app/api/routes/friends.js:87-132` (send friend request)
- Modify: `hbcu-app/api/routes/friends.js:169-212` (accept friend request)
- Modify: `hbcu-app/api/routes/follows.js:12-44` (follow)

**Step 1: Add notification import to media.js**

At the top of `media.js`, after existing requires, add:

```javascript
const { createNotification } = require('../services/notifications');
```

**Step 2: Add like notification in media.js**

In the like endpoint (after the like INSERT at ~line 625, before the count update), add:

```javascript
    // Get post owner for notification
    const [ownerRows] = await mediaPool.execute(
      'SELECT registry_id FROM app_media WHERE media_id = ?', [mediaId]
    );
    if (ownerRows.length > 0) {
      const [actorRows] = await mediaPool.execute(
        'SELECT first_name, last_name FROM registry_data WHERE registry_id = ?', [req.userId]
      );
      const actorName = actorRows[0] ? `${actorRows[0].first_name} ${actorRows[0].last_name[0]}.` : 'Someone';
      createNotification(req.app.get('io'), {
        recipientId: ownerRows[0].registry_id,
        type: 'like',
        actorId: req.userId,
        targetId: mediaId,
        targetType: 'media',
        message: `${actorName} liked your post`,
      }).catch(err => console.error('Like notification error:', err));
    }
```

**Step 3: Add comment notification in media.js**

In the comment endpoint (after the INSERT at ~line 736, before the count update), add:

```javascript
    // Get post owner for notification
    const [ownerRows] = await mediaPool.execute(
      'SELECT registry_id FROM app_media WHERE media_id = ?', [mediaId]
    );
    if (ownerRows.length > 0) {
      const [actorRows] = await mediaPool.execute(
        'SELECT first_name, last_name FROM registry_data WHERE registry_id = ?', [req.userId]
      );
      const actorName = actorRows[0] ? `${actorRows[0].first_name} ${actorRows[0].last_name[0]}.` : 'Someone';
      createNotification(req.app.get('io'), {
        recipientId: ownerRows[0].registry_id,
        type: 'comment',
        actorId: req.userId,
        targetId: mediaId,
        targetType: 'media',
        message: `${actorName} commented on your post`,
      }).catch(err => console.error('Comment notification error:', err));
    }
```

**Step 4: Add notification import to friends.js**

At the top of `friends.js`, after existing requires, add:

```javascript
const { createNotification } = require('../services/notifications');
```

**Step 5: Add friend request notification in friends.js**

In the send request endpoint (~line 127, after the INSERT and before the response), add:

```javascript
    // Notify the recipient of the friend request
    const [actorRows] = await hbcuPool.execute(
      'SELECT first_name, last_name FROM registry_data WHERE registry_id = ?', [authUserId]
    );
    const actorName = actorRows[0] ? `${actorRows[0].first_name} ${actorRows[0].last_name[0]}.` : 'Someone';
    createNotification(req.app.get('io'), {
      recipientId: parseInt(userId),
      type: 'friend_request',
      actorId: authUserId,
      targetId: authUserId,
      targetType: 'user',
      message: `${actorName} sent you a friend request`,
    }).catch(err => console.error('Friend request notification error:', err));
```

**Step 6: Add friend accept notification in friends.js**

In the accept endpoint (~line 203, after the system message INSERT and before the response), add:

```javascript
    // Notify the original requester that their request was accepted
    createNotification(req.app.get('io'), {
      recipientId: parseInt(userId),
      type: 'friend_accept',
      actorId: authUserId,
      targetId: authUserId,
      targetType: 'user',
      message: `${accepterName} accepted your friend request`,
    }).catch(err => console.error('Friend accept notification error:', err));
```

Note: `accepterName` is already defined on ~line 195 of the accept endpoint. And `userId` is the original requester's ID.

**Step 7: Add notification import and trigger to follows.js**

At the top of `follows.js`, after existing requires, add:

```javascript
const { createNotification } = require('../services/notifications');
```

In the follow endpoint (~line 33, after the INSERT and before the count query), add:

```javascript
    // Notify the followed user
    const [actorRows] = await hbcuPool.execute(
      'SELECT first_name, last_name FROM registry_data WHERE registry_id = ?', [req.userId]
    );
    const actorName = actorRows[0] ? `${actorRows[0].first_name} ${actorRows[0].last_name[0]}.` : 'Someone';
    createNotification(req.app.get('io'), {
      recipientId: followingId,
      type: 'follow',
      actorId: req.userId,
      targetId: req.userId,
      targetType: 'user',
      message: `${actorName} started following you`,
    }).catch(err => console.error('Follow notification error:', err));
```

Note: `follows.js` imports `hbcuPool` (not `mediaPool`). The `createNotification` service uses `mediaPool` internally for the notification insert. Both pools connect to the same `hbcu_central` database.

**Step 8: Verify all modified files parse**

```bash
cd /var/www/vhosts/hbcuconnect.com/httpdocs/hbcu-app/api
node -e "require('./routes/media')"
node -e "require('./routes/friends')"
node -e "require('./routes/follows')"
```

Expected: No errors.

**Step 9: Commit**

```bash
git add hbcu-app/api/routes/media.js hbcu-app/api/routes/friends.js hbcu-app/api/routes/follows.js
git commit -m "feat(notifications): add triggers for likes, comments, friends, follows"
```

---

### Task 5: Create NotificationBell Component (Mobile)

**Files:**
- Create: `hbcu-app/mobile/src/components/NotificationBell.js`

**Step 1: Create the NotificationBell component**

This component renders a bell icon with badge count and a dropdown modal showing 10 recent notifications. It follows the same pattern as `GearMenu` in `MainTabs.js`.

```javascript
import React, { useState, useCallback, useEffect, useRef } from 'react';
import {
  View, Text, TouchableOpacity, Modal, FlatList,
  StyleSheet, Dimensions, Platform,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { useNavigation } from '@react-navigation/native';
import api from '../services/api';
import { connectSocket } from '../services/socket';

const colors = {
  headerBg: '#1a1a2e',
  white: '#ffffff',
  hbcu: '#993300',
  unreadBg: '#FFF8F0',
};

const ICON_MAP = {
  like: 'heart',
  comment: 'chatbubble',
  friend_request: 'person-add',
  friend_accept: 'people',
  follow: 'person',
};

function timeAgo(dateStr) {
  const now = new Date();
  const d = new Date(dateStr);
  const diff = Math.floor((now - d) / 1000);
  if (diff < 60) return 'just now';
  if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
  if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
  if (diff < 604800) return Math.floor(diff / 86400) + 'd ago';
  return d.toLocaleDateString();
}

export default function NotificationBell() {
  const [menuVisible, setMenuVisible] = useState(false);
  const [notifications, setNotifications] = useState([]);
  const [unreadCount, setUnreadCount] = useState(0);
  const navigation = useNavigation();
  const menuVisibleRef = useRef(false);

  // Fetch unread count
  const fetchUnreadCount = useCallback(async () => {
    try {
      const res = await api.get('/notifications/unread-count');
      setUnreadCount(res.data.count || 0);
    } catch (err) {
      // Silently fail
    }
  }, []);

  // Poll every 30 seconds + fetch on mount
  useEffect(() => {
    fetchUnreadCount();
    const interval = setInterval(fetchUnreadCount, 30000);
    return () => clearInterval(interval);
  }, [fetchUnreadCount]);

  // Socket.IO real-time listener
  useEffect(() => {
    let sock = null;
    let handler = null;
    async function setup() {
      sock = await connectSocket();
      if (!sock) return;
      handler = (data) => {
        if (data.unread_count !== undefined) {
          setUnreadCount(data.unread_count);
        } else {
          setUnreadCount(prev => prev + 1);
        }
      };
      sock.on('new_notification', handler);
    }
    setup();
    return () => {
      if (sock && handler) sock.off('new_notification', handler);
    };
  }, []);

  // Fetch recent notifications when dropdown opens
  const openMenu = useCallback(async () => {
    setMenuVisible(true);
    menuVisibleRef.current = true;
    try {
      const res = await api.get('/notifications?page=1');
      const items = res.data.notifications || [];
      setNotifications(items);

      // Mark displayed unread notifications as read
      const unreadIds = items.filter(n => !n.is_read).map(n => n.notification_id);
      if (unreadIds.length > 0) {
        await api.put('/notifications/mark-read', { notification_ids: unreadIds });
        setUnreadCount(prev => Math.max(0, prev - unreadIds.length));
      }
    } catch (err) {
      console.error('Failed to load notifications:', err);
    }
  }, []);

  const closeMenu = () => {
    setMenuVisible(false);
    menuVisibleRef.current = false;
  };

  const handleTap = (item) => {
    closeMenu();
    if (item.target_type === 'media' && item.target_id) {
      navigation.navigate('PostDetail', { mediaId: item.target_id });
    } else if (item.target_type === 'user' && item.target_id) {
      navigation.navigate('ViewProfile', { userId: item.target_id });
    }
  };

  const handleViewAll = () => {
    closeMenu();
    navigation.navigate('Notifications');
  };

  const renderItem = ({ item }) => (
    <TouchableOpacity
      style={[styles.item, !item.is_read && styles.itemUnread]}
      onPress={() => handleTap(item)}
    >
      <View style={styles.iconWrap}>
        <Ionicons
          name={ICON_MAP[item.type] || 'notifications'}
          size={18}
          color={colors.hbcu}
        />
      </View>
      <View style={styles.itemContent}>
        <Text style={styles.itemText} numberOfLines={2}>{item.message}</Text>
        <Text style={styles.itemTime}>{timeAgo(item.created_at)}</Text>
      </View>
      {!item.is_read && <View style={styles.unreadDot} />}
    </TouchableOpacity>
  );

  // Limit dropdown to 10
  const displayItems = notifications.slice(0, 10);

  return (
    <>
      <TouchableOpacity style={styles.bellButton} onPress={openMenu}>
        <Ionicons name="notifications-outline" size={24} color={colors.white} />
        {unreadCount > 0 && (
          <View style={styles.badge}>
            <Text style={styles.badgeText}>
              {unreadCount > 99 ? '99+' : unreadCount}
            </Text>
          </View>
        )}
      </TouchableOpacity>

      <Modal
        visible={menuVisible}
        transparent
        animationType="fade"
        onRequestClose={closeMenu}
      >
        <TouchableOpacity
          style={styles.backdrop}
          activeOpacity={1}
          onPress={closeMenu}
        >
          <View style={styles.dropdown}>
            <View style={styles.dropdownHeader}>
              <Text style={styles.dropdownTitle}>Notifications</Text>
            </View>

            {displayItems.length === 0 ? (
              <View style={styles.empty}>
                <Text style={styles.emptyText}>No notifications yet</Text>
              </View>
            ) : (
              <FlatList
                data={displayItems}
                keyExtractor={item => String(item.notification_id)}
                renderItem={renderItem}
                style={styles.list}
              />
            )}

            <TouchableOpacity style={styles.viewAll} onPress={handleViewAll}>
              <Text style={styles.viewAllText}>View All Notifications</Text>
            </TouchableOpacity>
          </View>
        </TouchableOpacity>
      </Modal>
    </>
  );
}

const { width } = Dimensions.get('window');
const dropdownWidth = Math.min(width - 40, 360);

const styles = StyleSheet.create({
  bellButton: {
    paddingHorizontal: 12,
    paddingVertical: 8,
    position: 'relative',
  },
  badge: {
    position: 'absolute',
    top: 4,
    right: 6,
    backgroundColor: '#EF4444',
    borderRadius: 10,
    minWidth: 18,
    height: 18,
    alignItems: 'center',
    justifyContent: 'center',
    paddingHorizontal: 4,
  },
  badgeText: {
    color: '#fff',
    fontSize: 10,
    fontWeight: '700',
  },
  backdrop: {
    flex: 1,
    backgroundColor: 'rgba(0,0,0,0.3)',
    justifyContent: 'flex-start',
    alignItems: 'flex-end',
    paddingTop: Platform.OS === 'ios' ? 100 : 60,
    paddingRight: 10,
  },
  dropdown: {
    width: dropdownWidth,
    maxHeight: 480,
    backgroundColor: '#fff',
    borderRadius: 12,
    ...Platform.select({
      ios: { shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, shadowRadius: 8 },
      android: { elevation: 8 },
      web: { boxShadow: '0 4px 20px rgba(0,0,0,0.2)' },
    }),
    overflow: 'hidden',
  },
  dropdownHeader: {
    backgroundColor: colors.hbcu,
    paddingHorizontal: 16,
    paddingVertical: 12,
  },
  dropdownTitle: {
    color: '#fff',
    fontSize: 15,
    fontWeight: '600',
  },
  list: {
    maxHeight: 360,
  },
  item: {
    flexDirection: 'row',
    alignItems: 'center',
    paddingHorizontal: 14,
    paddingVertical: 12,
    borderBottomWidth: StyleSheet.hairlineWidth,
    borderBottomColor: '#eee',
  },
  itemUnread: {
    backgroundColor: colors.unreadBg,
  },
  iconWrap: {
    width: 34,
    height: 34,
    borderRadius: 17,
    backgroundColor: '#FEF3E7',
    alignItems: 'center',
    justifyContent: 'center',
    marginRight: 10,
  },
  itemContent: {
    flex: 1,
  },
  itemText: {
    fontSize: 13,
    color: '#333',
    lineHeight: 18,
  },
  itemTime: {
    fontSize: 11,
    color: '#999',
    marginTop: 2,
  },
  unreadDot: {
    width: 8,
    height: 8,
    borderRadius: 4,
    backgroundColor: colors.hbcu,
    marginLeft: 8,
  },
  empty: {
    padding: 30,
    alignItems: 'center',
  },
  emptyText: {
    color: '#999',
    fontSize: 14,
  },
  viewAll: {
    borderTopWidth: StyleSheet.hairlineWidth,
    borderTopColor: '#eee',
    paddingVertical: 12,
    alignItems: 'center',
  },
  viewAllText: {
    color: colors.hbcu,
    fontSize: 14,
    fontWeight: '600',
  },
});
```

**Step 2: Commit**

```bash
git add hbcu-app/mobile/src/components/NotificationBell.js
git commit -m "feat(notifications): add NotificationBell header component with dropdown"
```

---

### Task 6: Create NotificationsScreen (Mobile)

**Files:**
- Create: `hbcu-app/mobile/src/screens/notifications/NotificationsScreen.js`

**Step 1: Create the full notifications screen**

```javascript
import React, { useState, useCallback, useEffect } from 'react';
import {
  View, Text, FlatList, TouchableOpacity,
  StyleSheet, ActivityIndicator, RefreshControl,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { useNavigation } from '@react-navigation/native';
import api from '../../services/api';

const colors = {
  hbcu: '#993300',
  unreadBg: '#FFF8F0',
};

const ICON_MAP = {
  like: 'heart',
  comment: 'chatbubble',
  friend_request: 'person-add',
  friend_accept: 'people',
  follow: 'person',
};

function timeAgo(dateStr) {
  const now = new Date();
  const d = new Date(dateStr);
  const diff = Math.floor((now - d) / 1000);
  if (diff < 60) return 'just now';
  if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
  if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
  if (diff < 604800) return Math.floor(diff / 86400) + 'd ago';
  return d.toLocaleDateString();
}

export default function NotificationsScreen() {
  const [notifications, setNotifications] = useState([]);
  const [page, setPage] = useState(1);
  const [totalPages, setTotalPages] = useState(1);
  const [loading, setLoading] = useState(true);
  const [loadingMore, setLoadingMore] = useState(false);
  const [refreshing, setRefreshing] = useState(false);
  const navigation = useNavigation();

  const loadNotifications = useCallback(async (pageNum = 1, append = false) => {
    try {
      const res = await api.get(`/notifications?page=${pageNum}`);
      const items = res.data.notifications || [];
      setTotalPages(res.data.pages || 1);

      if (append) {
        setNotifications(prev => [...prev, ...items]);
      } else {
        setNotifications(items);
      }

      // Mark unread ones as read
      const unreadIds = items.filter(n => !n.is_read).map(n => n.notification_id);
      if (unreadIds.length > 0) {
        api.put('/notifications/mark-read', { notification_ids: unreadIds }).catch(() => {});
      }
    } catch (err) {
      console.error('Load notifications error:', err);
    }
  }, []);

  useEffect(() => {
    setLoading(true);
    loadNotifications(1).finally(() => setLoading(false));
  }, [loadNotifications]);

  const handleRefresh = async () => {
    setRefreshing(true);
    setPage(1);
    await loadNotifications(1);
    setRefreshing(false);
  };

  const handleLoadMore = async () => {
    if (loadingMore || page >= totalPages) return;
    setLoadingMore(true);
    const nextPage = page + 1;
    setPage(nextPage);
    await loadNotifications(nextPage, true);
    setLoadingMore(false);
  };

  const handleTap = (item) => {
    if (item.target_type === 'media' && item.target_id) {
      navigation.navigate('PostDetail', { mediaId: item.target_id });
    } else if (item.target_type === 'user' && item.target_id) {
      navigation.navigate('ViewProfile', { userId: item.target_id });
    }
  };

  const renderItem = ({ item }) => (
    <TouchableOpacity
      style={[styles.item, !item.is_read && styles.itemUnread]}
      onPress={() => handleTap(item)}
      activeOpacity={0.7}
    >
      <View style={styles.iconWrap}>
        <Ionicons
          name={ICON_MAP[item.type] || 'notifications'}
          size={20}
          color={colors.hbcu}
        />
      </View>
      <View style={styles.itemContent}>
        <Text style={styles.itemText}>{item.message}</Text>
        <Text style={styles.itemTime}>{timeAgo(item.created_at)}</Text>
      </View>
      {!item.is_read && <View style={styles.unreadDot} />}
    </TouchableOpacity>
  );

  if (loading) {
    return (
      <View style={styles.center}>
        <ActivityIndicator size="large" color={colors.hbcu} />
      </View>
    );
  }

  return (
    <FlatList
      data={notifications}
      keyExtractor={item => String(item.notification_id)}
      renderItem={renderItem}
      style={styles.container}
      refreshControl={
        <RefreshControl refreshing={refreshing} onRefresh={handleRefresh} colors={[colors.hbcu]} />
      }
      onEndReached={handleLoadMore}
      onEndReachedThreshold={0.3}
      ListFooterComponent={loadingMore ? (
        <ActivityIndicator style={styles.footer} size="small" color={colors.hbcu} />
      ) : null}
      ListEmptyComponent={
        <View style={styles.center}>
          <Ionicons name="notifications-off-outline" size={48} color="#ccc" />
          <Text style={styles.emptyText}>No notifications yet</Text>
        </View>
      }
    />
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f9f9f9',
  },
  center: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    padding: 40,
  },
  item: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#fff',
    paddingHorizontal: 16,
    paddingVertical: 14,
    borderBottomWidth: StyleSheet.hairlineWidth,
    borderBottomColor: '#eee',
  },
  itemUnread: {
    backgroundColor: colors.unreadBg,
  },
  iconWrap: {
    width: 40,
    height: 40,
    borderRadius: 20,
    backgroundColor: '#FEF3E7',
    alignItems: 'center',
    justifyContent: 'center',
    marginRight: 12,
  },
  itemContent: {
    flex: 1,
  },
  itemText: {
    fontSize: 14,
    color: '#333',
    lineHeight: 20,
  },
  itemTime: {
    fontSize: 12,
    color: '#999',
    marginTop: 3,
  },
  unreadDot: {
    width: 10,
    height: 10,
    borderRadius: 5,
    backgroundColor: colors.hbcu,
    marginLeft: 10,
  },
  emptyText: {
    color: '#999',
    fontSize: 16,
    marginTop: 12,
  },
  footer: {
    paddingVertical: 16,
  },
});
```

**Step 2: Commit**

```bash
git add hbcu-app/mobile/src/screens/notifications/NotificationsScreen.js
git commit -m "feat(notifications): add NotificationsScreen with infinite scroll"
```

---

### Task 7: Wire Up Navigation (MainTabs + AppNavigator)

**Files:**
- Modify: `hbcu-app/mobile/src/navigation/MainTabs.js:203-209` (add NotificationBell to header)
- Modify: `hbcu-app/mobile/src/navigation/AppNavigator.js:147-148` (register NotificationsScreen)

**Step 1: Add NotificationBell to MainTabs.js header**

At the top of `MainTabs.js`, add the import (alongside existing component imports):

```javascript
import NotificationBell from '../components/NotificationBell';
```

Then modify the `stackScreenOptions` object (line 203-209). Change line 208 from:

```javascript
  headerRight: () => <GearMenu />,
```

to:

```javascript
  headerRight: () => (
    <View style={{ flexDirection: 'row', alignItems: 'center' }}>
      <NotificationBell />
      <GearMenu />
    </View>
  ),
```

Also add `View` to the `react-native` import at the top of the file if not already there.

**Step 2: Register NotificationsScreen in AppNavigator.js**

At the top of `AppNavigator.js`, add the import:

```javascript
import NotificationsScreen from '../screens/notifications/NotificationsScreen';
```

After the last `<RootStack.Screen>` (BlockedUsers, ~line 147), add:

```javascript
        <RootStack.Screen
          name="Notifications"
          component={NotificationsScreen}
          options={{
            headerShown: true,
            headerStyle: { backgroundColor: colors.headerBg },
            headerTintColor: colors.white,
            headerTitle: 'Notifications',
            headerTitleStyle: { fontWeight: '600' },
          }}
        />
```

**Step 3: Commit**

```bash
git add hbcu-app/mobile/src/navigation/MainTabs.js hbcu-app/mobile/src/navigation/AppNavigator.js
git commit -m "feat(notifications): wire NotificationBell into header and register screen"
```

---

### Task 8: Restart API and Test End-to-End

**Step 1: Fix file ownership**

```bash
chown -R hbcuconnect:psacln /var/www/vhosts/hbcuconnect.com/httpdocs/hbcu-app/api/services/notifications.js
chown -R hbcuconnect:psacln /var/www/vhosts/hbcuconnect.com/httpdocs/hbcu-app/api/routes/notifications.js
```

**Step 2: Restart API**

```bash
cd /var/www/vhosts/hbcuconnect.com/httpdocs/hbcu-app/api
pm2 restart hbcu-api
```

Expected: Both instances restart with status "online".

**Step 3: Test API endpoints**

```bash
# Get a valid JWT token first (use an existing test user)
# Then test the endpoints:

# Test unread count
curl -s -H "Authorization: Bearer $TOKEN" https://hbcuconnect.com/api/notifications/unread-count

# Test notifications list
curl -s -H "Authorization: Bearer $TOKEN" https://hbcuconnect.com/api/notifications?page=1

# Test mark-read
curl -s -X PUT -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"notification_ids":[1]}' https://hbcuconnect.com/api/notifications/mark-read
```

**Step 4: Rebuild mobile preview**

```bash
cd /var/www/vhosts/hbcuconnect.com/httpdocs/hbcu-app/mobile
npx expo export --platform web --output-dir ../preview
```

**Step 5: Verify in browser**

Open the web preview and check:
1. Bell icon visible next to gear icon in header
2. Tapping bell opens dropdown with "No notifications yet"
3. Badge hidden when count is 0
4. "View All" navigates to NotificationsScreen

**Step 6: Test notification flow**

Using a second account, like a post from the first account. Check:
1. First account's bell badge increments
2. Opening dropdown shows the like notification
3. Tapping the notification navigates to the post

**Step 7: Final commit**

```bash
git add -A
git commit -m "feat(notifications): complete notifications system - API, real-time, mobile UI"
```
