<commit_before>import { currentXur } from '@d2api/date';
import CheckButton from 'app/dim-ui/CheckButton';
import PageWithMenu from 'app/dim-ui/PageWithMenu';
import ShowPageLoading from 'app/dim-ui/ShowPageLoading';
import { t } from 'app/i18next-t';
import { useLoadStores } from 'app/inventory/store/hooks';
import { getCurrentStore } from 'app/inventory/stores-helpers';
import { ItemFilter } from 'app/search/filter-types';
import { searchFilterSelector } from 'app/search/search-filter';
import ErrorPanel from 'app/shell/ErrorPanel';
import { RootState, ThunkDispatchProp } from 'app/store/types';
import { emptyArray, emptyObject } from 'app/utils/empty';
import { useSubscription } from 'app/utils/hooks';
import {
  DestinyCollectibleComponent,
  DestinyCurrenciesComponent,
  DestinyItemPlug,
  DestinyProfileResponse,
} from 'bungie-api-ts/destiny2';
import _ from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
import Hammer from 'react-hammerjs';
import { connect } from 'react-redux';
import { DestinyAccount } from '../accounts/destiny-account';
import { D2ManifestDefinitions } from '../destiny2/d2-definitions';
import CharacterSelect from '../dim-ui/CharacterSelect';
import ErrorBoundary from '../dim-ui/ErrorBoundary';
import { mergeCollectibles } from '../inventory/d2-stores';
import { InventoryBuckets } from '../inventory/inventory-buckets';
import {
  bucketsSelector,
  ownedItemsSelector,
  profileResponseSelector,
  sortedStoresSelector,
} from '../inventory/selectors';
import { DimStore } from '../inventory/store-types';
import { loadingTracker } from '../shell/loading-tracker';
import { refresh$ } from '../shell/refresh';
import { loadAllVendors } from './actions';
import {
  D2VendorGroup,
  filterVendorGroupsToSearch,
  filterVendorGroupsToUnacquired,
  toVendorGroups,
} from './d2-vendors';
import { VendorsState } from './reducer';
import Vendor from './Vendor';
import VendorsMenu from './VendorsMenu';

interface ProvidedProps {
  account: DestinyAccount;
}
interface StoreProps {
  stores: DimStore[];
  buckets?: InventoryBuckets;
  defs?: D2ManifestDefinitions;
  ownedItemHashes: Set<number>;
  isPhonePortrait: boolean;
  searchQuery: string;
  profileResponse?: DestinyProfileResponse;
  vendors: VendorsState['vendorsByCharacter'];
  filterItems: ItemFilter;
}

function mapStateToProps() {
  const ownedItemSelectorInstance = ownedItemsSelector();

  return (state: RootState): StoreProps => ({
    stores: sortedStoresSelector(state),
    ownedItemHashes: ownedItemSelectorInstance(state),
    buckets: bucketsSelector(state),
    defs: state.manifest.d2Manifest,
    isPhonePortrait: state.shell.isPhonePortrait,
    searchQuery: state.shell.searchQuery,
    filterItems: searchFilterSelector(state),
    profileResponse: profileResponseSelector(state),
    vendors: state.vendors.vendorsByCharacter,
  });
}

type Props = ProvidedProps & StoreProps & ThunkDispatchProp;

/**
 * The "All Vendors" page for D2 that shows all the rotating vendors.
 */
