Продолжение трекер-саги. Краткое содержание предыдущей серии - некоторые пользователей Хабра, в основном из тех кто одновременно участвует в нескольких дисcкусиях, активно используют функциональность трекера.

С переходом на новую версию дизайна, концепция трекера кардинально поменяла предоставляемый набор данных и функциональность в сравнении со старой версией. Не будем углубляться в причины этого, на данном этапе это есть объективная реальность, данная нам в ощущения.

В связи с ожиданиями окончания поддержки старой версии и её декомиссии, время от времени те  пользователи которые привыкли к работе в парадигме старой версии подают прошения богам Хабра , но пока безуспешно. Похоже что прошений недостаточно, а дары пока не пробовали (ну или не тем богам молились).

В комментах к подобной статье-прошении от @vconst, по поводу судьбы трекера, я предположил что в крайнем случае можно взять это дело в свои руки и если гора не идёт к Магомеду, попробовать самим эту гору сравнять. Тем более, что каждый первый из читателей Хабра в той или иной мере владеет инструментарием позволяющим этого добиться.

В качестве пробы пера, я написал скрипт для старой версии сайта, при внедрении которого на страницу, позволяющий добавить секцию трекера в правый сайдбар.

На удивление это случилось сильно проще чем ожидалось и лично для меня это оказалась довольно удобно.

Но тот скрипт не решал основную проблему - мигрировать функциональность старого  трекера в новую версию.

Для достижения этого существует вполне прямолинейный способ - читаем данные со страницы нового трекера, и потом для тех статей которые имеют новые комментарии, читаем страницу статьи для получения недостающих данных. В данном сценарии существует одно неудобство - придётся считывать множество страниц сайта (потенциально десятки) для формирования требуемого набора данных.

Поэтому переключившись в инферфейс новой версии, я углубился в изучение функциональности нового дизайна.

К сожалению чуда не произошло и в тёмных закоулках html новой версии не удалось обнаружить недостающих залежей данных. Я было приготовился к реализации воркера который время от времени оббегает страницы и обновляет статистики, но случайно обнаружил страницу, которая по сути является тем же трекером, но под другим именем - "закладки".

И самое интересное что требуемые данные уже отображаются на странице закладок.

В самом простом случае, пользователи новой версии  могут незначительно изменить свои привычки и приспособиться использовать галочку "добавить в закладки", вместо "добавить в трекер". Ну может быть добавить CSS правило для сокрытия превью для статей. И получится вполне себе рабочее решение.

Но на всякий случай, я всё же подшаманил скрипт для старой версии и приспособил его для использования закладок в качестве трекера на правом сайдбаре. (Скрипт можно найти в конце этой статьи)

Оказалось что результат практически не отличается от такового для старой версии. И количество страниц которые  необходимо прочитать, всего лишь одна или две, ну может три, вместо десятка.

Конечно можно чуток и его улучшить -  добавить автодобавление в закладки при добавление в трекер. Или реализовать автоудаление из закладок при отсутствии активностей в комментариях в статье за прошедшую неделю и т.д. и т.п. Всё это легко реализуется, было бы желание.

Но как же быть с закладками спросит пытливый читатель? И конечно будет прав. Есть два способа решения этой дилеммы.

Первый это мирное сосуществование.

Как мне кажется, статьи заслуживающие быть добавленными в закладки встречаются довольно редко, поэтому это не должно стать проблемой. Тем более трекать комменты в статье которую вы реально поместили в закладки ради закладки тоже вполне полезно.

Второй способ, это использование для закладок, один из множества уже существующих универсальных сервисов или плагинов для организации  закладок. Они специализируются на этом и имеют множество дополнительных полезных фич, типа поиск внутри статей из закладок, трекинг обновлений, добавлений кастомных тэгов и комментирование контента страницы.

И особенно обратить внимание на те из них, которые на момент добавления в закладки создаёт скриншот или 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>&nbsp;<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".

Буду благодарен за замечания и обратную связь.

Комментарии (3)


  1. askv
    17.11.2022 17:23

    Статью не осилил. У меня трекер частенько выдаёт некорректное количество уведомлений. Например, все уведомления прочитаны, а он пишет +1. Или +2, а там только одно непрочитанное. Со временем выравнивается.


    1. hbn3 Автор
      17.11.2022 17:30

      Я заметил что так бывает если вы написали свой коммент, тогда какое-то время не правильно считается.


      1. askv
        17.11.2022 17:37

        Может быть. Я никакой закономерности не заметил.