Mirror of @tangled.org/core. Running on a Raspberry Pi Zero 2 (Please be gentle).
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

appview/pages: PR mark-as-reviewed btn

Lewis: May this revision serve well! <lewis@tangled.org>

authored by

Lewis and committed by tangled.org 73cb06db 72aae8e3

+149 -7
+145 -1
appview/pages/templates/repo/fragments/diff.html
··· 14 14 {{ template "fragments/resizable" }} 15 15 {{ template "activeFileHighlight" }} 16 16 {{ template "fragments/line-quote-button" }} 17 + {{ template "reviewState" }} 17 18 </div> 18 19 {{ end }} 19 20 ··· 38 37 {{ $stat := $diff.Stats }} 39 38 {{ $count := len $diff.ChangedFiles }} 40 39 {{ template "repo/fragments/diffStatPill" $stat }} 41 - <span class="text-xs text-gray-600 dark:text-gray-400 hidden md:inline-flex">{{ $count }} changed file{{ if ne $count 1 }}s{{ end }}</span> 40 + <span id="changed-files-label" class="text-xs text-gray-600 dark:text-gray-400 hidden md:inline-flex" data-total="{{ $count }}">{{ $count }} changed file{{ if ne $count 1 }}s{{ end }}</span> 42 41 43 42 {{ if $root }} 44 43 {{ if $root.IsInterdiff }} ··· 179 178 {{ end }} 180 179 </div> 181 180 </div> 181 + <label 182 + data-review-btn="file-{{ .Id }}" 183 + onclick="event.stopPropagation()" 184 + class="review-btn hidden p-2 items-center gap-1 text-xs text-gray-400 dark:text-gray-500 hover:text-green-600 dark:hover:text-green-400 transition-colors cursor-pointer" 185 + title="Mark as reviewed" 186 + > 187 + <input 188 + type="checkbox" 189 + class="sr-only peer review-checkbox" 190 + data-file-id="file-{{ .Id }}" 191 + /> 192 + <span class="peer-checked:hidden">{{ i "circle" "size-4" }}</span> 193 + <span class="hidden peer-checked:inline text-green-600 dark:text-green-400">{{ i "circle-check" "size-4" }}</span> 194 + <span class="hidden md:inline">reviewed</span> 195 + </label> 182 196 </div> 183 197 </summary> 184 198 ··· 318 302 window.__activeFileScrollHandler = updateActiveFile; 319 303 document.addEventListener('scroll', updateActiveFile); 320 304 updateActiveFile(); 305 + })(); 306 + </script> 307 + {{ end }} 308 + 309 + {{ define "reviewState" }} 310 + <script> 311 + (() => { 312 + const linkBase = document.getElementById('round-link-base'); 313 + if (!linkBase) return; 314 + 315 + const isInterdiff = !!document.getElementById('is-interdiff'); 316 + const basePath = linkBase.value.replace(/^\//, ''); 317 + const storageKey = 'reviewed:' + basePath + (isInterdiff ? '/interdiff' : ''); 318 + 319 + const REVIEWED_PREFIX = 'reviewed:'; 320 + const MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000; 321 + 322 + const load = () => { 323 + try { 324 + const entry = JSON.parse(localStorage.getItem(storageKey) || '{}'); 325 + return new Set(Array.isArray(entry) ? entry : (entry.files || [])); 326 + } 327 + catch { return new Set(); } 328 + }; 329 + 330 + const save = (reviewed) => { 331 + const liveIds = new Set(Array.from(allFiles()).map(d => d.id)); 332 + localStorage.setItem(storageKey, JSON.stringify({ 333 + files: Array.from(reviewed).filter(id => liveIds.has(id)), 334 + ts: Date.now(), 335 + })); 336 + }; 337 + 338 + const pruneStale = () => { 339 + const now = Date.now(); 340 + Object.keys(localStorage) 341 + .filter(k => k.startsWith(REVIEWED_PREFIX) && k !== storageKey) 342 + .forEach(k => { 343 + try { 344 + const entry = JSON.parse(localStorage.getItem(k)); 345 + if (!entry.ts || now - entry.ts > MAX_AGE_MS) localStorage.removeItem(k); 346 + } catch { localStorage.removeItem(k); } 347 + }); 348 + }; 349 + if (Math.random() < 0.1) pruneStale(); 350 + 351 + const allFiles = () => 352 + document.querySelectorAll('details[id^="file-"]'); 353 + 354 + const applyOne = (fileId, isReviewed) => { 355 + const detail = document.getElementById(fileId); 356 + if (!detail) return; 357 + 358 + const btn = detail.querySelector('[data-review-btn]'); 359 + const checkbox = btn?.querySelector('input[type="checkbox"]'); 360 + const path = CSS.escape(fileId.replace('file-', '')); 361 + const treeLink = document.querySelector(`.filetree-link[data-path="${path}"]`); 362 + 363 + detail.classList.toggle('opacity-60', isReviewed); 364 + 365 + if (checkbox) checkbox.checked = isReviewed; 366 + 367 + if (treeLink) { 368 + const existing = treeLink.parentElement.querySelector('.review-indicator'); 369 + if (isReviewed && !existing) { 370 + const indicator = document.createElement('span'); 371 + indicator.className = 'review-indicator text-green-600 dark:text-green-400 flex-shrink-0'; 372 + indicator.innerHTML = '&#10003;'; 373 + treeLink.parentElement.appendChild(indicator); 374 + } else if (!isReviewed && existing) { 375 + existing.remove(); 376 + } 377 + } 378 + }; 379 + 380 + const updateProgress = (reviewed) => { 381 + const el = document.getElementById('changed-files-label'); 382 + if (!el) return; 383 + const total = parseInt(el.dataset.total, 10); 384 + const files = allFiles(); 385 + const count = Array.from(files).filter(d => reviewed.has(d.id)).length; 386 + const suffix = total === 1 ? 'file' : 'files'; 387 + const allDone = count === total; 388 + el.classList.toggle('text-green-600', allDone); 389 + el.classList.toggle('dark:text-green-400', allDone); 390 + el.classList.toggle('text-gray-600', !allDone); 391 + el.classList.toggle('dark:text-gray-400', !allDone); 392 + el.textContent = count > 0 393 + ? `${count}/${total} ${suffix} reviewed` 394 + : `${total} changed ${suffix}`; 395 + }; 396 + 397 + const reviewed = load(); 398 + 399 + const toggleReview = (fileId) => { 400 + const detail = document.getElementById(fileId); 401 + if (!detail) return; 402 + const isNowReviewed = !reviewed.has(fileId); 403 + if (isNowReviewed) { 404 + reviewed.add(fileId); 405 + detail.open = false; 406 + } else { 407 + reviewed.delete(fileId); 408 + } 409 + save(reviewed); 410 + applyOne(fileId, isNowReviewed); 411 + updateProgress(reviewed); 412 + }; 413 + 414 + document.getElementById('diff-area').addEventListener('change', (e) => { 415 + const checkbox = e.target.closest('.review-checkbox'); 416 + if (!checkbox) return; 417 + const fileId = checkbox.dataset.fileId; 418 + if (fileId) toggleReview(fileId); 419 + }); 420 + 421 + document.querySelectorAll('.review-btn').forEach(btn => { 422 + btn.classList.remove('hidden'); 423 + btn.classList.add('flex'); 424 + }); 425 + 426 + allFiles().forEach(detail => { 427 + if (reviewed.has(detail.id)) { 428 + applyOne(detail.id, true); 429 + detail.open = false; 430 + } 431 + }); 432 + updateProgress(reviewed); 321 433 })(); 322 434 </script> 323 435 {{ end }}
+3
appview/pages/templates/repo/pulls/pull.html
··· 100 100 101 101 {{ define "contentAfter" }} 102 102 <input type="hidden" id="round-link-base" value="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ .ActiveRound }}" /> 103 + {{ if .IsInterdiff }} 104 + <input type="hidden" id="is-interdiff" value="1" /> 105 + {{ end }} 103 106 {{ template "repo/fragments/diff" (list .Diff .DiffOpts $) }} 104 107 {{ end }} 105 108
+1 -6
types/diff.go
··· 84 84 func (d NiceDiff) FileTree() *filetree.FileTreeNode { 85 85 fs := make([]string, len(d.Diff)) 86 86 for i, s := range d.Diff { 87 - n := s.Names() 88 - if n.New == "" { 89 - fs[i] = n.Old 90 - } else { 91 - fs[i] = n.New 92 - } 87 + fs[i] = s.Id() 93 88 } 94 89 return filetree.FileTree(fs) 95 90 }