function Vendors({
  defs,
  stores,
  buckets,
  ownedItemHashes,
  isPhonePortrait,
  searchQuery,
  filterItems,
  profileResponse,
  vendors,
  dispatch,
  account,
}: Props) {
  const [characterId, setCharacterId] = useState<string>();
  const [filterToUnacquired, setFilterToUnacquired] = useState(false);

  const selectedStoreId = characterId || getCurrentStore(stores)?.id;

  useLoadStores(account, stores.length > 0);

  useEffect(() => {
    if (selectedStoreId) {
      dispatch(loadAllVendors(account, selectedStoreId));
    }
  }, [account, selectedStoreId, dispatch]);

  useSubscription(() =>
    refresh$.subscribe(() => {
      if (selectedStoreId) {
        loadingTracker.addPromise(dispatch(loadAllVendors(account, selectedStoreId, true)));
      }
    })
  );

  const onCharacterChanged = (storeId: string) => setCharacterId(storeId);

  const handleSwipe: HammerListener = (e) => {
    const characters = stores.filter((s) => !s.isVault);

    const selectedStoreIndex = selectedStoreId
      ? characters.findIndex((s) => s.id === selectedStoreId)
      : characters.findIndex((s) => s.current);

    if (e.direction === 2 && selectedStoreIndex < characters.length - 1) {
      setCharacterId(characters[selectedStoreIndex + 1].id);
    } else if (e.direction === 4 && selectedStoreIndex > 0) {
      setCharacterId(characters[selectedStoreIndex - 1].id);
    }
  };

  const mergedCollectibles = useMemo(
    () =>
      profileResponse
        ? mergeCollectibles(
            profileResponse.profileCollectibles,
            profileResponse.characterCollectibles
          )
        : emptyObject<{
            [x: number]: DestinyCollectibleComponent;
          }>(),
    [profileResponse]
  );

  const vendorData = selectedStoreId ? vendors[selectedStoreId] : undefined;
  const vendorsResponse = vendorData?.vendorsResponse;

  let vendorGroups = useMemo(
    () =>
      vendorsResponse && defs && buckets
        ? toVendorGroups(vendorsResponse, defs, buckets, account, mergedCollectibles)
        : emptyArray<D2VendorGroup>(),
    [account, buckets, defs, mergedCollectibles, vendorsResponse]
  );

  if (!vendorsResponse && vendorData?.error) {
    return (
      <PageWithMenu>
        <ErrorPanel error={vendorData.error} />
      </PageWithMenu>
    );
  }

  if (!stores.length) {
    return (
      <PageWithMenu>
        <ShowPageLoading message={t('Loading.Profile')} />
      </PageWithMenu>
    );
  }

  const selectedStore = stores.find((s) => s.id === selectedStoreId)!;
  const currencyLookups = vendorsResponse?.currencyLookups.data?.itemQuantities;

  if (vendorGroups && filterToUnacquired) {
    vendorGroups = filterVendorGroupsToUnacquired(vendorGroups, ownedItemHashes);
  }
  if (vendorGroups && searchQuery.length) {
    vendorGroups = filterVendorGroupsToSearch(vendorGroups, searchQuery, filterItems);
  }

  if (
    currentXur().start === undefined &&
    vendorGroups &&
    Object.values(vendorGroups).some((v) => v.def.hash === 3227191227)
  ) {
    const index = vendorGroups
      .map(function (v) {
        return v.def.hash;
      })
      .indexOf(3227191227);
    delete vendorGroups[index];
  }

  const fullOwnedItemHashes = enhanceOwnedItemsWithPlugSets(ownedItemHashes, defs, profileResponse);

  return (
    <PageWithMenu>
      <PageWithMenu.Menu>
        {selectedStore && (
          <CharacterSelect
            stores={stores}
            isPhonePortrait={isPhonePortrait}
            selectedStore={selectedStore}
            onCharacterChanged={onCharacterChanged}
          />
        )}
        {selectedStore && (
          <CheckButton checked={filterToUnacquired} onChange={setFilterToUnacquired}>
            {t('Vendors.FilterToUnacquired')}
          </CheckButton>
        )}
        {!isPhonePortrait && vendorGroups && <VendorsMenu groups={vendorGroups} />}
      </PageWithMenu.Menu>
      <PageWithMenu.Contents><commit_msg>Only remove entire vendorgroup if xur was only vendor delete does not update length<commit_after>import { currentXur } from '@d2api/date';
import CheckButton from 'app/dim-ui/CheckButton';
import PageWithMenu from 'app/dim-ui/PageWithMenu';
import ShowPageLoading from 'app/dim-ui/ShowPageLoading';
import { t } from 'app/i18next-t';
import { useLoadStores } from 'app/inventory/store/hooks';
import { getCurrentStore } from 'app/inventory/stores-helpers';
import { VENDORS } from 'app/search/d2-known-values';
import { ItemFilter } from 'app/search/filter-types';
import { searchFilterSelector } from 'app/search/search-filter';
import ErrorPanel from 'app/shell/ErrorPanel';
import { RootState, ThunkDispatchProp } from 'app/store/types';
import { emptyArray, emptyObject } from 'app/utils/empty';
import { useSubscription } from 'app/utils/hooks';
import {
  DestinyCollectibleComponent,
  DestinyCurrenciesComponent,
  DestinyItemPlug,
  DestinyProfileResponse,
} from 'bungie-api-ts/destiny2';
import _ from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
import Hammer from 'react-hammerjs';
import { connect } from 'react-redux';
import { DestinyAccount } from '../accounts/destiny-account';
import { D2ManifestDefinitions } from '../destiny2/d2-definitions';
import CharacterSelect from '../dim-ui/CharacterSelect';
import ErrorBoundary from '../dim-ui/ErrorBoundary';
import { mergeCollectibles } from '../inventory/d2-stores';
import { InventoryBuckets } from '../inventory/inventory-buckets';
import {
  bucketsSelector,
  ownedItemsSelector,
  profileResponseSelector,
  sortedStoresSelector,
} from '../inventory/selectors';
import { DimStore } from '../inventory/store-types';
import { loadingTracker } from '../shell/loading-tracker';
import { refresh$ } from '../shell/refresh';
import { loadAllVendors } from './actions';
import {
  D2VendorGroup,
  filterVendorGroupsToSearch,
  filterVendorGroupsToUnacquired,
  toVendorGroups,
} from './d2-vendors';
import { VendorsState } from './reducer';
import Vendor from './Vendor';
import VendorsMenu from './VendorsMenu';

interface ProvidedProps {
  account: DestinyAccount;
}
interface StoreProps {
  stores: DimStore[];
  buckets?: InventoryBuckets;
  defs?: D2ManifestDefinitions;
  ownedItemHashes: Set<number>;
  isPhonePortrait: boolean;
  searchQuery: string;
  profileResponse?: DestinyProfileResponse;
  vendors: VendorsState['vendorsByCharacter'];
  filterItems: ItemFilter;
}

function mapStateToProps() {
  const ownedItemSelectorInstance = ownedItemsSelector();

  return (state: RootState): StoreProps => ({
    stores: sortedStoresSelector(state),
    ownedItemHashes: ownedItemSelectorInstance(state),
    buckets: bucketsSelector(state),
    defs: state.manifest.d2Manifest,
    isPhonePortrait: state.shell.isPhonePortrait,
    searchQuery: state.shell.searchQuery,
    filterItems: searchFilterSelector(state),
    profileResponse: profileResponseSelector(state),
    vendors: state.vendors.vendorsByCharacter,
  });
}

type Props = ProvidedProps & StoreProps & ThunkDispatchProp;

/**
 * The "All Vendors" page for D2 that shows all the rotating vendors.
 */
function Vendors({
  defs,
  stores,
  buckets,
  ownedItemHashes,
  isPhonePortrait,
  searchQuery,
  filterItems,
  profileResponse,
  vendors,
  dispatch,
  account,
}: Props) {
  const [characterId, setCharacterId] = useState<string>();
  const [filterToUnacquired, setFilterToUnacquired] = useState(false);

  const selectedStoreId = characterId || getCurrentStore(stores)?.id;

  useLoadStores(account, stores.length > 0);

  useEffect(() => {
    if (selectedStoreId) {
      dispatch(loadAllVendors(account, selectedStoreId));
    }
  }, [account, selectedStoreId, dispatch]);

  useSubscription(() =>
    refresh$.subscribe(() => {
      if (selectedStoreId) {
        loadingTracker.addPromise(dispatch(loadAllVendors(account, selectedStoreId, true)));
      }
    })
  );

  const onCharacterChanged = (storeId: string) => setCharacterId(storeId);

  const handleSwipe: HammerListener = (e) => {
    const characters = stores.filter((s) => !s.isVault);

    const selectedStoreIndex = selectedStoreId
      ? characters.findIndex((s) => s.id === selectedStoreId)
      : characters.findIndex((s) => s.current);

    if (e.direction === 2 && selectedStoreIndex < characters.length - 1) {
      setCharacterId(characters[selectedStoreIndex + 1].id);
    } else if (e.direction === 4 && selectedStoreIndex > 0) {
      setCharacterId(characters[selectedStoreIndex - 1].id);
    }
  };

  const mergedCollectibles = useMemo(
    () =>
      profileResponse
        ? mergeCollectibles(
            profileResponse.profileCollectibles,
            profileResponse.characterCollectibles
          )
        : emptyObject<{
            [x: number]: DestinyCollectibleComponent;
          }>(),
    [profileResponse]
  );

  const vendorData = selectedStoreId ? vendors[selectedStoreId] : undefined;
  const vendorsResponse = vendorData?.vendorsResponse;

  let vendorGroups = useMemo(
    () =>
      vendorsResponse && defs && buckets
        ? toVendorGroups(vendorsResponse, defs, buckets, account, mergedCollectibles)
        : emptyArray<D2VendorGroup>(),
    [account, buckets, defs, mergedCollectibles, vendorsResponse]
  );

  if (!vendorsResponse && vendorData?.error) {
    return (
      <PageWithMenu>
        <ErrorPanel error={vendorData.error} />
      </PageWithMenu>
    );
  }

  if (!stores.length) {
    return (
      <PageWithMenu>
        <ShowPageLoading message={t('Loading.Profile')} />
      </PageWithMenu>
    );
  }

  const selectedStore = stores.find((s) => s.id === selectedStoreId)!;
  const currencyLookups = vendorsResponse?.currencyLookups.data?.itemQuantities;

  if (vendorGroups && filterToUnacquired) {
    vendorGroups = filterVendorGroupsToUnacquired(vendorGroups, ownedItemHashes);
  }
  if (vendorGroups && searchQuery.length) {
    vendorGroups = filterVendorGroupsToSearch(vendorGroups, searchQuery, filterItems);
  }

  if (
    currentXur().start === undefined &&
    vendorGroups &&
    Object.values(vendorGroups).some((v) => v.def.hash === 3227191227)
  ) {
    const index = vendorGroups
      .map(function (v) {
        return v.def.hash;
      })
      .indexOf(3227191227);
    if (Object.values(vendorGroups[index].vendors).some((v) => v.def.hash === VENDORS.XUR)) {
      const xurIndex = vendorGroups[index].vendors
        .map(function (v) {
          return v.def.hash;
        })
        .indexOf(VENDORS.XUR);
      delete vendorGroups[index].vendors[xurIndex]; // Remove Xur
      vendorGroups[index].vendors.length--;
    }
    if (!vendorGroups[index].vendors.length) {
      delete vendorGroups[index]; // Remove "Limited Time" if Xur was only Vendor
      vendorGroups.length--;
    }
  }

  const fullOwnedItemHashes = enhanceOwnedItemsWithPlugSets(ownedItemHashes, defs, profileResponse);

  return (
    <PageWithMenu>
      <PageWithMenu.Menu>
        {selectedStore && (
          <CharacterSelect
            stores={stores}
            isPhonePortrait={isPhonePortrait}
            selectedStore={selectedStore}
            onCharacterChanged={onCharacterChanged}
          />
        )}
        {selectedStore && (
          <CheckButton checked={filterToUnacquired} onChange={setFilterToUnacquired}>
            {t('Vendors.FilterToUnacquired')}
          </CheckButton>
        )}
        {!isPhonePortrait && vendorGroups && <VendorsMenu groups={vendorGroups} />}
      </PageWithMenu.Menu>
      <PageWithMenu.Contents>
{'fraction_non_alphanumeric': 0.12221151198017075, 'fraction_numerical': 0.00571467915174883, 'mean_word_length': 3.852990310725025, 'pattern_counts': {'":': 0, '<': 41, '<?xml version=': 0, '>': 70, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3921992', 'n_tokens_mistral': 4415, 'n_tokens_neox': 4317, 'n_words': 864}