shadowban-eu/TwitterShadowBanV2

View on GitHub
src/js/ui.js

Summary

Maintainability
A
2 hrs
Test Coverage
import TechInfo from './ui/TechInfo';
import qfSettingToast from './ui/qfSettingToast';
import Task from './ui/Task';
import I18N from './i18n';

import constructTaskData from './tasks';

export default class UI {
  constructor(test) {
    // create and add task elements
    this.tasks = constructTaskData().sort((a, b) => (a.idx - b.idx)).map(task => new Task(task));
    this.tasksById = this.tasks.reduce(
      (acc, task) => ({ [task.id]: task, ...acc }),
      {}
    );
    // user handle input and title synchronisation
    this.screenName = document.getElementById('screenName');
    this.screenNameLabel = document.querySelector('label[for="screenName"]');
    this.screenNamePrefix = document.querySelector('#controls .input-field .prefix');
    this.headerScreenName = document.getElementById('headerScreenName');
    this.screenName.addEventListener('keyup', this.updateHeaderScreenName, true);

    // button, initiating test
    this.checkButton = document.getElementById('check');
    this.checkButton.addEventListener('click', this.handleCheckClick);

    // PWA install (Add 2 Home Screen)
    this.deferredPrompt = null;
    this.installButton = document.querySelector('.install-trigger');
    window.addEventListener('beforeinstallprompt', (evt) => {
      evt.preventDefault();
      this.deferredPrompt = evt;
      this.installButton.addEventListener('click', this.handleInstallClick);
      this.installButton.parentElement.parentElement.classList.remove('hiddendiv');
    });

    // custom click handler for Materialize Collapsibles
    const handleCollapsibleClick = M.Collapsible.prototype._handleCollapsibleClick;
    M.Collapsible.prototype._handleCollapsibleClick = function _handleCollapsibleClick(evt) {
      evt.stopPropagation();
      // ignore link clicks
      if (evt.target.tagName === 'A') {
        return;
      }

      // ignore where attribute 'collapsible-non-interactive' is set
      const collapsibleNI = cash(evt.target)
        .closest('.collapsible')
        .attr('collapsible-non-interactive');
      const headerNI = cash(evt.target)
        .closest('.collapsible-header')
        .attr('collapsible-non-interactive');

      const isInteractive = collapsibleNI === null && headerNI === null;
      if (isInteractive) {
        handleCollapsibleClick.call(this, evt);
      }
    };
    // Keyboard events disabled entirely
    M.Collapsible.prototype._handleCollapsibleKeydown = () => {};


    // toast warning about qf option in notification settings
    if (!localStorage.getItem('testing-toast')) {
      this.qfSettingToastInstance = qfSettingToast(
        () => this.qfSettingToastDimsmiss(), // onClick (closing via OK button; not swiped)
        () => this.qfSettingToastDimsmiss(true) // onComplete (toast is fully closed)
      );
    }

    // actual test function
    this.test = test;

    // donate modal
    const donateModalElement = document.getElementById('donate-modal');
    this.donateModal = M.Modal.init(donateModalElement);

    donateModalElement.querySelector('[href]').addEventListener('click', () => {
      document.getElementById('donate-thanks').classList.remove('hide');
    });

    // set i18n strings
    I18N.resetElements();

    // all other collapsibles
    M.Collapsible.init(document.getElementById('tasks'));
    M.Collapsible.init(document.getElementById('searchFAQ'));
    M.Collapsible.init(document.getElementById('threadFAQ'));
    M.Collapsible.init(document.getElementById('barrierFAQ'));
    M.Collapsible.init(document.getElementById('qfdFAQ'));
    M.Collapsible.init(document.getElementById('functionality'));

    M.Materialbox.init(document.querySelector('.noban-claim ~ .materialboxed'));
  }

  runTest() {
    this.checkButton.focus(); // remove focus from input field, to close mobile screen kbd
    this.showDonateModal();
    this.reset(this.screenName.value);
    this.setLocationForScreenName();
    this.lock();
    this.test(this.screenName.value)
      .then(this.release)
      .catch(this.release);
  }

  // user handle input, title sync
  updateHeaderScreenName = (evt) => {
    evt.stopPropagation();
    if (evt.which === 13) {
      return this.handleCheckClick(evt);
    }
    const classes = this.screenNamePrefix.classList;
    if (!this.screenName.value) {
      classes.remove('invalid');
      classes.remove('valid');
      I18N.updateWithInterpolation(this.headerScreenName, {
        screenName: I18N.getSingleValue('common:screenNameDefault')
      });
      return false;
    }

    I18N.updateWithInterpolation(this.headerScreenName, {
      screenName: this.screenName.value.replace('@', '').trim()
    });

    if (!this.screenName.validity.patternMismatch) {
      classes.remove('invalid');
      classes.add('valid');
    } else {
      classes.remove('valid');
      classes.add('invalid');
    }
    I18N.updateWithInterpolation(this.headerScreenName, {
      screenName: this.screenName.value
    });
    return false;
  }

