# Comment Threading Skill

> **Domain:** Nested comment structures, hierarchical rendering, and parent-child relationships

---

## Purpose

This skill covers nested comment patterns, recursive template rendering, max depth handling, and thread traversal for the Comments Press Zone plugin.

---

## Comment Hierarchy Concepts

### WordPress Comment Structure

| Field | Purpose |
|-------|---------|
| `comment_ID` | Unique identifier |
| `comment_parent` | Parent comment ID (0 for top-level) |
| `comment_post_ID` | Post/page this comment belongs to |

### Depth Levels

```
Level 0 (Top-level)
├─ Level 1 (Reply to top-level)
│  ├─ Level 2 (Reply to level 1)
│  │  └─ Level 3 (Reply to level 2)
│  └─ Level 2
└─ Level 1
```

**Recommended max depth:** 5-6 levels

---

## Fetching Threaded Comments

### Get Comments with Children

```php
public function get_threaded_comments(int $post_id, array $args = []): array {
    $defaults = [
        'post_id' => $post_id,
        'status' => 'approve',
        'hierarchical' => 'flat', // Get flat list first
        'order' => 'ASC',
        'orderby' => 'comment_date_gmt',
    ];
    
    $args = array_merge($defaults, $args);
    $comments = get_comments($args);
    
    // Build hierarchy
    return $this->build_comment_tree($comments);
}

private function build_comment_tree(array $comments, int $parent_id = 0): array {
    $branch = [];
    
    foreach ($comments as $comment) {
        if ($comment->comment_parent == $parent_id) {
            $children = $this->build_comment_tree($comments, $comment->comment_ID);
            
            $item = [
                'id' => $comment->comment_ID,
                'author' => $comment->comment_author,
                'content' => $comment->comment_content,
                'date' => $comment->comment_date,
                'parent' => $comment->comment_parent,
                'children' => $children,
            ];
            
            $branch[] = $item;
        }
    }
    
    return $branch;
}
```

---

## Recursive Template Rendering

### Main Template (comments-list.php)

```php
<?php
/**
 * Comments list template
 *
 * @package CommentsPressZone
 */

if (!defined('ABSPATH')) {
    exit;
}

$post_id = get_the_ID();
$comments = $this->get_threaded_comments($post_id);
?>

<div class="presszone-comments-container">
    <h2 class="presszone-comments-heading">
        <?php echo esc_html__('Comments', 'comments-press-zone'); ?>
    </h2>
    
    <?php if (empty($comments)): ?>
        <p class="presszone-comments-empty">
            <?php echo esc_html__('No comments yet.', 'comments-press-zone'); ?>
        </p>
    <?php else: ?>
        <ul class="presszone-comments-list">
            <?php foreach ($comments as $comment): ?>
                <?php
                $args = [
                    'comment' => $comment,
                    'depth' => 1,
                    'max_depth' => 5,
                ];
                include PRESSZONE_COMMENTS_PATH . 'templates/partials/comment-item.php';
                ?>
            <?php endforeach; ?>
        </ul>
    <?php endif; ?>
</div>
```

### Recursive Item Template (partials/comment-item.php)

```php
<?php
/**
 * Single comment item (recursive)
 *
 * @package CommentsPressZone
 */

if (!defined('ABSPATH')) {
    exit;
}

$comment_id = $comment['id'];
$depth = $args['depth'] ?? 1;
$max_depth = $args['max_depth'] ?? 5;
$has_children = !empty($comment['children']);
?>

<li class="presszone-comments-item" data-comment-id="<?php echo esc_attr($comment_id); ?>" data-depth="<?php echo esc_attr($depth); ?>">
    <div class="presszone-comments-item__header">
        <span class="presszone-comments-author">
            <?php echo esc_html($comment['author']); ?>
        </span>
        <span class="presszone-comments-date">
            <?php echo esc_html(human_time_diff(strtotime($comment['date']), current_time('timestamp'))); ?>
            <?php echo esc_html__('ago', 'comments-press-zone'); ?>
        </span>
    </div>
    
    <div class="presszone-comments-item__content">
        <?php echo wp_kses_post($comment['content']); ?>
    </div>
    
    <div class="presszone-comments-item__actions">
        <?php if ($depth < $max_depth): ?>
            <button 
                type="button"
                class="presszone-comments-reply-btn"
                data-comment-id="<?php echo esc_attr($comment_id); ?>"
                aria-label="<?php echo esc_attr__('Reply to this comment', 'comments-press-zone'); ?>">
                <?php echo esc_html__('Reply', 'comments-press-zone'); ?>
            </button>
        <?php endif; ?>
    </div>
    
    <?php if ($has_children && $depth < $max_depth): ?>
        <ul class="presszone-comments-list presszone-comments-list--nested">
            <?php foreach ($comment['children'] as $child): ?>
                <?php
                $args = [
                    'comment' => $child,
                    'depth' => $depth + 1,
                    'max_depth' => $max_depth,
                ];
                include PRESSZONE_COMMENTS_PATH . 'templates/partials/comment-item.php';
                ?>
            <?php endforeach; ?>
        </ul>
    <?php endif; ?>
</li>
```

---

## Max Depth Handling

### Flatten Deeply Nested Comments

