# Plan: Social Platform — Follow, Post, Share, Chat & Spaces

---

## Completed (as of March 7, 2026)

### Phase 1: Follow System — DONE

The follow system is fully operational in production.

#### Follow API & Controller (`src/Controller/FollowController.php`)
| Route | Method | Status |
|---|---|---|
| `POST /api/follow/{userId}` | `follow()` | ✅ Done |
| `POST /api/unfollow/{userId}` | `unfollow()` | ✅ Done |
| `GET /api/is-following/{userId}` | `isFollowing()` | ✅ Done |
| `GET /users/{username}/followers` | `followers()` | ✅ Done |
| `GET /users/{username}/following` | `following()` | ✅ Done |
| `GET /api/followers/{userId}` | `getFollowers()` | ✅ Done |

#### Follow Notifications
- **Web notification**: When User A follows User B, B gets a web notification "أ بدأ بمتابعتك" via `UserNotification::notifyUser()` — stored in `user_web_notification` table.
- **Email notification**: Bilingual AR/EN email via Listmonk (template ID 9 — "Shamra Follow Notification TX"). Includes:
  - Follower's name, username, profile photo (or initial placeholder)
  - "View Profile" CTA button linking to follower's profile
  - Shamra branding with purple gradient header and logo
  - Full RTL support for Arabic locale
  - Locale-aware subject line: "فلان بدأ بمتابعتك | Shamra Academia" / "Name started following you | Shamra Academia"
- **Service method**: `ListmonkService::sendFollowNotificationEmail(User $recipient, User $follower)` — checks user preference, generates HMAC-signed unsubscribe URL, sends via Listmonk tx API.

#### Unsubscribe / Resubscribe
- **User preference**: `fos_user.follow_notification_enabled` (boolean, default `true`) — controls whether follow emails are sent.
- **Unsubscribe**: One-click link in email footer → `GET /newsletter/unsubscribe?email=...&token=...&type=follow` — sets preference to `false`, shows confirmation page.
- **Resubscribe**: One-click button on unsubscribe confirmation page → `GET /newsletter/resubscribe?email=...&token=...&type=follow` — sets preference back to `true`.
- **Security**: HMAC-SHA256 tokens (using `kernel.secret`), unforgeable, no expiry per RFC 8058. `List-Unsubscribe` header included for Gmail/Outlook one-click unsubscribe.

#### Key Files
| File | Purpose |
|---|---|
| `src/Controller/FollowController.php` | Follow/unfollow API, follower/following pages |
| `src/Service/ListmonkService.php` | `sendFollowNotificationEmail()`, template ID 9 |
| `src/Controller/NewsletterController.php` | Unsubscribe + resubscribe routes (type=follow) |
| `src/Entity/User.php` | `followNotificationEnabled` column, `follow()`/`unfollow()`/`isFollowing()` methods |
| `templates/newsletter/unsubscribe.html.twig` | Unsubscribe/resubscribe confirmation page |
| `migrations/Version20260306130000.php` | Added `follow_notification_enabled` to `fos_user` |
| Listmonk Template ID 9 | "Shamra Follow Notification TX" — bilingual email template |

#### Commits
- `f251c151` — feat: send email notification when a user gets followed (Listmonk template 9)
- `f508b080` — fix: follow email - bigger logo, RTL fix, one-click unsubscribe
- `0dc0ce7a` — feat: one-click resubscribe button on unsubscribe page for follow/recommendations

#### Still TODO for Phase 1
- [x] Follow button UI on profile page (`personal_info.html.twig`)
- [ ] Follow button on research cards next to author name
- [ ] "Followed user publishes research" notification
- [ ] "Followed user posts in a Space" notification

#### Completed Since Phase 1 (Mar 24, 2026)
- [x] Profile view counter displayed in hero stats bar (eye icon + `visitors` count)
- [x] Suggested researchers sidebar on `/feed` page (loads via `/api/suggested-researchers`)
- [x] Improved suggestion algorithm with multi-signal scoring (quality + recency + field match) and randomization
- [x] Online presence tracking (`lastActiveAt` field + `UserActivitySubscriber` throttled to 2min)
- [x] "Active Now" sidebar card on `/feed` page — shows online users with green dot, auto-refreshes every 60s
- [x] Green dot on suggested researcher avatars when online
- [x] `isOnlineNow()` method on User entity (5-minute threshold) — ready for chat integration

---

### Phase 6: Chat Messaging System — DONE

User-to-user direct messaging system deployed to production on March 7, 2026. Welcome chat auto-sends to new users (March 9, 2026).

> **Full spec, architecture, routes, tables, bugs, and future enhancements**: see [14-chat-messaging.md](14-chat-messaging.md)

