Telegram Web Sort Panel + Chart (Зроблено в Україні)

Панель сортировки, цветной график топ-100 сообщений. Поддержка /a/ и /k/, drag&drop, фильтры, экспорт CSV. 🇺🇦 Зроблено в Україні.

// ==UserScript==
// @name         Telegram Web Sort Panel + Chart (Зроблено в Україні)
// @namespace    https://example.com
// @version      3.2
// @description  Панель сортировки, цветной график топ-100 сообщений. Поддержка /a/ и /k/, drag&drop, фильтры, экспорт CSV. 🇺🇦 Зроблено в Україні.
// @match        https://web.telegram.org/a/*
// @match        https://web.telegram.org/k/*
// @grant        GM_addStyle
// @run-at       document-end
// ==/UserScript==

(function() {
  'use strict';

  // === Chart.js ===
  const chartScript = document.createElement('script');
  chartScript.src = 'https://cdn.jsdelivr.net/npm/chart.js';
  document.head.appendChild(chartScript);

  GM_addStyle(`
    #ua-panel {
      position: fixed;
      top: 100px; right: 20px;
      width: 280px; 
      background: var(--tg-theme-bg-color, #fff);
      color: var(--tg-theme-text-color, #000);
      border-radius: 10px;
      box-shadow: 0 2px 10px rgba(0,0,0,0.3);
      padding: 8px;
      z-index: 99999;
      font-family: Arial,sans-serif;
    }
    #ua-panel-header {
      display:flex;align-items:center;justify-content:space-between;
      font-weight:bold;
      cursor:move;
    }
    #ua-buttons button {
      margin:2px;padding:3px 5px;border:none;border-radius:4px;
      background:#0d6efd;color:#fff;cursor:pointer;font-size:12px;
    }
    #ua-buttons button:hover {opacity:0.8;}
    #ua-chart {width:100%;height:200px;}
    #ua-hide {
      position:absolute;top:2px;right:2px;cursor:pointer;font-weight:bold;
    }
  `);

  const panel = document.createElement('div');
  panel.id = 'ua-panel';
  panel.innerHTML = `
    <div id="ua-panel-header">
      <span>🇺🇦 Зроблено в Україні</span>
      <span id="ua-hide">✖</span>
    </div>
    <div id="ua-buttons">
      <button data-sort="reactions">По реакциям</button>
      <button data-sort="date_desc">Дата ↓</button>
      <button data-sort="date_asc">Дата ↑</button>
      <button data-sort="media">По медиа</button>
      <button data-sort="length">По длине</button>
      <button data-action="24h">За 24ч</button>
      <button data-action="refresh">Обновить</button>
      <button data-action="csv">Экспорт CSV</button>
    </div>
    <canvas id="ua-chart"></canvas>
  `;
  document.body.appendChild(panel);

  // === drag & drop + сохранение позиции ===
  let offsetX, offsetY, isDragging=false;
  const savedPos = JSON.parse(localStorage.getItem('uaPanelPos')||'{}');
  if(savedPos.left && savedPos.top){
    panel.style.left = savedPos.left+'px';
    panel.style.top = savedPos.top+'px';
    panel.style.right = 'unset';
  }
  panel.querySelector('#ua-panel-header').addEventListener('mousedown',e=>{
    isDragging=true;offsetX=e.clientX-panel.offsetLeft;offsetY=e.clientY-panel.offsetTop;
  });
  document.addEventListener('mouseup',()=>isDragging=false);
  document.addEventListener('mousemove',e=>{
    if(isDragging){
      panel.style.left=(e.clientX-offsetX)+'px';
      panel.style.top=(e.clientY-offsetY)+'px';
      panel.style.right='unset';
    }
  });
  window.addEventListener('beforeunload',()=>{
    localStorage.setItem('uaPanelPos',JSON.stringify({left:panel.offsetLeft,top:panel.offsetTop}));
  });

  // === скрыть ===
  panel.querySelector('#ua-hide').addEventListener('click',()=>{
    panel.style.display='none';
    localStorage.setItem('uaPanelHidden','true');
  });
  if(localStorage.getItem('uaPanelHidden')==='true') panel.style.display='none';

  // === Chart.js загрузился ===
  chartScript.onload = ()=>init();

  function init(){
    const ctx=document.getElementById('ua-chart').getContext('2d');
    let chart=new Chart(ctx,{type:'bar',data:{labels:[],datasets:[{label:'ТОП-10',data:[],backgroundColor:[]}]}});
    
    function randomColor(){
      return `hsl(${Math.floor(Math.random()*360)},70%,50%)`;
    }

    function collectMessages(){
      const msgs=document.querySelectorAll('[class*="message"]');
      let arr=[];
      msgs.forEach(m=>{
        const text=m.innerText||'';
        const date=m.querySelector('time')?.getAttribute('datetime')||'';
        const reactions=m.querySelectorAll('[class*="reaction"]').length;
        const media=m.querySelectorAll('img,video').length;
        arr.push({el:m,text,date,reactions,media});
      });
      return arr;
    }

    function updateChart(data){
      chart.data.labels=data.slice(0,10).map((_,i)=>i+1);
      chart.data.datasets[0].data=data.slice(0,10).map(d=>d.value);
      chart.data.datasets[0].backgroundColor=data.slice(0,10).map(()=>randomColor());
      chart.update();
    }

    function sortData(type){
      let msgs=collectMessages();
      let now=Date.now();
      if(type==='24h') msgs=msgs.filter(m=>now-(new Date(m.date).getTime())<86400000);
      let data=[];
      switch(type){
        case 'reactions':
          data=msgs.map(m=>({value:m.reactions,text:m.text})).sort((a,b)=>b.value-a.value);
          break;
        case 'date_desc':
          data=msgs.map(m=>({value:new Date(m.date).getTime(),text:m.text})).sort((a,b)=>b.value-a.value);
          break;
        case 'date_asc':
          data=msgs.map(m=>({value:new Date(m.date).getTime(),text:m.text})).sort((a,b)=>a.value-b.value);
          break;
        case 'media':
          data=msgs.map(m=>({value:m.media,text:m.text})).sort((a,b)=>b.value-a.value);
          break;
        case 'length':
          data=msgs.map(m=>({value:m.text.length,text:m.text})).sort((a,b)=>b.value-a.value);
          break;
        default:
          data=msgs.map(m=>({value:m.text.length,text:m.text}));
      }
      updateChart(data);
    }

    document.querySelectorAll('#ua-buttons button').forEach(btn=>{
      btn.addEventListener('click',()=>{
        const sort=btn.dataset.sort;
        const action=btn.dataset.action;
        if(sort) sortData(sort);
        if(action==='24h') sortData('24h');
        if(action==='refresh') sortData('reactions');
        if(action==='csv'){
          const msgs=collectMessages();
          let csv='Текст;Реакции;Медиа;Дата\n';
          msgs.forEach(m=>csv+=`"${m.text.replace(/"/g,'""')}";${m.reactions};${m.media};${m.date}\n`);
          const blob=new Blob([csv],{type:'text/csv'});
          const a=document.createElement('a');
          a.href=URL.createObjectURL(blob);
          a.download='telegram_export.csv';
          a.click();
        }
      });
    });

    const chatContainer=document.querySelector('#column-center')||document.body;
    if(chatContainer){
      const observer=new MutationObserver(()=>sortData('reactions'));
      observer.observe(chatContainer,{childList:true,subtree:true});
    }

    setTimeout(()=>sortData('reactions'),3000);
  }
})();