  // click handler for test initiator button
  handleCheckClick = (evt) => {
    evt.stopPropagation();
    if (this.checkButton.disabled) {
      return;
    }

    if (this.screenName.validity.valid) {
      this.runTest();
    } else {
      const toolTip = M.Tooltip.init(this.screenName);
      toolTip.isHovered = true;
      toolTip.open();
      window.setTimeout(() => {
        toolTip.close();
        toolTip.destroy();
      }, 5000);
    }
  };

  handleInstallClick = () => {
    this.deferredPrompt.prompt();
    this.deferredPrompt.userChoice.then((choice) => {
      if (choice.outcome === 'accepted') {
        this.installButton.classList.add('hiddendiv');
      } else {
        this.installButton.classList.remove('hiddendiv');
      }
      this.deferredPrompt = null;
    });
  };

  unhandledError = () => {
    const incompleteTasks = Array.from(document.querySelectorAll(
      '[data-task-status="pending"],[data-task-status="running"]'
    ));
    const taskUpdates = incompleteTasks.map(x => ({
      id: x.dataset.taskId,
      status: 'warn',
      msg: 'A server error occured. Failed to test. Please try again later.'
    }));
    this.updateTask(...taskUpdates);
  };

  initFromLocation = (location) => {
    const url = new URL(location);
    let screenName;

    if (url.pathname === '/share' && url.search) {
      const shareText = url.searchParams.get('text');
      const shareMatch = shareText.match(/https:\/\/twitter.com\/([A-Za-z0-9_]{1,15})/);
      if (shareMatch) {
        [, screenName] = shareMatch;
      }
    }

    const pathMatch = url.pathname.match(/^\/(?:@|%40)?([A-Za-z0-9_]{1,15})$/);
    if (pathMatch && !screenName) {
      [, screenName] = pathMatch;
    }

    if (screenName) {
      this.screenName.value = screenName;
      this.screenNameLabel.classList.add('active');
      this.updateHeaderScreenName({
        stopPropagation: () => {},
        which: 20
      });
      window.history.replaceState(...this.screenNameHistoryState);
      this.runTest();
    }
  };

  setLocationForScreenName = () => {
    window.history.replaceState(...this.screenNameHistoryState);
  };

  get screenNameHistoryState() {
    const screenName = this.screenName.value;
    return [{ screenName }, `Testing ${screenName}`, `/${screenName}`];
  }

  updateTask = (...updates) => {
    updates.forEach((update) => {
      if (!this.tasksById[update.id]) {
        console.warn(`Omitting unknown task id on update: ${update.id}`); // eslint-disable-line
        return;
      }
      this.tasksById[update.id].update(update.status, update.msg);
    });
  };

  // resets tasks to initial state (do this before each test!)
  reset = (screenName) => {
    this.updateTask({
      id: 'checkUser',
      status: 'running',
      msg: `Running test for @${screenName}.`
    }, {
      id: 'checkSearch',
      status: 'reset',
      msg: 'Search Ban'
    }, {
      id: 'checkSuggest',
      status: 'reset',
      msg: 'Search Suggestion Ban'
    }, {
      id: 'checkConventional',
      status: 'reset',
      msg: 'Thread Ban'
    }, {
      id: 'checkBarrier',
      status: 'reset',
      msg: 'Reply Deboosting'
    });
    TechInfo.reset();
  }

  // Prevents running multiple tests at the same time (disables button/{Enter} on handle <input>);
  // otherwise the updateTask() calls would mess up the results
  lock = () => { this.checkButton.disabled = true; };

  // Enable button/{Enter} event
  release = () => { this.checkButton.disabled = false; };

  qfSettingToastDimsmiss(swiped) {
    localStorage.setItem('testing-toast', true);
    if (!swiped) {
      this.qfSettingToastInstance.dismiss();
    }
  }

  showDonateModal() {
    const tested = parseInt(localStorage.getItem('tested') || '0', 10) + 1;
    localStorage.setItem('tested', tested);

    const seen = localStorage.getItem('donate-cta');
    if (!seen && tested === 7) {
      localStorage.setItem('donate-cta', true);
      this.donateModal.open();
    }
  }
}