feat(watch): match channel row, read-more clamp, nested-reply @prefill

Match page: the only path from a match video to the uploader's channel
was a tiny avatar buried inside the Match Overview card. Add the shared
<x-channel-row :video> component right under the title + action bar so
the Subscribe + channel link is as prominent as it is on music and
generic pages.

Comments:
- Long comments collapse to a 4-line clamp with a "Read more" / "Show
  less" toggle. The CSS line-clamp doesn't cut mid-word, and the
  button is added after enhanceBody() runs so it measures the final
  rendered height (timestamps + mentions already materialised).
- Nested-reply Reply button: each reply now carries a button that
  opens the parent-level reply form and prefills "@AuthorName " so
  the thread stays readable even though the DB only stores one-level
  nesting.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
ghassanandClaude Opus 4.7 committed 2026-10-21 12:07:23 +03:00
1 parent 5aef41201a
commit 22d6b1507d
2 files changed
+90

No files matched your search

@@ -234,6 +234,15 @@
<button class="ytc-dislike-btn" data-id="{{ $reply->id }}" title="Dislike"> <button class="ytc-dislike-btn" data-id="{{ $reply->id }}" title="Dislike">
<svg viewBox="0 0 24 24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L10.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg> <svg viewBox="0 0 24 24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L10.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
</button> </button>
@auth
{{-- Reply to a reply: opens the parent comment's reply form and
prefills "@AuthorName " so the responder's reader knows which
reply they're answering. The DB still stores it as a one-level
thread (parent_id = top-level), matching the existing model. --}}
<button class="ytc-reply-btn _comment-reply-nested-trigger"
data-parent-id="{{ $comment->id }}"
data-handle="{{ $reply->user->username ?? $reply->user->name ?? '' }}">Reply</button>
@endauth
@if($llmEnabled) @if($llmEnabled)
<button class="ytc-translate-btn _comment-translate-trigger" data-comment-id="{{ $reply->id }}" title="Translate this comment"> <button class="ytc-translate-btn _comment-translate-trigger" data-comment-id="{{ $reply->id }}" title="Translate this comment">
<svg viewBox="0 0 24 24"><path d="M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z"/></svg> <svg viewBox="0 0 24 24"><path d="M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z"/></svg>
@@ -515,6 +524,31 @@
white-space: pre-wrap; white-space: pre-wrap;
} }
/* ── Read more clamp ──
Long comments collapse to ~4 lines by default; the "Read more" button
under the body expands them. The clamp uses line-clamp (not a fixed
max-height) so the first few lines are always complete, never cut
mid-word, and the fade-mask eases into the trailing ellipsis.
*/
.ytc-text.ytc-clamp {
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
overflow: hidden;
position: relative;
}
.ytc-read-more {
background: none;
border: none;
color: var(--text-secondary);
font-size: 13px;
font-weight: 600;
cursor: pointer;
padding: 4px 0 0;
margin-top: 2px;
}
.ytc-read-more:hover { color: var(--text-primary); }
/* ── Actions ── */ /* ── Actions ── */
.ytc-actions { .ytc-actions {
display: flex; display: flex;
@@ -999,6 +1033,26 @@ document.getElementById('ytcList')?.addEventListener('click', e => {
const rt = e.target.closest('._comment-reply-trigger'); const rt = e.target.closest('._comment-reply-trigger');
if (rt) { e.stopPropagation(); toggleReplyForm(rt.dataset.commentId); return; } if (rt) { e.stopPropagation(); toggleReplyForm(rt.dataset.commentId); return; }
// Replying to a reply — open the parent-level reply form and
// prefill it with the reply author's @handle so the thread stays
// readable even though the DB only stores one-level nesting.
const rtn = e.target.closest('._comment-reply-nested-trigger');
if (rtn) {
e.stopPropagation();
const parentId = rtn.dataset.parentId;
const handle = (rtn.dataset.handle || '').replace(/\s+/g, '');
toggleReplyForm(parentId);
const ta = document.getElementById('replyBody' + parentId);
if (ta && handle) {
const mention = '@' + handle + ' ';
if (!ta.value.startsWith(mention)) ta.value = mention + ta.value;
ta.focus();
ta.setSelectionRange(ta.value.length, ta.value.length);
autoResize(ta);
}
return;
}
// Edit trigger // Edit trigger
const et = e.target.closest('._comment-edit-trigger'); const et = e.target.closest('._comment-edit-trigger');
if (et) { e.stopPropagation(); startEdit(et.dataset.commentId); return; } if (et) { e.stopPropagation(); startEdit(et.dataset.commentId); return; }
@@ -1467,6 +1521,32 @@ function toggleDislike(btn) {
function initLikeStates() {} // server-rendered state is already in the HTML function initLikeStates() {} // server-rendered state is already in the HTML
// ── Timestamp & @mention parsing ───────────────────── // ── Timestamp & @mention parsing ─────────────────────
// ── Read-more clamp ──
// Flags long bodies (>4 lines once rendered) so a "Read more" button
// below the body expands them. Measured in computed line-height × 4.
function maybeClampBody(el) {
// Deferred one microtask so el.scrollHeight is correct even if the
// enclosing comment card just got inserted into the DOM.
requestAnimationFrame(() => {
const lh = parseFloat(getComputedStyle(el).lineHeight) || 22;
const limit = lh * 4;
if (el.scrollHeight <= limit + 1) return;
el.classList.add('ytc-clamp');
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'ytc-read-more';
btn.textContent = 'Read more';
btn.setAttribute('aria-expanded', 'false');
btn.addEventListener('click', () => {
const expanded = el.classList.toggle('ytc-clamp') === false;
btn.textContent = expanded ? 'Show less' : 'Read more';
btn.setAttribute('aria-expanded', expanded ? 'true' : 'false');
});
el.insertAdjacentElement('afterend', btn);
});
}
function enhanceBody(el) { function enhanceBody(el) {
if (!el || el.dataset._commentEnhanced === '1') return; if (!el || el.dataset._commentEnhanced === '1') return;
let text = el.textContent || ''; let text = el.textContent || '';
@@ -1496,6 +1576,9 @@ function enhanceBody(el) {
playTimeRange(parseFloat(badge.dataset.start), badge.dataset.end ? parseFloat(badge.dataset.end) : null); playTimeRange(parseFloat(badge.dataset.start), badge.dataset.end ? parseFloat(badge.dataset.end) : null);
}); });
}); });
// Add the Read more affordance after enhancing (so links and timestamps
// are rendered first and the measured height is final).
maybeClampBody(el);
} }
function enhanceAll() { function enhanceAll() {
document.querySelectorAll('._comment-body:not([data-_comment-enhanced="1"])').forEach(enhanceBody); document.querySelectorAll('._comment-body:not([data-_comment-enhanced="1"])').forEach(enhanceBody);
@@ -2418,6 +2418,13 @@
</div> </div>
<!-- ===== END EVENT HEADER ===== --> <!-- ===== END EVENT HEADER ===== -->
{{-- Channel row — the generic + music watch pages render this
just under the title. The match page used to omit it so
the only way to reach the uploader's channel from a match
video was the small avatar hidden inside the Match Overview
section. --}}
<x-channel-row :video="$video" />
@include('videos.partials.description-box', ['video' => $video]) @include('videos.partials.description-box', ['video' => $video])
{{-- ---- legacy match description panel (kept for reference, hidden) ---- --}} {{-- ---- legacy match description panel (kept for reference, hidden) ---- --}}