tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

tabs.js (9933B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
      4 
      5 /**
      6 * Tabs reducer
      7 */
      8 
      9 import { prefs } from "../utils/prefs";
     10 
     11 export function initialTabState({
     12  urls = [],
     13  prettyPrintedURLs = new Set(),
     14 } = {}) {
     15  return {
     16    // List of source URL's which should be automatically opened in a tab.
     17    // This array will be stored as-is in the persisted async storage.
     18    // The order of URLs in this list is important and will be used to restore
     19    // tabs in the same order.
     20    //
     21    // Array<String>
     22    urls,
     23 
     24    // List of sources which are pretty printed.
     25    //
     26    // Set<String>
     27    // (converted into Array in the asyncStorage)
     28    prettyPrintedURLs,
     29 
     30    // Similar but opposite of prettyPrintedURLs.
     31    // List of sources which pretty printing has been manually disabled
     32    // or are not considered minified when auto-pretty printing is enabled
     33    // Set<String>
     34    prettyPrintedDisabledURLs: new Set(),
     35 
     36    // List of sources for which tabs should be currently displayed.
     37    // This is transient data, specific to the current document and at this precise time.
     38    //
     39    // Array<Source objects>
     40    openedSources: [],
     41  };
     42 }
     43 
     44 function update(state = initialTabState(), action) {
     45  switch (action.type) {
     46    case "ADD_TAB":
     47      return updateTabsWithNewActiveSource(state, [action.source], true);
     48 
     49    case "MOVE_TAB":
     50      return moveTabInList(state, action.url, action.tabIndex);
     51 
     52    case "MOVE_TAB_BY_SOURCE_ID":
     53      return moveTabInListBySourceId(state, action.sourceId, action.tabIndex);
     54 
     55    case "CLOSE_TABS_FOR_SOURCES":
     56      return closeTabsForSources(state, action.sources, true);
     57 
     58    case "ADD_ORIGINAL_SOURCES": {
     59      return updateTabsWithNewActiveSource(
     60        state,
     61        action.originalSources,
     62        false
     63      );
     64    }
     65 
     66    case "INSERT_SOURCE_ACTORS": {
     67      const sources = action.sourceActors.map(
     68        sourceActor => sourceActor.sourceObject
     69      );
     70      return updateTabsWithNewActiveSource(state, sources, false);
     71    }
     72 
     73    case "REMOVE_SOURCES": {
     74      return closeTabsForSources(state, action.sources, false);
     75    }
     76 
     77    case "REMOVE_PRETTY_PRINTED_SOURCE": {
     78      return removePrettyPrintedSource(state, action.source);
     79    }
     80 
     81    default:
     82      return state;
     83  }
     84 }
     85 
     86 /**
     87 * Allow unregistering pretty printed source earlier than source unregistering.
     88 */
     89 function removePrettyPrintedSource(state, source) {
     90  const generatedSourceURL = source.isPrettyPrinted
     91    ? source.generatedSource.url
     92    : source.url;
     93  const prettyPrintedURLs = new Set(state.prettyPrintedURLs);
     94  prettyPrintedURLs.delete(generatedSourceURL);
     95 
     96  let prettyPrintedDisabledURLs = state.prettyPrintedDisabledURLs;
     97  // When auto-pretty printing is enabled, record sources which have been manually disabled
     98  if (prefs.autoPrettyPrint) {
     99    prettyPrintedDisabledURLs = new Set(prettyPrintedDisabledURLs);
    100    prettyPrintedDisabledURLs.add(generatedSourceURL);
    101  }
    102 
    103  return { ...state, prettyPrintedURLs, prettyPrintedDisabledURLs };
    104 }
    105 
    106 /**
    107 * Update the tab list for a given set of sources.
    108 * Either when the user adds a tab (forceAdding will be true),
    109 * or when sources are registered (forceAdding will be false).
    110 *
    111 * @param {object} state
    112 * @param {Array<Source>} sources
    113 * @param {boolean} forceAdding
    114 *        If true, a tab should be opened for all the passed sources,
    115 *        even if the source has no url.
    116 *        If false, only sources matching a previously opened URL
    117 *        will be restored.
    118 * @return {object} Modified state object
    119 */
    120 function updateTabsWithNewActiveSource(state, sources, forceAdding = false) {
    121  let { urls, openedSources, prettyPrintedURLs, prettyPrintedDisabledURLs } =
    122    state;
    123  for (let source of sources) {
    124    // When we are adding a pretty printed source, we don't add a new tab.
    125    // We  only ensure the tab for the minimized/generated source is opened.
    126    //
    127    // We then rely on `prettyPrintedURLs` to pick the right source in the editor.
    128    if (source.isPrettyPrinted) {
    129      source = source.generatedSource;
    130 
    131      // Also ensure bookeeping that the source has been pretty printed.
    132      if (state.prettyPrintedURLs == prettyPrintedURLs) {
    133        prettyPrintedURLs = new Set(prettyPrintedURLs);
    134      }
    135      prettyPrintedURLs.add(source.url);
    136      prettyPrintedDisabledURLs.delete(source.url);
    137    }
    138 
    139    const { url } = source;
    140    // Ignore the source if it is already opened.
    141    // Also, when we are adding the tab (forceAdding=true), we want to add the tab for source,
    142    // even if they don't have a URL.
    143    // Otherwise, when we are simply registering a new active source (forceAdding=false),
    144    // we only want to show a tab if the source is in the persisted state.urls list.
    145    if (
    146      openedSources.includes(source) ||
    147      (!forceAdding && (!url || !urls.includes(url)))
    148    ) {
    149      continue;
    150    }
    151 
    152    // If we are pass that point in the for loop, we are opening a tab for the current source
    153    if (openedSources === state.openedSources) {
    154      openedSources = [...openedSources];
    155    }
    156 
    157    let index = -1;
    158    if (url) {
    159      if (!urls.includes(url)) {
    160        // Ensure adding the url to the persisted list
    161        if (urls === state.urls) {
    162          urls = [...state.urls];
    163        }
    164        // Newly opened tabs are always added first.
    165        urls.unshift(url);
    166      } else {
    167        // In this branch, we are restoring a previously opened tab.
    168        // We lookup for the position of this source in the persisted list (state.urls),
    169        // then find the index for the first persisted source which has an opened tab before it.
    170        const indexInUrls = urls.indexOf(url);
    171        for (let i = indexInUrls - 1; i >= 0; i--) {
    172          const previousSourceUrl = urls[i];
    173          index = openedSources.findIndex(s => s.url === previousSourceUrl);
    174          if (index != -1) {
    175            break;
    176          }
    177        }
    178      }
    179    }
    180 
    181    if (index == -1) {
    182      // Newly opened tabs are always added first.
    183      openedSources.unshift(source);
    184    } else {
    185      // Otherwise add the source at the expected location.
    186      // i.e. right after the source already opened which is before the currently added source in the persistent list of URLs (state.urls)
    187      openedSources.splice(index + 1, 0, source);
    188    }
    189  }
    190 
    191  if (
    192    openedSources != state.openedSources ||
    193    urls != state.urls ||
    194    prettyPrintedURLs != state.prettyPrintedURLs ||
    195    prettyPrintedDisabledURLs != state.prettyPrintedDisabledURLs
    196  ) {
    197    return {
    198      ...state,
    199      urls,
    200      openedSources,
    201      prettyPrintedURLs,
    202      prettyPrintedDisabledURLs,
    203    };
    204  }
    205  return state;
    206 }
    207 
    208 function closeTabsForSources(state, sources, permanent = false) {
    209  if (!sources.length) {
    210    return state;
    211  }
    212  // Pretty printed source have their tab refering to the minimized source
    213  const tabSources = sources.map(s =>
    214    s.isPrettyPrinted ? s.generatedSource : s
    215  );
    216 
    217  const newOpenedSources = state.openedSources.filter(source => {
    218    return !tabSources.includes(source);
    219  });
    220 
    221  // Bails out if no tab has changed
    222  if (newOpenedSources.length == state.openedSources.length) {
    223    return state;
    224  }
    225 
    226  // Also remove from the url list, if the tab closing is permanent.
    227  // i.e. when the user requested to close the tab, and not when a source is simply unregistered from the store.
    228  let { urls, prettyPrintedURLs } = state;
    229  if (permanent) {
    230    const sourceURLs = tabSources.map(source => source.url);
    231    urls = state.urls.filter(url => !sourceURLs.includes(url));
    232 
    233    // In case of pretty printing, tab's always refer to the minimized source.
    234    // So it is fair to unregister the tab's source's URL from `prettyPrintedURLs`
    235    // which contains the urls of the minmized source.
    236    prettyPrintedURLs = new Set(state.prettyPrintedURLs);
    237    for (const url of sourceURLs) {
    238      prettyPrintedURLs.delete(url);
    239    }
    240  }
    241  return { ...state, urls, prettyPrintedURLs, openedSources: newOpenedSources };
    242 }
    243 
    244 function moveTabInList(state, url, newIndex) {
    245  const currentIndex = state.openedSources.findIndex(
    246    source => source.url == url
    247  );
    248  return moveTab(state, currentIndex, newIndex);
    249 }
    250 
    251 function moveTabInListBySourceId(state, sourceId, newIndex) {
    252  const currentIndex = state.openedSources.findIndex(
    253    source => source.id == sourceId
    254  );
    255  return moveTab(state, currentIndex, newIndex);
    256 }
    257 
    258 function moveTab(state, currentIndex, newIndex) {
    259  // Avoid any state change if we are on the same position or the new is invalid
    260  if (currentIndex == newIndex || isNaN(newIndex)) {
    261    return state;
    262  }
    263 
    264  const { openedSources } = state;
    265  const source = openedSources[currentIndex];
    266 
    267  const newOpenedSources = Array.from(openedSources);
    268  // Remove the tab from its current location
    269  newOpenedSources.splice(currentIndex, 1);
    270  // And add it to the new one
    271  newOpenedSources.splice(newIndex, 0, source);
    272 
    273  // If the tabs relates to a source with a URL, also move it in the list of URLs
    274  let newUrls = state.urls;
    275  const { url } = source;
    276  if (url) {
    277    const urlIndex = state.urls.indexOf(url);
    278    let newUrlIndex = 0;
    279    // Lookup for the previous tab with a URL in order to move the tab
    280    // just after that one in the list of all URLs.
    281    for (let i = newIndex; i >= 0; i--) {
    282      const previousTabUrl = newOpenedSources[i].url;
    283      if (previousTabUrl) {
    284        newUrlIndex = state.urls.indexOf(previousTabUrl);
    285        break;
    286      }
    287    }
    288    if (urlIndex != -1 && newUrlIndex != -1) {
    289      newUrls = Array.from(state.urls);
    290      // Remove the tab from its current location
    291      newUrls.splice(urlIndex, 1);
    292      // And add it to the new one
    293      newUrls.splice(newUrlIndex, 0, url);
    294    }
    295  }
    296 
    297  return { ...state, urls: newUrls, openedSources: newOpenedSources };
    298 }
    299 
    300 export default update;