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:
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) ---- --}}
|
||||||
|
|||||||
Reference in new issue
Block a user