```php
public function get_threaded_comments(int $post_id, int $max_depth = 5): array {
    $comments = get_comments([
        'post_id' => $post_id,
        'status' => 'approve',
        'hierarchical' => 'flat',
    ]);
    
    return $this->build_comment_tree_with_limit($comments, 0, 1, $max_depth);
}

private function build_comment_tree_with_limit(
    array $comments,
    int $parent_id,
    int $current_depth,
    int $max_depth
): array {
    $branch = [];
    
    foreach ($comments as $comment) {
        if ($comment->comment_parent == $parent_id) {
            $children = [];
            
            // Only recurse if we haven't reached max depth
            if ($current_depth < $max_depth) {
                $children = $this->build_comment_tree_with_limit(
                    $comments,
                    $comment->comment_ID,
                    $current_depth + 1,
                    $max_depth
                );
            }
            
            $branch[] = [
                'id' => $comment->comment_ID,
                'author' => $comment->comment_author,
                'content' => $comment->comment_content,
                'date' => $comment->comment_date,
                'parent' => $comment->comment_parent,
                'children' => $children,
                'depth' => $current_depth,
            ];
        }
    }
    
    return $branch;
}
```

---

## Reply Context Display

### Show Parent Comment Context

```php
public function get_reply_context(int $comment_id): ?array {
    $comment = get_comment($comment_id);
    
    if (!$comment || $comment->comment_parent == 0) {
        return null;
    }
    
    $parent = get_comment($comment->comment_parent);
    
    if (!$parent) {
        return null;
    }
    
    return [
        'parent_id' => $parent->comment_ID,
        'parent_author' => $parent->comment_author,
        'parent_excerpt' => wp_trim_words($parent->comment_content, 15),
    ];
}
```

### Reply Context UI

```php
<?php $context = $this->get_reply_context($comment_id); ?>
<?php if ($context): ?>
    <div class="presszone-comments-reply-context">
        <span class="presszone-comments-reply-context__label">
            <?php echo esc_html__('Replying to', 'comments-press-zone'); ?>
        </span>
        <span class="presszone-comments-reply-context__author">
            <?php echo esc_html($context['parent_author']); ?>
        </span>
        <span class="presszone-comments-reply-context__excerpt">
            "<?php echo esc_html($context['parent_excerpt']); ?>..."
        </span>
    </div>
<?php endif; ?>
```

---

## Thread Traversal Algorithms

### Get All Descendants

```php
public function get_comment_descendants(int $comment_id): array {
    global $wpdb;
    
    $descendants = [];
    $queue = [$comment_id];
    
    while (!empty($queue)) {
        $parent_id = array_shift($queue);
        
        $children = $wpdb->get_results($wpdb->prepare(
            "SELECT comment_ID FROM {$wpdb->comments} 
             WHERE comment_parent = %d AND comment_approved = '1'",
            $parent_id
        ), ARRAY_A);
        
        foreach ($children as $child) {
            $child_id = $child['comment_ID'];
            $descendants[] = $child_id;
            $queue[] = $child_id;
        }
    }
    
    return $descendants;
}
```

### Count Replies (Direct Children Only)

```php
public function count_direct_replies(int $comment_id): int {
    global $wpdb;
    
    return (int) $wpdb->get_var($wpdb->prepare(
        "SELECT COUNT(*) FROM {$wpdb->comments} 
         WHERE comment_parent = %d AND comment_approved = '1'",
        $comment_id
    ));
}
```

### Count All Replies (Including Nested)

```php
public function count_all_replies(int $comment_id): int {
    $descendants = $this->get_comment_descendants($comment_id);
    return count($descendants);
}
```

---

## CSS Styling for Depth

```scss
.presszone-comments-list {
    list-style: none;
    padding: 0;
    margin: 0;
    
    &--nested {
        margin-left: $presszone-comments-spacing-xl;
        margin-top: $presszone-comments-spacing-md;
        padding-left: $presszone-comments-spacing-lg;
        border-left: 2px solid $presszone-comments-border;
        
        .dark-mode & {
            border-left-color: $presszone-comments-border-dark;
        }
    }
}

.presszone-comments-item {
    margin-bottom: $presszone-comments-spacing-lg;
    
    // Reduce spacing at deeper levels
    &[data-depth="3"],
    &[data-depth="4"],
    &[data-depth="5"] {
        .presszone-comments-list--nested {
            margin-left: $presszone-comments-spacing-md;
            padding-left: $presszone-comments-spacing-sm;
        }
    }
}
```

---

## Frontend Reply Handling

```javascript
function handleReply(commentId) {
    const replyForm = document.querySelector('.presszone-comments-reply-form');
    const parentInput = replyForm.querySelector('input[name="parent"]');
    
    // Set parent comment ID
    parentInput.value = commentId;
    
    // Move form to reply position
    const commentItem = document.querySelector(`[data-comment-id="${commentId}"]`);
    const actions = commentItem.querySelector('.presszone-comments-item__actions');
    actions.appendChild(replyForm);
    
    // Focus textarea
    replyForm.querySelector('textarea').focus();
    
    // Show cancel button
    showCancelReplyButton(replyForm);
}

function cancelReply() {
    const replyForm = document.querySelector('.presszone-comments-reply-form');
    const parentInput = replyForm.querySelector('input[name="parent"]');
    
    // Reset parent
    parentInput.value = '0';
    
    // Move form back to main position
    const mainContainer = document.querySelector('.presszone-comments-container');
    mainContainer.appendChild(replyForm);
}
```

---

## Common Mistakes to Avoid

| Mistake | Fix |
|---------|-----|
| Not limiting max depth | Set reasonable limit (5-6 levels) |
| Infinite recursion | Always check depth limit |
| Not escaping output | Use `esc_html()`, `wp_kses_post()` |
| Hardcoded depth values | Make max depth configurable |
| Missing reply context | Show parent comment when replying |

---

## Testing Checklist

- [ ] Top-level comments render correctly
- [ ] Nested replies render recursively
- [ ] Max depth enforced (no reply button beyond limit)
- [ ] Reply context shows parent comment
- [ ] All descendants retrieved correctly
- [ ] Reply counts accurate
- [ ] CSS indentation works at all depths
- [ ] Reply form moves to correct position