#### Key Files (quick ref)
- `src/Service/ChatService.php` — business logic
- `src/Controller/ChatController.php` — 14 routes
- `src/Entity/Conversation.php` + `src/Entity/ChatMessage.php` — entities
- `templates/chat/inbox.html.twig` + `conversation.html.twig` — UI

#### Remaining Chat TODOs (tracked in [14-chat-messaging.md](14-chat-messaging.md))
- [ ] "Contact Researcher" on paper detail pages
- [ ] Typing indicators
- [ ] Image & file sharing
- [ ] Voice messages, reactions, link previews
- [ ] Community group chats
- [ ] Chat integration for online presence (green dot in chat topbar + inbox)

---

## TL;DR
Transform Shamra from a "library" into an **academic social platform** by building a unified social layer: user-to-user following, profile-level posts (status updates, paper sharing with commentary), comments on posts, **direct messaging between users**, and energizing the existing Spaces (Communities) system. Most primitives already exist in the codebase — the `user_follower` join table, the `CommunityQuestion` entity (= Post), `CommunityQuestionDiscussion` (= threaded Comments), notification infrastructure, and 23 community templates. This plan extends those primitives to user profiles, paper pages, and private conversations.

---

## What Already Exists (Audit)

| Concept | Existing Infrastructure | Table / Entity | Gap | Status |
|---|---|---|---|---|
| **Follow** | `user_follower` ManyToMany self-join on User | `user_follower` | ~~No controller/API~~ | ✅ Done |
| **Posts** | `CommunityQuestion` + new `ProfilePost` | `community_question`, `profile_post` | ~~Posts only in Spaces~~ | ✅ Done |
| **Comments (threaded)** | `ProfilePostComment` + `CommunityQuestionDiscussion` | `profile_post_comment` | ~~Only tied to CommunityQuestion~~ | ✅ Done |
| **Chat Messaging** | `Conversation` + `ChatMessage` entities | `conversation`, `chat_message` | ~~No user-to-user messaging~~ | ✅ Done |
| **Spaces** | `Community` entity — public/private, cover images, tags, members | `community` | Still underused; no cross-post | ❌ Phase 5 |
| **Notifications** | `UserWebNotification` + `UserNotification` service + Firebase push | `user_web_notification` | Works for follow, post, comment, chat | ✅ Done |
| **Feed** | `FeedService` with 4 tabs (All/Spaces/Researchers/Discover) | — (computed) | ~~No unified feed~~ | ✅ Done |
| **Profile** | `publicProfile`, `bio`, `image`, `visitors`, `studyField`, `interests`, social links | `fos_user` | ~~No activity stream~~ | ✅ Done |

---

## Architecture Decision: ProfilePost vs. Extending CommunityQuestion

Two possible approaches:

### Option A: New `ProfilePost` entity (Recommended)
Create a dedicated `profile_post` table for posts that live on a user's profile (not inside a Space). A `ProfilePost` can optionally reference a research paper (`Research` entity) for "share paper with comment" functionality.

**Pros**: Clean separation of concerns; profile posts have different rules (no community, no approval, author-owned). Can evolve independently (reactions, media attachments later). Simple queries: "give me all posts by user X."

**Cons**: New entity + migration. Some duplication with `CommunityQuestion`.

### Option B: Extend `CommunityQuestion` with nullable community
Make the `community` FK nullable — a question with `community = null` is a "profile post." Add a nullable `sharedResearch` FK.

**Pros**: Reuses existing code, templates, comments.

**Cons**: Muddies the entity; every query that fetches space posts must now filter out profile posts and vice versa. `CommunityQuestion` naming becomes confusing.

**Decision: Option A** — new `ProfilePost` entity. It keeps each concept clean and lets us add profile-specific features (paper sharing, pinned posts) without polluting the Space model.

---

## New Entities

### ProfilePost (`profile_post`)

| Column | Type | Notes |
|---|---|---|
| `id` | int (auto) | PK |
| `user_id` | FK → `fos_user` | Author |
| `content` | text | Post body (supports rich text / Markdown) |
| `shared_research_id` | FK → `research`, nullable | If sharing a paper, reference it |
| `shared_url` | string(500), nullable | Optional external link |
| `image` | string(255), nullable | Optional attached image |
| `visibility` | enum: `public`, `followers`, `private` | Default: `public` |
| `is_pinned` | bool, default false | Pin to top of profile |
| `likes_count` | int, default 0 | Denormalized like counter |
| `comments_count` | int, default 0 | Denormalized comment counter |
| `views` | int, default 0 | View counter |
| `slug` | string(30), unique | Auto `uniqid()` |
| `created_at` | datetime_immutable | |
| `updated_at` | datetime_immutable, nullable | |
| `deleted` | bool, default false | Soft delete |

