Двуликий «трекер»

от автора

Продолжение трекер-саги. Краткое содержание предыдущей серии — некоторые пользователей Хабра, в основном из тех кто одновременно участвует в нескольких дис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».

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


ссылка на оригинал статьи https://habr.com/ru/post/699806/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *