vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Refresh the repository tab counts while the page is open
a2ec95616f4216c743cb1ae358ef8e9449ad8d1f · Keenan Nemetz · 9d ago · parent 0789f22e
Refresh the repository tab counts while the page is open
The header's open counts were only as fresh as the page load, so a reader
watching a repository had to reload to find out that an issue or a change had
been opened.
The tabs now poll /r/{name}/counts every 30s while the page is visible. The
response is the two badges as out-of-band updates, so the current tab, the
clone menu and anything the reader has typed stay where they are. A badge that
moves keeps one short ring, because a number changing elsewhere on the screen
is easy to miss. The endpoint answers 404 for a repository the viewer cannot
read, like every other page under /r/{name}.
server/web/static/app.js +27
| @@ -747,3 +747,30 @@ const copyText = (value, done) => { | |||
| 747 | 747 | select(rows().length ? 0 : -1); | |
| 748 | 748 | }); | |
| 749 | 749 | })(); | |
| 750 | |||
| 751 | // The repository tabs poll their open counts, and a number changing while the | ||
| 752 | // reader is looking somewhere else is easy to miss. A badge that moved keeps one | ||
| 753 | // short ring; the page's first counts are the baseline, so only a later poll that | ||
| 754 | // changes a number marks the tab. An out-of-band response replaces one badge at a | ||
| 755 | // time, so every swap reads all of them: the badge that has not been replaced yet | ||
| 756 | // is still the value already recorded and is left alone. | ||
| 757 | (() => { | ||
| 758 | const seen = new Map(); | ||
| 759 | const read = () => { | ||
| 760 | document.querySelectorAll('.tabs .chip-count[id]').forEach((badge) => { | ||
| 761 | const value = badge.textContent.trim(); | ||
| 762 | if (seen.get(badge.id) === value) return; | ||
| 763 | const first = !seen.has(badge.id); | ||
| 764 | seen.set(badge.id, value); | ||
| 765 | if (first) return; | ||
| 766 | // The class is removed and the layout read in between so a second change | ||
| 767 | // restarts the animation instead of finding the class already applied. | ||
| 768 | badge.classList.remove('count-changed'); | ||
| 769 | void badge.offsetWidth; | ||
| 770 | badge.classList.add('count-changed'); | ||
| 771 | badge.addEventListener('animationend', () => badge.classList.remove('count-changed'), { once: true }); | ||
| 772 | }); | ||
| 773 | }; | ||
| 774 | read(); | ||
| 775 | document.body.addEventListener('htmx:oobAfterSwap', read); | ||
| 776 | })(); | ||