### ProfilePostComment (`profile_post_comment`)

| Column | Type | Notes |
|---|---|---|
| `id` | int (auto) | PK |
| `post_id` | FK → `profile_post` | Parent post |
| `user_id` | FK → `fos_user` | Comment author |
| `parent_id` | FK → self, nullable | For nested threading (like `CommunityQuestionDiscussion`) |
| `content` | text | Comment body |
| `likes_count` | int, default 0 | |
| `slug` | string(30), unique | |
| `created_at` | datetime_immutable | |
| `updated_at` | datetime_immutable, nullable | |
| `deleted` | bool, default false | Soft delete |

### ProfilePostLike (`profile_post_like`)

| Column | Type | Notes |
|---|---|---|
| `id` | int (auto) | PK |
| `post_id` | FK → `profile_post`, nullable | Liked a post |
| `comment_id` | FK → `profile_post_comment`, nullable | Liked a comment |
| `user_id` | FK → `fos_user` | Who liked |
| `created_at` | datetime_immutable | |
| Unique constraint | `(user_id, post_id)` and `(user_id, comment_id)` | One like per user per item |

---

## Implementation Phases

### Phase 1: Follow System (1-2 days)

*Unchanged from original plan — this is the foundation.*

#### Step 1.1: Fix User Entity Follower Mapping

The current `$follower` mapping is unidirectional. Add the inverse side so we can query "who follows me":

```php
// User.php — owning side (users I follow)
#[ORM\ManyToMany(targetEntity: User::class, inversedBy: 'followers')]
#[ORM\JoinTable(name: 'user_follower')]
#[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id')]
#[ORM\InverseJoinColumn(name: 'follower_id', referencedColumnName: 'id')]
private Collection $following;

// User.php — inverse side (users who follow me)
#[ORM\ManyToMany(targetEntity: User::class, mappedBy: 'following')]
private Collection $followers;
```

Add helper methods: `follow(User $user)`, `unfollow(User $user)`, `isFollowing(User $user)`, `getFollowersCount()`, `getFollowingCount()`.

#### Step 1.2: Follow Controller + API

Create `src/Controller/FollowController.php`:

| Route | Method | Action |
|---|---|---|
| `POST /api/follow/{userId}` | `follow()` | Add to following |
| `POST /api/unfollow/{userId}` | `unfollow()` | Remove from following |
| `GET /api/is-following/{userId}` | `isFollowing()` | Check status (JSON) |
| `GET /users/{username}/followers` | `followers()` | Paginated follower list (HTML) |
| `GET /users/{username}/following` | `following()` | Paginated following list (HTML) |

#### Step 1.3: Follow Button on Profile + Research Cards

- Profile page (`personal_info.html.twig`): "Follow" / "Following" toggle button + follower/following counts
- Research cards: small "Follow" link next to author name (logged-in users only)
- JS: fetch API, optimistic UI update

#### Step 1.4: Follow Notifications

| Event | Notification |
|---|---|
| User A follows User B | B gets: "أ بدأ بمتابعتك" / "A started following you" |
| Followed user publishes research | "أ نشر بحثًا جديدًا: {title}" |
| Followed user posts in a Space | "أ نشر في {space}: {title}" |
| Followed user creates a profile post | "أ نشر تحديثًا جديدًا" (Phase 2) |

Use `CreateFollowerNotifications` Messenger message for batch notification when user has 50+ followers.

---

### Phase 2: Profile Posts — "Status Updates" (2-3 days)

#### Step 2.1: Create Entities + Migration

Create `ProfilePost`, `ProfilePostComment`, `ProfilePostLike` entities and generate migration.

#### Step 2.2: ProfilePost Controller

Create `src/Controller/ProfilePostController.php`:

| Route | Method | Action |
|---|---|---|
| `POST /api/profile-post` | `create()` | Create a new profile post (text, optional image) |
| `GET /api/profile-posts/{username}` | `list()` | Paginated posts for a user (JSON, for infinite scroll) |
| `GET /post/{slug}` | `show()` | Single post detail page (HTML, for SEO + sharing) |
| `PUT /api/profile-post/{slug}` | `update()` | Edit post (author only) |
| `DELETE /api/profile-post/{slug}` | `delete()` | Soft-delete post (author only) |
| `POST /api/profile-post/{slug}/like` | `toggleLike()` | Like / unlike |
| `POST /api/profile-post/{slug}/pin` | `togglePin()` | Pin / unpin (author only, max 3 pinned) |

#### Step 2.3: Comment Controller

Create `src/Controller/ProfilePostCommentController.php`:

| Route | Method | Action |
|---|---|---|
| `POST /api/profile-post/{slug}/comment` | `create()` | Add comment |
| `GET /api/profile-post/{slug}/comments` | `list()` | Paginated comments (JSON) |
| `PUT /api/profile-post-comment/{slug}` | `update()` | Edit (author only) |
| `DELETE /api/profile-post-comment/{slug}` | `delete()` | Soft-delete (author only) |
| `POST /api/profile-post-comment/{slug}/like` | `toggleLike()` | Like / unlike |
| `POST /api/profile-post-comment/{slug}/reply` | `reply()` | Nested reply |

#### Step 2.4: Profile Page — Activity Tab

Add a new "Posts" or "Activity" (نشاط / منشورات) tab to the profile page:
- Shows the user's posts in reverse chronological order (pinned first)
- Post composer at the top (own profile only): text area + optional image upload
- Each post card shows: avatar, name, time ago, content, shared paper (if any), like count, comment count, "Like" / "Comment" / "Share" buttons
- Infinite scroll pagination via JSON API

#### Step 2.5: Post Composer UI

A clean, minimal composer (not a full rich editor):
```
┌─────────────────────────────────────────┐
│ 📝 ماذا يدور في ذهنك؟                    │
│ What's on your mind?                    │
│                                         │
│                                         │
├─────────────────────────────────────────┤
│ 📷 Image  🔗 Link  🌐 Public ▾   [Post] │
└─────────────────────────────────────────┘
```

---

### Phase 3: Share Paper to Profile (1 day)

This is the killer academic-social feature — turning passive paper reading into social sharing.

#### Step 3.1: "Share to Profile" Button on Paper Pages

On research detail pages (e.g., `/show/{slug}`), add a "Share" button that opens a modal:

```
┌─────────────────────────────────────────────┐
│ 📄 شارك هذا البحث على ملفك الشخصي           │
│    Share this paper to your profile          │
│                                              │
│ ┌──────────────────────────────────────────┐ │
│ │ أضف تعليقك... / Add your comment...     │ │
│ │                                          │ │
│ └──────────────────────────────────────────┘ │
│                                              │
│ ┌──────────────────────────────────────────┐ │
│ │ 📄 Paper Title                           │ │
│ │ Authors · Journal · Year                 │ │
│ │ First 2 lines of abstract...             │ │
│ └──────────────────────────────────────────┘ │
│                                              │
│              [Cancel]  [Share / شارك]        │
└─────────────────────────────────────────────┘
```

#### Step 3.2: Implementation

- `POST /api/profile-post` with `sharedResearchId` parameter
- The `ProfilePost` record stores the `shared_research_id` FK
- On the feed / profile, shared-paper posts render with a rich paper card (title, authors, abstract snippet, link to paper)
- The paper's page can show "Shared by X researchers" count

#### Step 3.3: Paper Page Social Proof

