Продолжение трекер-саги. Краткое содержание предыдущей серии - некоторые пользователей Хабра, в основном из тех кто одновременно участвует в нескольких дисcкусиях, активно используют функциональность трекера.
С переходом на новую версию дизайна, концепция трекера кардинально поменяла предоставляемый набор данных и функциональность в сравнении со старой версией. Не будем углубляться в причины этого, на данном этапе это есть объективная реальность, данная нам в ощущения.
В связи с ожиданиями окончания поддержки старой версии и её декомиссии, время от времени те пользователи которые привыкли к работе в парадигме старой версии подают прошения богам Хабра , но пока безуспешно. Похоже что прошений недостаточно, а дары пока не пробовали (ну или не тем богам молились).
В комментах к подобной статье-прошении от @vconst, по поводу судьбы трекера, я предположил что в крайнем случае можно взять это дело в свои руки и если гора не идёт к Магомеду, попробовать самим эту гору сравнять. Тем более, что каждый первый из читателей Хабра в той или иной мере владеет инструментарием позволяющим этого добиться.
В качестве пробы пера, я написал скрипт для старой версии сайта, при внедрении которого на страницу, позволяющий добавить секцию трекера в правый сайдбар.
На удивление это случилось сильно проще чем ожидалось и лично для меня это оказалась довольно удобно.
Но тот скрипт не решал основную проблему - мигрировать функциональность старого трекера в новую версию.
Для достижения этого существует вполне прямолинейный способ - читаем данные со страницы нового трекера, и потом для тех статей которые имеют новые комментарии, читаем страницу статьи для получения недостающих данных. В данном сценарии существует одно неудобство - придётся считывать множество страниц сайта (потенциально десятки) для формирования требуемого набора данных.
Поэтому переключившись в инферфейс новой версии, я углубился в изучение функциональности нового дизайна.
К сожалению чуда не произошло и в тёмных закоулках html новой версии не удалось обнаружить недостающих залежей данных. Я было приготовился к реализации воркера который время от времени оббегает страницы и обновляет статистики, но случайно обнаружил страницу, которая по сути является тем же трекером, но под другим именем - "закладки".
И самое интересное что требуемые данные уже отображаются на странице закладок.
В самом простом случае, пользователи новой версии могут незначительно изменить свои привычки и приспособиться использовать галочку "добавить в закладки", вместо "добавить в трекер". Ну может быть добавить CSS правило для сокрытия превью для статей. И получится вполне себе рабочее решение.
Но на всякий случай, я всё же подшаманил скрипт для старой версии и приспособил его для использования закладок в качестве трекера на правом сайдбаре. (Скрипт можно найти в конце этой статьи)
Оказалось что результат практически не отличается от такового для старой версии. И количество страниц которые необходимо прочитать, всего лишь одна или две, ну может три, вместо десятка.
![](https://habrastorage.org/getpro/habr/upload_files/7a7/222/a67/7a7222a671d057b5419479c4a592f2d0.png)
Конечно можно чуток и его улучшить - добавить автодобавление в закладки при добавление в трекер. Или реализовать автоудаление из закладок при отсутствии активностей в комментариях в статье за прошедшую неделю и т.д. и т.п. Всё это легко реализуется, было бы желание.
Но как же быть с закладками спросит пытливый читатель? И конечно будет прав. Есть два способа решения этой дилеммы.
Первый это мирное сосуществование.
Как мне кажется, статьи заслуживающие быть добавленными в закладки встречаются довольно редко, поэтому это не должно стать проблемой. Тем более трекать комменты в статье которую вы реально поместили в закладки ради закладки тоже вполне полезно.
Второй способ, это использование для закладок, один из множества уже существующих универсальных сервисов или плагинов для организации закладок. Они специализируются на этом и имеют множество дополнительных полезных фич, типа поиск внутри статей из закладок, трекинг обновлений, добавлений кастомных тэгов и комментирование контента страницы.
И особенно обратить внимание на те из них, которые на момент добавления в закладки создаёт скриншот или PDF страницы.
Это кстати решит давнюю проблему закладок Хабра - пропажу страниц при их удалении автором. Это конечно не частое явление и не конец света, в конце концов можно на одном из зеркал их отыскать, но всё же. Так что я лично склоняюсь ко второму варианту.
Кстати, можете поделится своими любимыми сервисами в комментах, может кому пригодится. Лично я пользуюсь букмарками самого браузера, организовав древовидную структуру папок.
Кстати, нет худа без добра - пока смотрел на структуру сайта для реализации трекера, заодно решил проблему которая время от времени меня подбешивала - случайное обновление страницы и пропажа выделения новых комментов.
Если интересно, опубликую и это решение, но скрипт надо бы чуток причесать. Если же нету интереса к нему - мне ок и нечёсанным пользоваться, что время зазря терять.
На этом я с данной сагой завязываю, считаю что это вполне рабочее решение, хоть и не идеальное, но тратить время на дальнейшие эксерсайзы нету большого желания.
Если у вас есть идеи того что ещё можно сделать с функциональностью Хабра, чтобы ширнармассам стало удобнее - отпишитесь в комментах, может быть удасться и их реализовать.
Ну и конечно в отличии от расширений браузера, выделеный сервер с краулером комментов и статей с лёгкостью позволит реализовать 99% хотелок, с гораздо лучшей производительностью. Увы, но мне его поддерживать не с руки, если вы в себе чувствуете потенциал для этого, дерзайте, я готов по мере сил подсобить.
У меня есть задумки:
1. Комментирование контента статьи, т.е. возможность добавления замечаний или просто выделение цветом частей текста, с возможностью делиться этим с другими участниками.
2. Сохранение историй изменений, т.е. чтобы было можно увидеть различия между статьёй и комментами различных версий.
3. Трекинг комментариев под моим комментом или всей веткой в которой я что либо писал.
4. Трекинг активностей и ведение профайлов некоторых пользователей (добавлять свои комменты для них, назначать свои иконки чтобы сразу было понятно, выделять их комменты цветом, скрывать их)
5. Прокси для внедрения кастомных скриптов/css для мобильной версии сайта (многие мобильные браузеры не поддерживают расширения в мобильных версиях, а хочется)
6. Создание тематических подборок или роадмапов из статей и возможности ими управлять группой товарищей.
7. Функцию "совет аксакала" - возможность пользователям советовать статьи и подписываться на их ленту(ы), чтобы не пропустить интересный материал (не только с хабра)
8. Организовать ленты - "лучшее в этот день год назад"/"три/пять лет назад"/"забытые бриллианты" - чтобы постоянно шла лента ретроспективы старых статей.
9. Бот(ы) для мессенджеров
10. Отдавать полный архив, а также дневные, недельные и месячные дельты всем желающим. Пущай играются с данными, может интересное получится.
11. Отдавать фиды статей и комментов через API , тоже самое - пусть желающие творят добро.
12 Трекинг моих плюсов и минусов для статей и комментов, иногда было бы интересно глянуть на свои былые подпрыгивания.
Я не думаю что имеет смысл создавать ещё одно зеркало хабра, в дополнение к десятку уже существующих, но сервер который поможет данными приложениям и расширениям, конечно будет полезным дополнением к самому сайту.
Код скрипта для новой версии
(function () {
function injectTracker() {
const MAX_TRACKS = 30;
const MAX_PAGES = 3;
// To add the tracker as a first block on the right sidebar, change "false" to "true"
const insertAsFirstBlock = false;
const userName = getUserName();
if(!userName)
return;
// styling
const trackerBlockCss = `<style scoped>
@keyframes rotate { 0% { transform: rotate(0); } 100% { transform: rotate(360deg); } }
svg.tm-article-comments-counter-link__icon { display: none; }
.post-info__title a { color: #444; }
a[href*="#comments"] { color: #82a3b1; }
a[href*="#first_unread"] { color: #cf0000; }
.tracker-stats-info { display: flex; }
a.tm-block__title { text-decoration: none; }
.tm-user-info__username, span.tracker * { font-weight: normal; }
span.tracker { position: absolute; right: 0; display: flex; margin-left: auto; padding-right: 1em; }
span.tracker-hide-or-remove { right: 1em; display: none; position: absolute; background: white; }
li:hover .tm-article-title span.tracker-hide-or-remove { display: inline-block; color: #888; }
.tm-article-title:hover span.remove-tracker:hover { color: #444; }
.tracker-refresh-icon, .delete-icon, .hide-icon { cursor: pointer; }
.icon-anim { animation-name: rotate; animation-duration: 1s; animation-iteration-count: infinite; animation-timing-function: linear; animation-play-state: running; }
</style>`;
const deleteSvg = '<svg class="delete-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" style="vertical-align: middle;" height="16" width="16"><path d="m16.5 33.6 7.5-7.5 7.5 7.5 2.1-2.1-7.5-7.5 7.5-7.5-2.1-2.1-7.5 7.5-7.5-7.5-2.1 2.1 7.5 7.5-7.5 7.5ZM24 44q-4.1 0-7.75-1.575-3.65-1.575-6.375-4.3-2.725-2.725-4.3-6.375Q4 28.1 4 24q0-4.15 1.575-7.8 1.575-3.65 4.3-6.35 2.725-2.7 6.375-4.275Q19.9 4 24 4q4.15 0 7.8 1.575 3.65 1.575 6.35 4.275 2.7 2.7 4.275 6.35Q44 19.85 44 24q0 4.1-1.575 7.75-1.575 3.65-4.275 6.375t-6.35 4.3Q28.15 44 24 44Zm0-3q7.1 0 12.05-4.975Q41 31.05 41 24q0-7.1-4.95-12.05Q31.1 7 24 7q-7.05 0-12.025 4.95Q7 16.9 7 24q0 7.05 4.975 12.025Q16.95 41 24 41Zm0-17Z"/></svg>';
const refeshSvg = '<svg class="tracker-refresh-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" style="vertical-align: middle;" height="20" width="20" ><path d="M24 40q-6.65 0-11.325-4.675Q8 30.65 8 24q0-6.65 4.675-11.325Q17.35 8 24 8q4.25 0 7.45 1.725T37 14.45V8h3v12.7H27.3v-3h8.4q-1.9-3-4.85-4.85Q27.9 11 24 11q-5.45 0-9.225 3.775Q11 18.55 11 24q0 5.45 3.775 9.225Q18.55 37 24 37q4.15 0 7.6-2.375 3.45-2.375 4.8-6.275h3.1q-1.45 5.25-5.75 8.45Q29.45 40 24 40Z"/></svg>';
const hideSvg = '<svg class="hide-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" style="vertical-align: middle;" height="16" width="16"><path d="m31.45 27.05-2.2-2.2q1.3-3.55-1.35-5.9-2.65-2.35-5.75-1.2l-2.2-2.2q.85-.55 1.9-.8 1.05-.25 2.15-.25 3.55 0 6.025 2.475Q32.5 19.45 32.5 23q0 1.1-.275 2.175-.275 1.075-.775 1.875Zm6.45 6.45-2-2q2.45-1.8 4.275-4.025Q42 25.25 42.85 23q-2.5-5.55-7.5-8.775Q30.35 11 24.5 11q-2.1 0-4.3.4-2.2.4-3.45.95L14.45 10q1.75-.8 4.475-1.4Q21.65 8 24.25 8q7.15 0 13.075 4.075Q43.25 16.15 46 23q-1.3 3.2-3.35 5.85-2.05 2.65-4.75 4.65Zm2.9 11.3-8.4-8.25q-1.75.7-3.95 1.075T24 38q-7.3 0-13.25-4.075T2 23q1-2.6 2.775-5.075T9.1 13.2L2.8 6.9l2.1-2.15L42.75 42.6ZM11.15 15.3q-1.85 1.35-3.575 3.55Q5.85 21.05 5.1 23q2.55 5.55 7.675 8.775Q17.9 35 24.4 35q1.65 0 3.25-.2t2.4-.6l-3.2-3.2q-.55.25-1.35.375T24 31.5q-3.5 0-6-2.45T15.5 23q0-.75.125-1.5T16 20.15Zm15.25 7.1Zm-5.8 2.9Z"/></svg>';
const oldTrackerBlock = animateRefreshIcon();
let hiddenItems = localStorage.getItem('hbr_tracker');
hiddenItems = hiddenItems ? JSON.parse(hiddenItems) : [];
let currentTrack = 0;
fetch(`https://habr.com/ru/users/${userName}/favorites/posts/`)
.then(response => response.text())
.then(response => processTracker(response));
function processTracker(response) {
const d = document.querySelector('.tm-sexy-sidebar');
if (!d)
return null;
oldTrackerBlock?.parentNode.removeChild(oldTrackerBlock);
const articles = parseArticles(response);
const newDiv = addNewElement(d, 'section', 'tm-block tm-block tm-block_spacing-top tm-block_spacing-bottom block-tracker', trackerBlockCss, insertAsFirstBlock);
const headerDiv = addNewElement(newDiv, 'header', 'tm-block__header tm-block__header', '<div class="tm-block__header-container"><h2 class="tm-block__title"><a class="tm-block__title" href="https://habr.com/ru/tracker/" target="_blank">Tracker</a> <span class="tracker-refresh-btn" >' + refeshSvg + '</h2></div> ');
headerDiv.querySelector('.tracker-refresh-btn').addEventListener('click', injectTracker);
const newContent = addNewElement(newDiv, 'div', 'tm-block__body tm-block__body');
const newUl = addNewElement(newContent, 'ul', 'tm-article-list-block__list');
if (articles)
addArticles(articles, 1, newUl, newContent);
}
function addArticles(articles, pageId, newUl, newContent) {
articles.filter(el => {
if (currentTrack >= MAX_TRACKS)
return false;
currentTrack += addTrack(el, newUl);
});
if (currentTrack === 0)
addNewElement(newContent, 'p', '', 'No articles with new comments');
else if (currentTrack < MAX_TRACKS && pageId < MAX_PAGES && articles.length == 20)
processNextPage(pageId + 1, newUl, newContent);
}
function parseArticles(response) {
const m = response.match(/<div[^>]+data-test-id="articles-list"[^>]*>(.*?)<\/body>/s);
if (!m)
return null;
const parseDiv = document.createElement("div");
parseDiv.innerHTML = m[0];
return [...parseDiv.querySelectorAll('div.tm-articles-list article')];
}
function processNextPage(pageId, newUl, newContent) {
setTimeout(() => {
fetch(`https://habr.com/ru/users/${userName}/favorites/posts/page${pageId}/`)
.then(response => response.text())
.then(response => {
const articles = parseArticles(response);
if (articles)
addArticles(articles, pageId, newUl, newContent);
});
}, 0);
}
function addTrack(el, newUl) {
const firstTd = el.querySelector('div.tm-article-snippet__meta span.tm-user-info__user');
if (!firstTd)
return 0;
const trackerHtml = el.querySelector('div.tm-article-comments-counter-link')?.innerHTML;
if (!trackerHtml?.includes('+'))
return 0;
const articleId = parseInt(el.getAttribute('id'));
if (hiddenItems.includes(articleId))
return 0;
const newLi = addNewElement(newUl, 'li', 'tm-article-list-block__item');
const newArcticle = addNewElement(newLi, 'article', 'tm-article-snippet-block-sidebar tm-article-snippet-block-sidebar_preview');
const newTopDiv = addNewElement(newArcticle, 'div', 'tracker-stats-info');
addNewElement(newTopDiv, 'span', 'post_author', firstTd?.innerHTML.replaceAll('<a ', '<a target="_blank" '));
addNewElement(newTopDiv, 'span', 'tracker', trackerHtml.replaceAll('<a ', '<a target="_blank" '));
const newPost = addNewElement(newArcticle, 'h2', 'tm-article-title tm-article-title_sidebar', `<span class="tracker-hide-or-remove"><span class="hide-tracker" title="Hide from tracker">${hideSvg}</span> <span class="remove-tracker" title="Remove from tracker">${deleteSvg}</span></span>` + el.querySelector('h2.tm-article-snippet__title')?.innerHTML.replaceAll('<a ', '<a target="_blank" ').replaceAll('"tm-article-snippet__title-link"', '"tm-article-title__link"'));
newPost.querySelector('.hide-tracker').addEventListener('click', hideTracker);
newPost.querySelector('.remove-tracker').addEventListener('click', removeTracker);
return 1;
}
function removeTracker(event) {
if (!confirm('Do you really want to delete it?'))
return;
const postId = getPostId(event.currentTarget);
if (!postId)
return;
event.currentTarget.querySelector('svg')?.classList.add('icon-anim');
fetch(`https://habr.com/kek/v1/articles/${postId}/bookmarks/remove`, {
method: 'POST',
body: {},
headers: { 'Content-Type': 'application/json' }
}).then(() => injectTracker());
}
function hideTracker(event) {
const postId = getPostId(event.currentTarget);
if (!postId)
return;
if (hiddenItems.length > 100)
hiddenItems.length = 99;
hiddenItems.unshift(postId);
localStorage.setItem('hbr_tracker', JSON.stringify(hiddenItems));
injectTracker();
}
function getPostId(el) {
const arctileEl = el.parentNode.parentNode.querySelector('* > a');
if (!arctileEl)
return;
const m = arctileEl.getAttribute('href')?.match(/\/([0-9]+)\//);
if (!m)
return null;
return parseInt(m[1]);
}
function addNewElement(parentElement, tageName, className, html, makeAsFirstChild) {
const newEl = document.createElement(tageName);
newEl.className = className ?? '';
newEl.innerHTML = html ?? '';
if (makeAsFirstChild)
parentElement.insertBefore(newEl, parentElement.firstChild);
else
parentElement.appendChild(newEl);
return newEl;
}
function animateRefreshIcon() {
const oldTrackerBlock = document.querySelector('section.block-tracker');
oldTrackerBlock?.querySelector('.tracker-refresh-icon')?.classList.add('icon-anim');
return oldTrackerBlock;
}
function getUserName()
{
const userHref = document.querySelector('.tm-footer-menu__list-item > a[href*="/ru/users/"]')?.getAttribute("href");
if (!userHref)
return null;
const um = userHref.match(/\/ru\/users\/([^\/]+)\/posts\//)
if (!um)
return null;
return um[1];
}
}
injectTracker();
})();
Если вы используете Greasemonkey или подобное расширение, то можете добавить этот скрипт для сайта Хабра и на каждой странице у вас будет новая секция "tracker".
Я использую кастомизированный HabrSanitizer и если вы также его поклонник и хотели бы добавить функциональность трекера к нему, то надо загрузить исходники к себе на локальный компьютер, добавить его в качестве локального расширения (Load Unpacked) и вставить код скрипта перед строчкой (на сегодня это строка 397)
Можно протестировать функциональность и без установки расширения. На любой странице хабра нужно открыть DevTools и вставить код этого скрипта в Console. Но конечно это единовременное изменение страницы и при навигации со страницы, трекер блок будет удалён и на новой странице необходимо будет повторить процедуру ещё раз.
Если всё сделано правильно то вы увидите под блоком "Читают сейчас" новый блок, "Tracker".
Буду благодарен за замечания и обратную связь.
askv
Статью не осилил. У меня трекер частенько выдаёт некорректное количество уведомлений. Например, все уведомления прочитаны, а он пишет +1. Или +2, а там только одно непрочитанное. Со временем выравнивается.
hbn3 Автор
Я заметил что так бывает если вы написали свой коммент, тогда какое-то время не правильно считается.
askv
Может быть. Я никакой закономерности не заметил.