Skip to main content

Overview

Jefftube’s comment system enables users to leave feedback and engage in discussions about videos. The platform supports threaded conversations with a full reply system, reactions, and rate limiting to ensure quality discussions.

Leaving Comments

You can comment on any video from the video player page. The comment input appears directly below the video information.
1

Focus the Input

Click or tap the “Add a comment…” text field. The input expands to show action buttons.
2

Write Your Comment

Type your comment (up to 300 characters). The input field will expand as you type.
Press Enter to submit or Shift + Enter to add a line break.
3

Submit or Cancel

Click the Comment button to post, or Cancel to dismiss.

Comment Input Interface

<div className="flex gap-3">
  {/* User avatar with initial */}
  <div className="w-10 h-10 rounded-full bg-tertiary">
    <span>{userInitial}</span>
  </div>
  
  {/* Expandable textarea */}
  <textarea
    placeholder="Add a comment..."
    rows={1}
    className="border-b focus:border-primary"
    onKeyDown={(e) => {
      if (e.key === 'Enter' && !e.shiftKey) handleSubmit();
      if (e.key === 'Escape') handleCancel();
    }}
  />
</div>

Rate Limiting

To prevent spam and maintain discussion quality, Jefftube enforces comment rate limits:
10 comments per user per video per dayOnce you reach this limit, you’ll receive an error message: “You have left too many comments already” with a 429 status code.

How Rate Limiting Works

// Server checks comments in the last 24 hours
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
const recentComments = await db
  .select({ count: sql`count(*)::int` })
  .from(comments)
  .where(
    and(
      eq(comments.videoId, videoId),
      eq(comments.userId, user.id),
      sql`${comments.createdAt} > ${oneDayAgo}`
    )
  );

if (recentComments[0].count >= 10) {
  return { error: "You have left too many comments already" };
}
Rate limits are tracked per IP address using a hashed identifier. The limit resets 24 hours after your first comment.

Character Limit

All comments must be 300 characters or less. This includes both top-level comments and replies.
// Validation on the server
if (content.trim().length > 300) {
  return c.json({ error: "Comment must be 300 characters or less" }, 400);
}
The 300-character limit encourages concise, meaningful feedback while preventing wall-of-text spam. It’s roughly equivalent to 2-3 sentences, perfect for quick reactions and thoughts.

Threaded Replies

Comments support a two-level threading system: top-level comments and direct replies.

Replying to Comments

1

Click Reply

Under any comment, click the Reply button to open a reply input field.
2

Write Your Reply

The input shows a placeholder like “Reply to @username…”. Type your response (also limited to 300 characters).
3

Submit

Click Comment or press Enter. Your reply appears nested under the parent comment.

Viewing Replies

When a comment has replies, you’ll see a blue ”↓ X replies” button:
<button onClick={() => setShowReplies(!showReplies)}>
  <svg className={showReplies ? "rotate-180" : ""}>
    <path d="M7 10l5 5 5-5z" />
  </svg>
  {replyCount} {replyCount === 1 ? "reply" : "replies"}
</button>
Click to expand or collapse the reply thread.
Reply buttons only appear on top-level comments. You cannot reply to a reply (no nested threading beyond one level).

Comment Likes

You can like any comment to show appreciation or agreement.

Liking a Comment

Click the thumbs-up icon next to a comment. The icon fills with color (blue) and the like count increments.
<button onClick={handleLike}>
  <LikeIcon 
    filled={comment.userLike === true} 
    className={comment.userLike === true ? "text-blue-500" : ""}
  />
  <span>{comment.likes}</span>
</button>

Like Count Display

Like counts use animated number transitions for smooth visual feedback:
import NumberFlow from "@number-flow/react";

<NumberFlow 
  value={comment.likes}
  style={{ width: Math.max(1, Math.ceil(comment.likes / 10)) + 'ch' }}
  className="text-xs text-secondary"
/>
Your like status is tracked per user (by IP address) and persists across sessions. You can like multiple comments, but only once per comment.

Comment Metadata

Each comment displays:

Username

Automatically generated username (e.g., @user-abc123)

Avatar

Avatar showing the first letter of the username

Timestamp

Relative time (e.g., “5 minutes ago”, “2 days ago”)

Like Count

Number of likes with animated transitions

Time Formatting

Timestamps are displayed as relative time for better readability:
function formatTimeAgo(dateString: string): string {
  const seconds = Math.floor((now.getTime() - date.getTime()) / 1000);
  
  if (seconds < 60) return "just now";
  if (seconds < 3600) return `${Math.floor(seconds / 60)} minutes ago`;
  if (seconds < 86400) return `${Math.floor(seconds / 3600)} hours ago`;
  if (seconds < 604800) return `${Math.floor(seconds / 86400)} days ago`;
  if (seconds < 2592000) return `${Math.floor(seconds / 604800)} weeks ago`;
  if (seconds < 31536000) return `${Math.floor(seconds / 2592000)} months ago`;
  return `${Math.floor(seconds / 31536000)} years ago`;
}

Comment Sections

Video Page Comments

On regular video pages (/watch/:videoId), the comment section appears below the video info, displaying:
  • Total comment count in the header
  • Comment input for adding new comments
  • List of the 100 most recent top-level comments with their replies
<CommentSection videoId={video.id} />

Shorts Comments

On the Shorts page, comments open in a slide-out modal:
1

Open Comments

Tap the comment icon with the comment count on the right side of the screen.
2

View & Engage

The modal slides in from the right, showing the full comment section.
3

Close Modal

Tap the X button or the backdrop to dismiss.

User Identity

Jefftube uses IP-based user identification:
  • Each unique IP address gets a generated username (e.g., @user-a1b2c3)
  • The username persists across sessions from the same IP
  • Avatar displays the first letter after the @ symbol
// Avatar initial extraction
function getInitial(username: string): string {
  if (username.startsWith("@")) {
    return username[1]?.toUpperCase() || "?";
  }
  return username[0]?.toUpperCase() || "?";
}
This approach allows engagement without requiring account creation while still maintaining some consistency for repeat visitors.

Anti-Spam Protection

The comment system includes multiple layers of spam protection:
  1. reCAPTCHA v3: All comment submissions are validated
  2. Rate Limiting: Maximum 10 comments per user per video per day
  3. Character Limit: 300 character maximum prevents spam walls
  4. IP-based Identity: Prevents easy sockpuppeting
// Comments require reCAPTCHA token
commentsRoutes.post(
  "/videos/:videoId/comments", 
  recaptcha,  // Middleware validates token
  async (c) => {
    // ... create comment
  }
);

Loading States

The comment section shows loading indicators while fetching:
{isLoading && (
  <div className="animate-pulse">
    {[1, 2, 3].map((i) => (
      <div key={i} className="flex gap-3">
        <div className="w-10 h-10 bg-secondary rounded-full" />
        <div className="flex-1">
          <div className="h-4 bg-secondary rounded w-32 mb-2" />
          <div className="h-4 bg-secondary rounded w-full" />
        </div>
      </div>
    ))}
  </div>
)}
After submitting a comment, the UI updates optimistically and then revalidates to ensure consistency.

Build docs developers (and LLMs) love