On the paper detail page, add a sidebar section:
- "N researchers shared this paper"
- Show 3-5 avatars of sharers
- Link to see all shares (each share = a post with the user's commentary)

---

### Phase 4: Unified Feed — "Home Timeline" (2 days)

#### Step 4.1: Feed Aggregation Service

Create `src/Service/FeedService.php` that merges:
1. **Profile posts** from followed users
2. **Space posts** from followed Spaces (existing `CommunityQuestion`)
3. **Paper shares** from followed users (subset of profile posts)
4. **New research** published by followed users

Each feed item is normalized to a common shape:
```php
[
    'type' => 'profile_post' | 'space_post' | 'paper_published',
    'actor' => User,
    'content' => string,
    'paper' => ?Research,
    'space' => ?Community,
    'post' => ProfilePost | CommunityQuestion,
    'createdAt' => DateTimeImmutable,
]
```

#### Step 4.2: Feed Page

Replace or enhance the current `/communities/` feed page with a unified timeline:
- Tab 1: **All** (merged feed from followed users + spaces)
- Tab 2: **Spaces** (current behavior — posts from Spaces)
- Tab 3: **Researchers** (profile posts + paper shares from followed users only)
- Tab 4: **Discover** (trending posts across the platform, suggested content)

This extends the existing `communities_feed.html.twig` rather than replacing it.

#### Step 4.3: Feed Caching Strategy

- Use the same caching approach as `CommunityController::all()` (currently 5-minute cache)
- Cache key: `feed:{userId}:{tab}:{page}`
- Invalidate on: new post by followed user, new follow, new space join
- For "Discover" tab: longer cache (15 min), not user-specific

---

### Phase 5: Activate Spaces (1-2 days)

Spaces already work but feel disconnected. Integrate them into the social layer:

#### Step 5.1: Space Discovery from Profile

- On user profiles, show "Spaces" tab: spaces they own + spaces they're members of
- When viewing another researcher's profile, you see their spaces → natural discovery

#### Step 5.2: Cross-Post to Space

When creating a profile post, allow optionally cross-posting to a Space the user belongs to:
```
┌─────────────────────────────────────────┐
│ 📝 Post content...                      │
├─────────────────────────────────────────┤
│ 📷 Image  🔗 Link  🏘️ Post to: [My Profile ▾] │
│                     ├─ My Profile          │
│                     ├─ Space: AI Research   │
│                     └─ Space: Comp Science  │
└─────────────────────────────────────────┘
```

This drives content into Spaces organically, solving the "empty space" problem.

#### Step 5.3: Space Recommendations in Feed

In the "Discover" tab of the feed, intersperse:
- Trending posts from public Spaces (already implemented in `CommunityController`)
- "Join this Space" cards based on user's `studyField` and `interests` tags (algorithm already exists in `suggestCommunities()`)

#### Step 5.4: "Create Space" Prompt

If a user has 10+ followers and no spaces, nudge them:
> "You have 15 followers! Create a Space to build a community around your research area."

---

## Data Model Diagram

```
┌──────────┐  follows   ┌──────────┐
│  User A  │───────────▶│  User B  │
└──────────┘            └──────────┘
     │                       │
     │ creates               │ creates
     ▼                       ▼
┌──────────────┐      ┌──────────────┐
│ ProfilePost  │      │ ProfilePost  │
│ (text/share) │      │ (paper share)│
└──────────────┘      └──────────────┘
     │                       │
     │ has                   │ references
     ▼                       ▼
┌────────────────┐    ┌──────────┐
│ProfilePostComment│   │ Research │
│ (nested)       │    │ (paper)  │
└────────────────┘    └──────────┘

     ┌──────────┐  member_of  ┌───────────┐
     │  User A  │────────────▶│ Community │
     └──────────┘             │ (Space)   │
                              └───────────┘
                                   │
                                   │ has
                                   ▼
                          ┌─────────────────┐
                          │CommunityQuestion│
                          │ (Space Post)    │
                          └─────────────────┘
                                   │
                                   │ has
                                   ▼
                          ┌─────────────────────────┐
                          │CommunityQuestionDiscussion│
                          │ (Space Comment, nested)  │
                          └─────────────────────────┘
```

---

## Migration Summary

| Migration | What |
|---|---|
| `Version20260310100000` | Fix `user_follower` to support bidirectional mapping (may need column rename) |
| `Version20260310100100` | Create `profile_post` table |
| `Version20260310100200` | Create `profile_post_comment` table |
| `Version20260310100300` | Create `profile_post_like` table |

---

## Translation Keys

### Arabic (`translations/Social.ar.yml`)
```yaml
social.follow: "متابعة"
social.following: "متابَع"
social.unfollow: "إلغاء المتابعة"
social.followers: "المتابعون"
social.following_list: "يتابع"
social.posts: "المنشورات"
social.share_paper: "شارك البحث"
social.add_comment: "أضف تعليقًا..."
social.whats_on_your_mind: "ماذا يدور في ذهنك؟"
social.like: "إعجاب"
social.comment: "تعليق"
social.share: "مشاركة"
social.pin_post: "تثبيت المنشور"
social.shared_paper: "شارك بحثًا"
social.published_paper: "نشر بحثًا جديدًا"
social.started_following: "بدأ بمتابعتك"
social.post_to: "نشر في"
social.my_profile: "ملفي الشخصي"
social.discover: "اكتشف"
social.researchers_shared: "باحث شاركوا هذا البحث"
social.create_space_prompt: "لديك {count} متابعين! أنشئ مساحة لبناء مجتمع حول مجال بحثك."
social.all_feed: "الكل"
social.spaces_feed: "المساحات"
social.researchers_feed: "الباحثون"
```

### English (`translations/Social.en.yml`)
```yaml
social.follow: "Follow"
social.following: "Following"
social.unfollow: "Unfollow"
social.followers: "Followers"
social.following_list: "Following"
social.posts: "Posts"
social.share_paper: "Share Paper"
social.add_comment: "Add a comment..."
social.whats_on_your_mind: "What's on your mind?"
social.like: "Like"
social.comment: "Comment"
social.share: "Share"
social.pin_post: "Pin Post"
social.shared_paper: "shared a paper"
social.published_paper: "published a new paper"
social.started_following: "started following you"
social.post_to: "Post to"
social.my_profile: "My Profile"
social.discover: "Discover"
social.researchers_shared: "researchers shared this paper"
social.create_space_prompt: "You have {count} followers! Create a Space to build a community around your research area."
social.all_feed: "All"
social.spaces_feed: "Spaces"
social.researchers_feed: "Researchers"
```

---

## Implementation Status

> Last updated: March 7, 2026

| Phase | Feature | Status | Deployed | Key Commits |
|-------|---------|--------|----------|-------------|
| **1** | Follow System (API + UI + notifications) | ✅ Complete | 2026-03-03 | Entity mapping, `FollowController`, profile button, notifications |
| **2** | Profile Posts (create, list, comment, like) | ✅ Complete | 2026-03-04 | Entities, controllers, templates, migrations |
| **3** | Share Paper to Profile | ✅ Complete | 2026-03-05 | Share modal, `PaperResolverService`, rich cards, social proof sidebar |
| **4** | Unified Feed / Timeline | ✅ Complete | 2026-03-05 | `FeedService`, feed tabs (All/Spaces/Researchers/Discover), backfill logic |
| **5** | Activate Spaces | ❌ Not started | — | — |
| **6** | Chat Messaging System | ✅ Complete | 2026-03-07 | `422a8d73` — Conversation + ChatMessage entities, ChatService, 14-route ChatController, inbox + conversation templates, async notifications (web/push/email), header badge |

### Post-Launch Fixes & Enhancements (March 5, 2026)

| Fix / Feature | Commit | Details |
|---|---|---|
| ProfilePostRepository autowiring | `94e15e12` | Missing service wiring for ProfilePostRepository |
| URL validation (relative URLs) | `7f1f1ce1` | `FILTER_VALIDATE_URL` rejects relative URLs — added custom validation |
| Rich paper preview cards | `d6c42794` | Bare URLs replaced with rich paper cards in shared posts |
| ES-only paper resolution | `4db3c896` | Created `PaperResolverService` — 4-step lookup (MySQL Arabic → MySQL English → ES Arabic → ES English) |
| SEO: OG, Twitter Cards, JSON-LD | `91b9de1c`, `dd7a8dd7` | OpenGraph + Twitter Card meta tags + JSON-LD Article schema on post pages |
| Comment controller JSON fix | `57fc57f1` | Controller reads form data but JS sends JSON — added `getRequestParam()` helper |
| Comment button focus guard | `b1fc4b8a`, `846584f4` | Null check for logged-out users + correct `app_login` route |
| Feed: backfill all tab | `57753678`, `688e78ed` | "All" tab always backfills with discover content; trending tiebreaker uses `createdAt` |
| Hide suggested researchers (anon) | `78afa473` | Sidebar spinner for logged-out users — wrapped in `{% if isAuthenticated %}` |
| Email notifications (Listmonk) | `0b3be4ae` | Bilingual email on comment/reply via Listmonk transactional API (template ID 8) |
| Comment edit/delete UI | `ad29a39f` | Three-dot menu on comments with inline edit + delete (author + post owner) |

---

## Priority & Effort Estimate

| Phase | Feature | Effort | Priority | Depends On | Status |
|-------|---------|--------|----------|------------|--------|
| **1** | Follow System (API + UI + notifications) | 1-2 days | **P0 — must ship first** | Nothing | ✅ Done |
| **2** | Profile Posts (create, list, comment, like) | 2-3 days | **P0 — core social feature** | Phase 1 | ✅ Done |
| **3** | Share Paper to Profile | 1 day | **P1 — high impact, low effort** | Phase 2 | ✅ Done |
| **4** | Unified Feed / Timeline | 2 days | **P1 — ties everything together** | Phases 1-3 | ✅ Done |
| **5** | Activate Spaces (cross-post, discovery, prompts) | 1-2 days | **P2 — growth accelerator** | Phase 4 | ❌ Not started |
| **6** | Chat Messaging | 1 day | **P1 — core social feature** | Phase 1 | ✅ Done |

**Total: ~8-11 days of focused development.**
**Phases 1-4 + 6 completed in ~4 days. Phase 5 remaining.**

---

## Why This Works for an Academic Platform

1. **Paper sharing with commentary** is the academic equivalent of a retweet-with-quote. Researchers naturally want to say "I found this interesting because..." — this gives them a home for that.

2. **Profile posts** aren't just vanity — they're academic micro-blogging. Share a conference observation, a methodology tip, a call for collaborators. Short-form content fills the gap between "publishing a paper" (months) and "nothing" (today).

3. **Following is one-directional** (like Twitter/X, not Facebook). This fits academia: a grad student follows a professor, not the other way around. No awkward friend requests.

4. **Spaces become communities of practice**, not just empty forums. Cross-posting from profile to Space means content flows in naturally. Researchers post something, then choose to share it with their Space too.

5. **The feed becomes the homepage**. Instead of landing on a search page, logged-in users see what their network is discussing, sharing, and publishing. This is what drives daily active usage.

---

## Verification Checklist

### Follow & Social (this file)
- [x] Follow button appears on other users' profiles; toggles correctly
- [x] Follower/following counts update in real-time
- [x] Followers/following pages show correct paginated lists
- [x] Post composer appears on own profile; creates post successfully
- [ ] Posts with optional images render correctly
- [x] Shared-paper posts show rich paper card with link
- [x] Comments (threaded) work on profile posts
- [x] Like/unlike toggles on posts and comments
- [x] Unified feed shows mixed content from followed users + spaces
- [x] Feed tabs (All / Spaces / Researchers / Discover) filter correctly
- [x] Notifications fire for: new follower, followed user posts, followed user shares paper
- [ ] Cross-post to Space works from profile post composer
- [ ] User's spaces appear on their profile
- [x] Suggested researchers and spaces appear in Discover tab
- [x] Arabic/English translations render correctly throughout
- [x] Mobile-responsive layout for all new UI components
- [x] SEO: OpenGraph, Twitter Cards, JSON-LD on post detail pages
- [x] Comment edit/delete UI for comment authors (and post owners can delete)
- [x] Email notifications on comment/reply via Listmonk (bilingual, locale-aware)
- [x] Feed backfills with discover content when user follows nobody

### Chat (see [14-chat-messaging.md](14-chat-messaging.md) for full status)
- [x] Core chat deployed (inbox, conversations, send/receive, read receipts, block, search)
- [x] Online presence tracking (`isOnlineNow()`, feed sidebar green dots)

---

## Decisions Log

| Decision | Choice | Rationale |
|---|---|---|
| Profile posts vs. extend CommunityQuestion | **New `ProfilePost` entity** | Clean separation; different rules (no community, no approval, author-owned); avoids polluting Space queries |
| Follow model | **One-directional** (like Twitter) | Fits academic hierarchy; simpler than mutual friendships |
| Comment threading | **Nested (self-referencing FK)** | Mirrors existing `CommunityQuestionDiscussion` pattern; users expect it |
| Feed storage | **Computed on-the-fly** (like current CommunityFeeds) | Simpler than fan-out-on-write; fine for current scale. Revisit if feed latency > 500ms |
| Likes | **Simple toggle, denormalized counter** | Sufficient for MVP; no need for reaction types yet |
| Visibility | **Public / Followers / Private** | Gives researchers control; default public for maximum discoverability |
| Spaces activation | **Cross-post, not replace** | Profile posts and Space posts are complementary, not competing |
| Comment notifications | **Listmonk transactional email** | Template ID 8, bilingual (Go template with locale switch), sent on comment + reply |
| Paper resolution | **4-step fallback** (MySQL Arabic → English → ES Arabic → ES English) | Papers exist in 3 data sources; `PaperResolverService` handles all cases |
| Comment edit/delete | **Inline UI with three-dot menu** | Author can edit/delete own comments; post owner can delete any comment |

> Chat-specific decisions (model, polling, notifications, message types) are in [14-chat-messaging.md](14-chat-messaging.md).

---

## Researcher Suggestion Algorithm — Design & Recommendation

> Added March 5, 2026 after analyzing production data.

### Current Implementation (Updated March 24, 2026)

`findSuggestedResearchers()` in `FollowController.php` was rewritten with a multi-signal scoring + randomization algorithm:

#### Scoring System (0–11 points per user)

| Signal | Points | Logic |
|--------|--------|-------|
| Real profile photo (not default) | +2 | `image NOT LIKE 'default_profile_pic%'` |
| Has bio | +1 | `bio IS NOT NULL AND bio != ''` |
| Logged in within last 7 days | +3 | `last_login >= NOW() - 7 days` — active users |
| Joined within last 30 days | +1 | `created_at >= NOW() - 30 days` — new members |
| ≥10 profile views | +1 | `visitors >= 10` — some traction |
| Same study field | +3 | `study_field_id = :userFieldId` |

#### Selection Strategy

1. Fetch a pool of 24 candidates (6× the 4 slots needed) sorted by `score DESC, RAND()`
2. Pick top 2 by score (ensures quality)
3. Fill remaining 2 from pool randomly (ensures variety)
4. Shuffle final order (no stale positions)

This replaces the old approach (single field-match signal + `visitors DESC` — always showed the same 4 users).

#### Previous Implementation (before March 24, 2026)

Old `findSuggestedResearchers()` did:
1. Exclude already-followed users + self
2. Prefer users with same `studyField` (via DQL `CASE WHEN`)
3. Fallback sort by `visitors` DESC
4. Limit 5

This was basic — single signal, no quality filters, no randomization, always the same top 4.

### Production Data Reality (as of March 2026)

| Signal | Fill Rate | Notes |
|--------|-----------|-------|
| **`field_id`** (study field) | 5,945 / 32,706 (18%) | **Best structured signal** — 50+ distinct fields |
| **`tag_interests_user`** (auto-tracked tags) | 4,042 users, ~36K rows, avg 9 tags/user | **Second best signal** — accumulated from research browsing |
| **`user_interests`** (manual interests) | 64 users, 207 rows | Too sparse to rely on |
| **`user_read_history`** | 22 users, 78 rows | Newly introduced, still growing |
| **`work`** (university/institution) | 15 users | Nearly empty |
| **`bio`** | 5 users | Nearly empty |
| **`address`** | 15 users | Nearly empty |
| **`study_degree`** | 26 users | Nearly empty |
| **Follow relationships** | 0 total | Social layer is brand new |
| **`publicProfile`** flag | Available | Filter for suggestionable users |

### Recommendation: Stay with MySQL — Skip Elasticsearch

**ES would be overkill for this use case because:**
1. **Dataset is small** — 33K users, 6K with fields, 4K with tags. MySQL handles this in <50ms with proper indexes.
2. **No semantic matching needed yet** — signals are structured (field IDs, tag IDs), not free text.
3. **Additional infrastructure cost** — maintaining a user index, reindexing on profile changes, syncing deletes.
4. **The "exclude already followed" requirement** is trivially handled with `NOT IN` in SQL, but requires a `must_not` + terms query in ES that's more complex to maintain.

**When to reconsider ES:**
- 100K+ active users with rich profiles
- Need semantic matching ("machine learning" ≈ "deep learning" ≈ "تعلم آلي")
- Want to match on free-text bio/description fields
- Need to factor in research paper content similarity (user A reads papers similar to user B)

### Proposed Multi-Signal Scoring Algorithm

Replace the current single-signal approach with a weighted scoring system:

```
Score = (field_match × 40) + (shared_tags × 5) + (has_profile_pic × 5) + (has_posts × 10) + (popularity_bonus)
```

| Signal | Points | Logic |
|--------|--------|-------|
| Same `field_id` | +40 | Strongest structured signal |
| Each shared tag interest | +5 (per tag) | Users who browse similar research |
| Has non-default profile image | +5 | Indicator of active/real profile |
| Has published posts | +10 | Active content creator |
| Visitor count (popularity) | +1–10 | `LEAST(visitors / 100, 10)` capped |

### Implementation: Single Efficient SQL Query

```sql
SELECT u.id, u.username, u.first_name, u.last_name, u.image,
       f.arabic_full_name, f.english_full_name,
       (CASE WHEN u.field_id = :userFieldId THEN 40 ELSE 0 END)
       + COALESCE(shared.tag_count, 0) * 5
       + (CASE WHEN u.image IS NOT NULL
               AND u.image != 'default_profile_pic_m.png'
               AND u.image != 'default_profile_pic_f.png'
          THEN 5 ELSE 0 END)
       + (CASE WHEN EXISTS (
              SELECT 1 FROM profile_post pp
              WHERE pp.user_id = u.id AND pp.deleted = 0
          ) THEN 10 ELSE 0 END)
       + LEAST(u.visitors / 100, 10)
       AS score
FROM fos_user u
LEFT JOIN academy_field f ON u.field_id = f.id
LEFT JOIN (
    SELECT tiu2.user_id, COUNT(*) as tag_count
    FROM tag_interests_user tiu2
    WHERE tiu2.tag_id IN (
        SELECT tag_id FROM tag_interests_user WHERE user_id = :currentUserId
    )
    GROUP BY tiu2.user_id
) shared ON shared.user_id = u.id
WHERE u.id NOT IN (:excludeIds)
  AND u.enabled = 1
  AND u.publicProfile = 1
ORDER BY score DESC, u.visitors DESC
LIMIT :limit
```

**Performance**: The subquery on `tag_interests_user` benefits from an index on `(tag_id, user_id)`. For 36K rows, this runs in <50ms. If it ever slows down, pre-materialize shared tag counts into a `user_similarity_cache` table via a nightly cron.

### Display: Show Field Instead of University (for now)

Since only 15 users have `work` filled in, **show `studyField` (academy_field) as the subtitle** in suggestion cards. This covers 18% of users — far better than university coverage.

Display priority for suggestion card subtitle:
1. `work` (university/institution) — if filled (15 users)
2. `academy_field.arabic_full_name` / `english_full_name` — if set (5,945 users)
3. Nothing — for users with neither

### Future Enhancements (when data grows)

1. **"Read the same papers" signal** — once `user_read_history` has meaningful data, add: "users who read papers you read" as a collaborative filtering signal (+3 pts per shared paper read)
2. **Mutual followers** — "users followed by people you follow" (social graph proximity)
3. **Community co-membership** — users in the same Spaces
4. **Diversification** — avoid showing 5 users all from the same field; enforce max 2 per field in results
5. **Negative signals** — if a user dismisses a suggestion, exclude them for 30 days (requires a `suggestion_dismissal` table)


