Загрузка данных


import React, { FC, lazy, useContext, useEffect, useState } from 'react';

import { DNDWrapper } from '@components/DNDWrapper';
import { InstrumentSearch } from '@components/InstrumentSearch';
import WidgetContentWrapper from '@components/WidgetContentWrapper';
import WidgetHeader from '@components/WidgetHeader';
import { useDropInstrument } from '@hooks/dnd';

import { useAppSelect } from '@hooks/useAppSelector';
import { useChangeProperties } from '@modules/widgetProperties';
import { getIsShowMoexChartInChart } from '@store/selectors/cachedData';
import { WidgetEnvContext } from '@terminal/desktop/workspaces/default/components/Widget/context';
import { useWidgetHeaderName } from '@utils/useWidgetName';

import useChartComponentFacade from './hooks/useChartComponentFacade';

import DEFAULT_PROPERTIES from './properties/defaultProperties';

import type { WidgetProperties } from './properties/types';
import type { ChartContainerProps } from './types';

const LazyTREntry = lazy(() => import('./components/MoexChart/MoexChart'));

export const Chart: FC<ChartContainerProps> = function (props): JSX.Element {
  const indicativeData = props.widgetContentProps?.indicativeData;

  const { updateProperties } = useChangeProperties<WidgetProperties>();

  const isShowMoexChartInChart = useAppSelect(getIsShowMoexChartInChart);

  const [isMoexChartShow, setIsMoexChartShow] = useState(
    // Доработка пошла хотфиксом, поэтому не стал исправлять все рабочие столы
    props.widgetContentProps?.isMoexChartShow ?? DEFAULT_PROPERTIES.isMoexChartShow,
  );

  const {
    chartContainerRef,
    dropDownOpen,
    setDropdownOpen,
    currentInstrument,
    handlerSaveAsExcel,
    setIsWidgetHeaderContextMenuOpen,
    isWidgetHeaderContextMenuOpen,
    onDropInstruments,
    addInstrumentFromModal,
    isOver,
  } = useChartComponentFacade(props);

  /* на дроп обновляем название бумаги в сторе
    и делаем апдейт на бэк, чтобы сохранить изменения
    при обновлении страницы */
  const { dropRef, isDragged } = useDropInstrument((dragData) => {
    onDropInstruments(dragData.properties.issKey, true);
  });

  const [isOpenEmptyAction, setIsOpenEmptyAction] = useState<boolean>(false);

  const openInstrumentModal = () => {
    setIsOpenEmptyAction(true);
    setIsWidgetHeaderContextMenuOpen(false);
  };

  // TODO: Зависит от сеток
  const { isDraggedOver } = useContext(WidgetEnvContext);
  const contractsInstrumentName = useWidgetHeaderName(currentInstrument.current);

  const instrumentName = indicativeData
    ? `${indicativeData.instrumentName} ${indicativeData.settlement} - ${indicativeData.firmName}`
    : contractsInstrumentName;

  useEffect(() => {
    updateProperties((state) => {
      state.isMoexChartShow = isMoexChartShow;
    });
  }, [isMoexChartShow, updateProperties]);

  return (
    <DNDWrapper
      ref={dropRef}
      isOver={isOver}
      canDrop
    >
      <WidgetHeader
        {...props}
        dropdownOpen={dropDownOpen}
        setDropdownOpen={setDropdownOpen}
        itemsSearchIcon={[true]}
        handlerSaveAsExcel={indicativeData ? null : handlerSaveAsExcel}
        addToWidgetNamePrefix={instrumentName}
        setIsOpenContextMenuFromWidget={setIsWidgetHeaderContextMenuOpen}
        isOpenContextMenuFromWidget={isWidgetHeaderContextMenuOpen}
        openInstrumentsModal={openInstrumentModal}
      />
      <WidgetContentWrapper {...props}>
        <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
          {isShowMoexChartInChart && (
            <LazyTREntry
              fullName={currentInstrument.current}
              widgetId={props.widgetId}
            />
          )}

          <div
            className="TVChartContainer"
            style={{
              width: '100%',
              height: '100%',
              pointerEvents: isDragged || isDraggedOver ? 'none' : 'inherit',
              display: !isMoexChartShow ? 'block' : 'none',
            }}
            ref={chartContainerRef}
          />

          {isOpenEmptyAction && (
            <InstrumentSearch
              setOpen={setIsOpenEmptyAction}
              isOpen={isOpenEmptyAction}
              variant="single"
              widgetId={props.widgetId}
              // withNRD={false}
              addInstruments={addInstrumentFromModal}
            />
          )}
        </div>
      </WidgetContentWrapper>
    </DNDWrapper>
  );
};


import classNames from 'classnames';
import React, { Component, PropsWithChildren } from 'react';

import { WidgetToastContainer } from '@components/Toast';

import features from '@features';

import styles from './widgetContentWrapper.module.scss';

import type { WidgetContentBasicProps } from 'types/Widgets';

type WidgetContentWrapperProps = PropsWithChildren<Record<string, unknown> & WidgetContentBasicProps>;

interface WidgetContentWrapperState {
  error: boolean;
}

class WidgetContentWrapper extends Component<WidgetContentWrapperProps, WidgetContentWrapperState> {
  render() {
    const {
      children,
      onWidgetContentClick,
      scrollable = true,
      refWrapper,
      style,
      widgetId,
      containsIframe,
      isIframeView,
    } = this.props;

    return (
      <div
        onClick={onWidgetContentClick}
        className={classNames(styles['widget-content-wrapper'], {
          '-not-scroll': !scrollable,
          [styles['widget-content-wrapper-contains-iframe']]: features.enableBookBuilder && containsIframe,
          [styles['widget-content-wrapper-inside-iframe']]: features.enableBookBuilder && isIframeView,
        })}
        style={style}
        ref={refWrapper}
      >
        {children}

        <WidgetToastContainer widgetId={widgetId} />
      </div>
    );
  }
}

export default WidgetContentWrapper;


import { Dropdown, Input, MenuProps } from 'antd';
import classNames from 'classnames';
import React, { FC, MouseEvent, ReactNode, useEffect, useState } from 'react';

import { ContextMenu } from '@components/ContextMenu';
import { IconButton } from '@components/IconButton';
import Icons from '@components/Icons';

import './widgetHeader.scss';

import { CloseIcon } from '@components/Icons/CloseIcon';
import { WidgetBindIcon } from '@components/Icons/WidgetBindIcon';
import { InputSearch } from '@components/Input';
import { TextInput } from '@components/TextMad';
import { Footnote } from '@components/TextUI';
import { MacroDataWidgetMenu } from '@components/WidgetHeader/components/MacroDataWidgetMenu';
import { bindingWidgets } from '@components/WidgetHeader/config';
import { DRUG_HEADER_CLASSNAME, NO_DRAG_CLASSNAME } from '@configs/appConfig';
import { useAppSelect } from '@hooks/useAppSelector';
import { useWidgetNamePrefix } from '@hooks/useWidgetNamePrefix';
import colors from '@styles/colors.module.scss';
import { CheckboxValue } from '@uikit/Checkbox';
import Typography from '@uikit/Typography';
import { wrapBetaInSpan } from '@utils/wrapBetaInSpan';
import { FixingData } from '@widgets/Fixings/useFixingsFacade';
import { useChatUIConfig } from '@widgets/NoTradeChat/context/ChatUIConfig';
import { FromNoTradeChat } from 'types/NoTradeChat';
import { isDefined } from 'types/utils';
import { WidgetContentType } from 'types/Widgets';

import { DefaultMenuProps, DefaultWidgetMenu } from './components/DefaultWidgetMenu';

import { FilterSwitch, FilterSwitchProps } from './components/FilterSwitch';
import { useDropdownPosition } from './hooks/useDropdownPosition';
import useWidgetBind from './hooks/useWidgetBind';
import { useWidgetHeaderFacade } from './hooks/useWidgetHeaderFacade';
import styles from './widgetHeader.module.scss';

import type { WidgetContentBasicProps } from 'types/Widgets';

const { MoreVertIcon, FilterTableIcon } = Icons;

export interface WidgetHeaderProps extends WidgetContentBasicProps {
  editWidget?(e: MouseEvent): void;
  /* Необходимые элементы для отображения элемента меню "Настроить" */
  items?: MenuProps['items'];
  itemsSearchIcon?: MenuProps['items'] | boolean[];
  addToWidgetNamePrefix?: string;
  dropDownRender?(menu: React.ReactNode): JSX.Element;
  dropDownRenderSearchIcon?(menu: React.ReactNode): JSX.Element;
  /* Отображение элемента меню "Настроить". По дефолту включено */
  showWidgetSetting?: boolean;
  dropdownOpen?: boolean;
  setDropdownOpen?: React.Dispatch<React.SetStateAction<boolean>>;
  ticketFixingsWidget?: FixingData | null;
  visible?: boolean;
  setVisible?: React.Dispatch<React.SetStateAction<boolean>>;
  fromNoTradeChat?: FromNoTradeChat;
  setFilterToolState?: React.Dispatch<
    React.SetStateAction<{
      isOpenFilterBlock: boolean;
      isOpenFilterTools: boolean;
      activeFilterCount: number;
    }>
  >;
  filterToolState?: {
    isOpenFilterBlock: boolean;
    isOpenFilterTools: boolean;
    activeFilterCount: number;
  } | null;
  /** undefined - не отображаем, null - дизейблим */
  handlerSaveAsExcel?: (() => void) | null;
  setIsLinkedWidget?: React.Dispatch<React.SetStateAction<boolean>>;
  setIsOpenedSearchBlock?: React.Dispatch<React.SetStateAction<boolean>>;
  isOpenedSearchBlock?: boolean;
  widgetType?: WidgetContentType;
  handleHeaderSearch?: (value: string) => void;
  headerSearchValue?: string;
  className?: string;
  filterCheckedList?: CheckboxValue[];
  isAbleToExpand?: boolean;
  children?: React.ReactNode;
  isOpenContextMenuFromWidget?: boolean;
  setIsOpenContextMenuFromWidget?: React.Dispatch<React.SetStateAction<boolean>>;
  /** Связать с другим виджетом */
  hideBindIcon?: boolean;
  openInstrumentsModal?: () => void;
  menuItems?: ReactNode[];
  filterSwitch?: FilterSwitchProps;
  /** Компонент поиска содержимого из таблицы  */
  tableSearchComponent?: React.JSX.Element | undefined;
  /** Кнопки, размещаемые в хедере справа */
  rightBtns?: ReactNode;
  /** Информация, размещаемая под названием виджета (например, торговый логин пользователя) */
  details?: string;
  /** Вместо троеточия будет просто крестик, по клику на который можно будет закрыть виджет */
  closeIconInsteadOfDotsWithDropdown?: boolean;
}

const WidgetHeader: FC<WidgetHeaderProps> = function (props): JSX.Element {
  const {
    widgetId,
    items = [],
    itemsSearchIcon = [],
    dropDownRender,
    dropDownRenderSearchIcon,
    addToWidgetNamePrefix = '',
    showWidgetSetting = true,
    dropdownOpen,
    setDropdownOpen,
    ticketFixingsWidget = null,
    visible = false,
    setVisible,
    fromNoTradeChat,
    setFilterToolState,
    filterToolState = null,
    handlerSaveAsExcel,
    setIsLinkedWidget,
    setIsOpenedSearchBlock,
    isOpenedSearchBlock,
    handleHeaderSearch,
    className,
    filterCheckedList,
    isAbleToExpand,
    children,
    isOpenContextMenuFromWidget,
    setIsOpenContextMenuFromWidget,
    hideBindIcon,
    openInstrumentsModal,
    headerSearchValue,
    menuItems,
    filterSwitch,
    tableSearchComponent,
    rightBtns,
    details,
    closeIconInsteadOfDotsWithDropdown,
  } = props;

  const {
    type,
    name,
    isEditMode,
    inputRef,
    inputValue,
    onInputChange,
    onEditBtnClick,
    onExpandBtnClick,
    deleteWidget,
    isExpand,
    event,
    isMoreBtnDropdownOpen,
    setIsMoreBtnDropdownOpen,
    onMoreBtnClick,
    resetEvent,
  } = useWidgetHeaderFacade(props);

  const { itemsBind, bindCount, colorIconBind, isBindDisabled } = useWidgetBind({
    widgetId,
    type,
    setIsLinkedWidget,
  });

  const { namePrefix } = useWidgetNamePrefix(type);

  const [dropwdownSearchIcon] = useDropdownPosition({
    isOpen: dropdownOpen ?? false,
  });

  const uiConfig = useChatUIConfig();

  const widgetType = useAppSelect((state) => state.widgets.widgets.find((it) => it.id === widgetId))?.type;

  const isCanBindWidget = widgetType && bindingWidgets.includes(widgetType);

  const [linkedWidgets, setLinkedWidgets] = useState(false);

  const defaultMenuProps: DefaultMenuProps = {
    onEditBtnClick,
    fromNoTradeChat,
    setIsMoreBtnDropdownOpen,
    showWidgetSetting,
    dropDownRender,
    items,
    visible,
    setIsOpenedSearchBlock,
    setVisible,
    dropDownRenderSearchIcon,
    itemsSearchIcon,
    dropwdownSearchIcon,
    dropdownOpen,
    setDropdownOpen,
    handlerSaveAsExcel,
    onExpandBtnClick,
    isExpand,
    deleteWidget,
    linkedWidgets,
    setLinkedWidgets,
    itemsBind,
    bindCount,
    isCanBindWidget,
    filterCheckedList,
    openInstrumentsModal,
  };

  const dropdownMenuMap: Partial<Record<WidgetContentType, JSX.Element>> = {
    [WidgetContentType.macroData]: (
      <MacroDataWidgetMenu
        deleteWidget={deleteWidget}
        items={items}
        setVisible={setVisible}
        visible={visible}
        handlerSaveAsExcel={handlerSaveAsExcel}
        setFilterToolState={setFilterToolState}
        resetEvent={resetEvent}
        dropDownRender={dropDownRender}
      />
    ),
    [WidgetContentType.news]: (
      <DefaultWidgetMenu
        defaultMenuProps={{ ...defaultMenuProps, isNewsWidget: true }}
        isAbleToExpand={isAbleToExpand}
      />
    ),
  };

  useEffect(() => {
    if (!isMoreBtnDropdownOpen) {
      setLinkedWidgets(false);
    }
  }, [isMoreBtnDropdownOpen]);

  const onSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    if (!handleHeaderSearch) {
      return;
    }
    const { value } = e.target;
    handleHeaderSearch(value);
  };

  if (children) {
    // используется в виджете IncomingCall
    return (
      <div
        style={{ display: `${isOpenedSearchBlock ? 'none' : ''}` }}
        className={classNames(styles['workspace-widget-content-header-children'], DRUG_HEADER_CLASSNAME, className)}
      >
        {children}
      </div>
    );
  }

  const shouldDisplaySearchBar = !!filterToolState?.isOpenFilterBlock;

  const haveCustomSearchBar = !!tableSearchComponent;

  return (
    <div
      style={{ display: `${isOpenedSearchBlock ? 'none' : ''}` }}
      className={classNames(
        styles['workspace-widget-content-header'],
        {
          [styles['no-cursor-move']]: uiConfig?.isIframeView,
          [styles['workspace-widget-content-header_large']]: Boolean(details),
        },
        DRUG_HEADER_CLASSNAME,
        className,
      )}
    >
      {/* Компонент поиска содержимого из таблицы */}
      {haveCustomSearchBar && tableSearchComponent}
      {!haveCustomSearchBar && shouldDisplaySearchBar && (
        <div className="virtual-table-search-top">
          <div className="virtual-table-search-input">
            <InputSearch
              placeholder="Поиск"
              value={headerSearchValue}
              onChange={onSearchChange}
            />
          </div>
          <div className={styles.flexGapSmall}>
            <div
              onClick={() => {
                if (setFilterToolState) {
                  setFilterToolState((prevState) => ({
                    ...prevState,
                    isOpenFilterTools: !filterToolState?.isOpenFilterTools,
                  }));
                }
              }}
              className="virtual-table-search-icon virtual-table-search-icon-filter"
            >
              <FilterTableIcon style={filterToolState?.isOpenFilterTools ? { color: '#6C7FE0' } : {}} />
              {filterToolState.activeFilterCount > 0 && !filterToolState?.isOpenFilterTools && (
                <div className="virtual-table-search-icon-count">{filterToolState.activeFilterCount}</div>
              )}
            </div>
            <div
              onMouseDown={(e) => e.stopPropagation()}
              onClick={() => {
                if (handleHeaderSearch) {
                  handleHeaderSearch('');
                }
                if (setFilterToolState) {
                  setFilterToolState((prevState) => ({
                    ...prevState,
                    isOpenFilterBlock: false,
                    isOpenFilterTools: false,
                  }));
                }
              }}
              className="virtual-table-search-icon virtual-table-search-icon-close"
            >
              <CloseIcon />
            </div>
          </div>
        </div>
      )}

      {!haveCustomSearchBar && !shouldDisplaySearchBar && (
        <div className={styles['workspace-widget-content-header_container']}>
          <div className={styles['title-container']}>
            <div className={styles.title}>
              <TextInput
                style={{
                  textOverflow: 'ellipsis',
                  overflow: 'hidden',
                  display: 'flex',
                  alignItems: 'center',
                }}
              >
                {wrapBetaInSpan(String(namePrefix))}
                {wrapBetaInSpan(addToWidgetNamePrefix)}
              </TextInput>
              <TextInput style={{ display: 'flex', color: colors['text-interface-on-color'] }}>
                {name && !isEditMode ? `- ${name}` : ''}
              </TextInput>
              {!hideBindIcon && (
                <Dropdown
                  disabled={isBindDisabled}
                  placement="bottomLeft"
                  arrow={{ pointAtCenter: true }}
                  menu={{ items: itemsBind }}
                  trigger={['click']}
                >
                  <div
                    style={{ color: colorIconBind || 'grey' }}
                    className={classNames(styles['left-btn-container'], NO_DRAG_CLASSNAME)}
                  >
                    <button
                      style={{ color: colorIconBind || 'grey' }}
                      type="button"
                    >
                      <WidgetBindIcon />
                    </button>
                    {bindCount || ''}
                  </div>
                </Dropdown>
              )}
              {ticketFixingsWidget && <Footnote style={{ display: 'flex' }}>{ticketFixingsWidget.ticker}</Footnote>}
              {isEditMode && (
                <>
                  <Footnote style={{ display: 'flex' }}>-</Footnote>
                  <Input
                    style={{ display: 'flex', height: '21px' }}
                    ref={inputRef}
                    value={inputValue}
                    onChange={onInputChange}
                  />
                </>
              )}
            </div>
            {details && (
              <Typography.Paragraph.XS
                text={details}
                className={styles.subtitle}
              />
            )}
          </div>
          <div className={styles['right-btn-container']}>
            {/** TODO Убрать когда точно будем знать что это нам не нужно */}
            {filterToolState && filterToolState?.activeFilterCount > 0 && (
              <div
                aria-disabled
                onClick={() => {
                  if (setFilterToolState) {
                    setFilterToolState((prevState) => ({
                      ...prevState,
                      isOpenFilterTools: true,
                      isOpenFilterBlock: true,
                    }));
                  }
                }}
                className={classNames(
                  'virtual-table-search-icon',
                  'virtual-table-search-icon-filter',
                  'virtual-table-search-icon-filter-close',
                )}
              >
                <FilterTableIcon />
                {filterToolState.activeFilterCount > 0 && (
                  <div className="virtual-table-search-icon-count">{filterToolState.activeFilterCount}</div>
                )}
              </div>
            )}
            {rightBtns}
            {filterSwitch && <FilterSwitch {...filterSwitch} />}
            {closeIconInsteadOfDotsWithDropdown ? (
              <IconButton
                className={classNames(NO_DRAG_CLASSNAME, styles.closeIcon)}
                icon={<CloseIcon />}
                onClick={deleteWidget}
              />
            ) : (
              <button
                type="button"
                className={NO_DRAG_CLASSNAME}
                onClick={onMoreBtnClick}
              >
                <MoreVertIcon />
              </button>
            )}

            <ContextMenu
              event={event}
              openContextMenu={
                isDefined(isOpenContextMenuFromWidget) ? isOpenContextMenuFromWidget : isMoreBtnDropdownOpen
              }
              setOpenContextMenu={
                isDefined(setIsOpenContextMenuFromWidget) ? setIsOpenContextMenuFromWidget : setIsMoreBtnDropdownOpen
              }
              setDropdownOpen={setDropdownOpen}
              setVisible={setVisible}
            >
              {dropdownMenuMap[widgetType as WidgetContentType] ?? (
                <DefaultWidgetMenu
                  defaultMenuProps={defaultMenuProps}
                  isAbleToExpand={isAbleToExpand}
                  menuItems={menuItems}
                />
              )}
            </ContextMenu>
          </div>
        </div>
      )}
    </div>
  );
};

export default WidgetHeader;


import isEqual from 'lodash/isEqual';
import { Dispatch, MutableRefObject, SetStateAction, useCallback, useEffect, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';

import { candlesController } from '@api/controllers/candles';
import { uiConfig } from '@configs/manager';
import { communicator } from '@core/comm';
import { useAppSelect } from '@hooks/useAppSelector';
import {
  ChartingLibraryWidgetOptions,
  IChartingLibraryWidget,
  ResolutionString,
  widget,
} from '@modules/charting_library/charting_library';
import { Contract } from '@modules/contracts';
import { CORPACTIONS_OPEN_EXESTED_WIDGET_EVENT, HIGHLIGHT_WIDGET_EVENT } from '@modules/widgets/shared';
import { addContentPropsToWidget, unbindWidgets } from '@store/slices/widgets';

import { getState } from '@store/store';
import { chartToReqTimeConverter } from '@utils/chartToReqTimeConverter';
import { useWidgetsBind } from '@utils/hooks/useWidgetsBind';

import { DAY_LINE_ON_CHART_BOARDS, DEFAULT_SYMBOL, MINUTE_CANDLES_ON_CHART_BOARDS } from '../const';
import useDatafeed from '../datafeed';
import { ResolutionMap } from '../types';

import { isIndicativeTicker } from '../utils/isIndicativeTicker';
import { mapResolutionIntervals } from '../utils/resolutionIntervals';
import { formatToDateTime } from '../utils/utils';

import { useChartPublicContext } from './useChartPublicContext';

import type { WidgetProperties } from '../properties/types';
import type { ChartContainerProps } from '../types';
import type { Candles } from 'types/Candles';
import type { Widget } from 'types/Widgets';

enum SeriesStyle {
  Bars = 0,
  Candles = 1,
  Line = 2,
  Area = 3,
  HeikenAshi = 8,
  HollowCandles = 9,
  Baseline = 10,
  HiLo = 12,
  Column = 13,
  Renko = 4,
  Kagi = 5,
  PointAndFigure = 6,
  LineBreak = 7,
}

interface UseChartComponentFacadeReturn {
  chartContainerRef: MutableRefObject<HTMLDivElement>;
  savedTvWidget: IChartingLibraryWidget | null;
  dropDownOpen: boolean;
  setDropdownOpen: Dispatch<SetStateAction<boolean>>;
  currentInstrument: MutableRefObject<string>;
  handlerSaveAsExcel: (() => void) | undefined;
  isWidgetHeaderContextMenuOpen: boolean;
  setIsWidgetHeaderContextMenuOpen: Dispatch<SetStateAction<boolean>>;
  onDropInstruments: (val: string, withUpdate?: boolean) => void;
  addInstrumentFromModal: (instruments: Contract[]) => void;
  isOver: boolean;
}

const DEFAULT_OPTIONS = {
  symbol: DEFAULT_SYMBOL,
  interval: '1' as ResolutionString,
  datafeedUrl: 'https://demo_feed.tradingview.com',
  libraryPath: `${uiConfig.staticContextPath || ''}/charting_library/`,
  chartsStorageUrl: 'https://saveload.tradingview.com',
  chartsStorageApiVersion: '1.1' as const,
  clientId: 'tradingview.com',
  userId: 'public_user_id',
  fullscreen: false,
  autosize: true,
  studiesOverrides: {},
};

export default function useChartComponentFacade(props: ChartContainerProps): UseChartComponentFacadeReturn {
  const { widgetId } = props;

  const [isOver, setIsOver] = useState<boolean>(false);

  const chartContainerRef = useRef<HTMLDivElement | null>(null) as MutableRefObject<HTMLDivElement>;
  const tvWidgetRef = useRef<IChartingLibraryWidget | null>(null);
  const dispatch = useDispatch();
  const indicativeData = props.widgetContentProps?.indicativeData;

  const chartPrevStateRef = useRef<object>({});

  // используется для определения источника отображаемого в виджете инструмента (из привязки или нет)
  const isInstrumentChangedFromBinding = useRef<boolean>(false);
  const prevTimeframe = useRef<string>('');

  const lastIntervalChangeTime = useRef<number | null>(null);
  const timeDiffOfIntervalChangeTime = useRef<number | null>(null);

  const [savedTvWidget, setTvWidget] = useState<IChartingLibraryWidget | null>(null);
  /**
   * Копия chartIsReady тк некоторые useCallback хранят старое значение переменной.
   * Полностью не переходим на реф тк стейт используется в useEffect.
   * Рекомендуется использовать реф, за исключением если требуется срабатывать useEffect на chartIsReady.
   */
  const isChartIsReadyFromRef = useRef<boolean>(false);
  const [chartIsReady, setChartIsReadyFromUseState] = useState(false);
  const [isWidgetHeaderContextMenuOpen, setIsWidgetHeaderContextMenuOpen] = useState(false);

  const setChartIsReady = useCallback((isReady: boolean) => {
    setChartIsReadyFromUseState(isReady);
    isChartIsReadyFromRef.current = isReady;
  }, []);

  useEffect(() => {
    tvWidgetRef.current = savedTvWidget;
  }, [savedTvWidget]);

  const language = useAppSelect((state) => state.localisation.localisation);

  const { chartState } = useAppSelect((state) => state.widgets.widgets.find(({ id }) => id === widgetId))
    ?.widgetContentProps as WidgetProperties;

  // TODO временно реф из-за непредсказуемого изменения если используется useState,
  // нужен рефакторинг и вернуть обратно useState
  const currentInstrument = useRef<NonNullable<Contract['issKey']>>(
    (chartState?.savedInstrument ?? DEFAULT_OPTIONS.symbol) || '',
  );

  const { triggerRelatedWidgetsToUpdate, getMasterInstrumentFromPublicContext } = useWidgetsBind({
    widgetId,
  });

  // Функция работает внутри интервала, поэтому используем не хуковый доступ к стору
  const saveContentProps = (
    val: Partial<WidgetProperties['chartState']> & { withUpdate?: boolean; cleanIndicativeData?: boolean },
  ): void => {
    // если вызываем функцию с withUpdate, то оправляем в стор и делаем запрос на изменения
    const { withUpdate, cleanIndicativeData, ...chartStateVal } = val;
    const oldProps = (getState().widgets.widgets.find(({ id }) => id === widgetId) as Widget)?.widgetContentProps;

    if (oldProps) {
      dispatch(
        addContentPropsToWidget({
          id: widgetId,
          withoutSend: !withUpdate,
          widgetContentProps: {
            chartState: { ...oldProps.chartState, ...chartStateVal },
            // при смене инструмента очищаем индикативные данные, переданные из виджета Индикативные котировки
            indicativeData: cleanIndicativeData ? undefined : oldProps.indicativeData,
          },
        }),
      );
    }
  };

  const handlerSaveAsExcel = () => {
    const chart = savedTvWidget?.activeChart();
    const visibleRange = chart ? chart.getVisibleRange() : null;

    const checkedInterval = chart?.resolution();

    // Интервал из которого фактически будут формироваться свечи на графике
    const interval = mapResolutionIntervals((checkedInterval ?? DEFAULT_OPTIONS.interval) as keyof ResolutionMap);
    const timezoneTitle = tvWidgetRef.current?.activeChart().getTimezoneApi().getTimezone()?.title;
    let timezoneAsNumber;

    if (timezoneTitle === 'Биржа') {
      timezoneAsNumber = 3;
    } else if (timezoneTitle && timezoneTitle.includes(':')) {
      // если часовой пояс с дробным числом в формате UTC то по договоренности с бэком берем только целую часть
      timezoneAsNumber = Number(
        timezoneTitle
          .split('UTC' as string)[1]
          .split(')')[0]
          .split(':')[0],
      );
    } else {
      timezoneAsNumber = timezoneTitle ? Number(timezoneTitle.split('UTC')[1].split(')')[0]) : 3;
    }

    if (visibleRange) {
      const startDateTime = formatToDateTime(visibleRange.from);
      const endDateTime = formatToDateTime(visibleRange.to);

      const data: Candles = {
        secId: currentInstrument.current.replace(/:/g, '.'),
        from: startDateTime,
        till: endDateTime,
        interval: chartToReqTimeConverter(interval ?? DEFAULT_OPTIONS.interval),
        value: chartToReqTimeConverter(checkedInterval ?? DEFAULT_OPTIONS.interval),
        offset: timezoneAsNumber,
      };

      candlesController
        .upload(data)
        .then((response) => {
          // eslint-disable-next-line @typescript-eslint/no-shadow -- TODO исправить reasign
          const { data, headers } = response;

          const blob = new Blob([data]);
          const fileName = headers['content-disposition']?.match(/filename="(.+)"/)?.[1] || 'default_filename';

          const link = document.createElement('a');
          link.href = window.URL.createObjectURL(blob);
          link.download = fileName;
          link.click();
        })
        .catch((error) => {
          // eslint-disable-next-line no-console -- Обработка исключения
          console.error('Error uploading data:', error);
        });
    } else {
      // eslint-disable-next-line no-console -- Обработка исключения
      console.error('Chart is undefined or visible range is not available.');
    }
  };

  useEffect(() => {
    triggerRelatedWidgetsToUpdate(currentInstrument.current);

    const savedActiveChart = chartIsReady ? savedTvWidget?.activeChart?.() : false;

    if (!chartIsReady || !tvWidgetRef.current || !savedTvWidget || !savedActiveChart) {
      return;
    }

    if (currentInstrument.current !== savedActiveChart.symbol()) {
      savedActiveChart.setSymbol(currentInstrument.current);
    }
    // [currentInstrument.current] в зависимостях
    // т.к. здесь не нужен ререндер потому что достаточно прокинуть в TV новое значение
    // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO Разобраться с зависимостями
  }, [currentInstrument.current, chartIsReady]);

  const onInstrumentChangeCallback = useCallback((prevInstr: string, newInstr: string) => {
    if (prevInstr === newInstr || !isChartIsReadyFromRef.current) {
      return;
    }

    const newBoardInLowerCase = newInstr.split(':')[1]?.toLowerCase() ?? '';
    const prevBoardInLowerCase = prevInstr.split(':')[1]?.toLowerCase() ?? '';

    // для двух специфичных случаев точечно изменяем тип линии и таймфрейм
    if (
      ((prevInstr === DEFAULT_SYMBOL ||
        !DAY_LINE_ON_CHART_BOARDS.includes(prevBoardInLowerCase) ||
        !MINUTE_CANDLES_ON_CHART_BOARDS.includes(prevBoardInLowerCase)) &&
        DAY_LINE_ON_CHART_BOARDS.includes(newBoardInLowerCase)) ||
      (MINUTE_CANDLES_ON_CHART_BOARDS.includes(prevBoardInLowerCase) &&
        DAY_LINE_ON_CHART_BOARDS.includes(newBoardInLowerCase))
    ) {
      tvWidgetRef.current?.activeChart().setChartType(SeriesStyle.Line);
      tvWidgetRef.current?.activeChart().setResolution('1D' as ResolutionString);
    }

    if (
      MINUTE_CANDLES_ON_CHART_BOARDS.includes(newBoardInLowerCase) &&
      DAY_LINE_ON_CHART_BOARDS.includes(prevBoardInLowerCase)
    ) {
      tvWidgetRef.current?.activeChart().setChartType(SeriesStyle.Candles);
      tvWidgetRef.current?.activeChart().setResolution('1' as ResolutionString);
    }

    // для индикатива при выборе из Поиска инструментов и после перезагрузки устанавливаем
    // нужные настройки графика
    if (isIndicativeTicker(newInstr)) {
      tvWidgetRef.current?.activeChart().setChartType(SeriesStyle.Line);
      tvWidgetRef.current?.activeChart().setResolution('60' as ResolutionString);
    }
  }, []);

  const onInstrumentChange = useCallback(
    (newVal: string, withUpdate?: boolean, unbind = true): void => {
      const prev = currentInstrument.current;
      currentInstrument.current = newVal;

      if (unbind) {
        dispatch(unbindWidgets({ widgetId }));
      }

      // при смене инструмента очищаем индикативные данные виджета график
      // т.к. логика для графика индикатива построена на наличии в widgetContentProps данных indicativeData
      saveContentProps({ savedInstrument: newVal, withUpdate, cleanIndicativeData: true });
      setIsWidgetHeaderContextMenuOpen(false);
      onInstrumentChangeCallback(prev, newVal);
    },
    // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO Разобраться с зависимостями
    [onInstrumentChangeCallback, chartIsReady],
  );

  const addInstrumentFromModal = useCallback(
    (instruments: Pick<Contract, 'issKey'>[]) => {
      const instr = instruments[0];

      isInstrumentChangedFromBinding.current = false;

      if (instr.issKey !== null) {
        onInstrumentChange(instr.issKey);
      }
    },
    [onInstrumentChange],
  );

  const onInstrumentChangeFromBind = useCallback(
    (instrumentId: string) => {
      isInstrumentChangedFromBinding.current = true;
      onInstrumentChange(instrumentId, true, false);
    },
    [onInstrumentChange],
  );

  const onDropInstruments = useCallback((val: string, withUpdate?: boolean): void => {
    isInstrumentChangedFromBinding.current = false;
    onInstrumentChange(val, withUpdate);
    // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO Разобраться с зависимостями
  }, []);

  useChartPublicContext({
    setCurrInstrument: onInstrumentChangeFromBind,
    widgetId,
    getMasterInstrumentFromPublicContext,
  });

  const { datafeed, windowIntervalRequester } = useDatafeed(
    tvWidgetRef,
    prevTimeframe,
    timeDiffOfIntervalChangeTime,
    isInstrumentChangedFromBinding,
    indicativeData,
  );

  const createWidget = () => {
    if (!chartContainerRef.current) {
      return;
    }
    const todayData = new Date();
    const halfYearAgo = new Date();
    halfYearAgo.setMonth(halfYearAgo.getMonth() - 6);

    const threeMonthsAgo = new Date();
    threeMonthsAgo.setDate(threeMonthsAgo.getDate() - 90);
    const oneDayAgo = new Date();
    oneDayAgo.setDate(oneDayAgo.getDate() - 1);

    const thisYearStartDate = new Date();
    thisYearStartDate.setMonth(0);
    thisYearStartDate.setDate(1);

    const diffBetween = todayData.getTime() - thisYearStartDate.getTime();
    const dayDiff = diffBetween / 1000 / 3600 / 24;
    const minutesDiff = diffBetween / 1000 / 60;

    const ytlResolution = `${dayDiff > 0 ? dayDiff : minutesDiff}${dayDiff > 0 ? 'D' : ''}` as ResolutionString;

    const widgetOptions: ChartingLibraryWidgetOptions = {
      symbol: currentInstrument.current ?? chartState?.savedInstrument ?? (DEFAULT_OPTIONS.symbol as string),
      // BEWARE: no trailing slash is expected in feed URL
      // tslint:disable-next-line:no-any
      datafeed,
      interval: (chartState?.interval ?? DEFAULT_OPTIONS.interval) as ChartingLibraryWidgetOptions['interval'],
      container: chartContainerRef.current,
      library_path: DEFAULT_OPTIONS.libraryPath as string,
      locale: language,
      timezone: 'Europe/Moscow',
      charts_storage_url: DEFAULT_OPTIONS.chartsStorageUrl,
      charts_storage_api_version: DEFAULT_OPTIONS.chartsStorageApiVersion,
      client_id: DEFAULT_OPTIONS.clientId,
      time_frames: [
        { text: '1D', resolution: '60' as ResolutionString, description: '1d' },
        { text: '5d', resolution: '60' as ResolutionString, description: '5d' },
        { text: '1M', resolution: 'D' as ResolutionString, description: '1M' },
        { text: '6M', resolution: 'D' as ResolutionString, description: '6M' },
        {
          text: ytlResolution,
          resolution: '7D' as ResolutionString,
          description: 'YTL',
          title: 'YTD',
        },
        { text: '1Y', resolution: '7D' as ResolutionString, description: '6M' },
        { text: '5Y', resolution: '1M' as ResolutionString, description: '6M' },
        {
          text: '1000y',
          resolution: '7D' as ResolutionString,
          description: 'All',
          title: 'All',
        },
      ],

      user_id: DEFAULT_OPTIONS.userId,
      fullscreen: DEFAULT_OPTIONS.fullscreen,
      autosize: DEFAULT_OPTIONS.autosize,
      symbol_search_request_delay: 1000,
      enabled_features: ['study_templates', 'hide_left_toolbar_by_default', 'header_compare', 'header_indicators'],
      disabled_features: ['use_localstorage_for_settings'],
      custom_css_url: 'css/customstyles.css',
      theme: 'Dark',
      save_load_adapter: {
        charts: [],
        studyTemplates: [],
        drawingTemplates: [],
        getAllCharts() {
          return Promise.resolve(this.charts);
        },

        removeChart(id: number) {
          for (let i = 0; i < this.charts.length; i += 1) {
            if (this.charts[i].id === id) {
              this.charts.splice(i, 1);
              return Promise.resolve();
            }
          }

          return Promise.reject();
        },

        saveChart(chartData: Record<string, unknown>) {
          if (!chartData.id) {
            // eslint-disable-next-line no-param-reassign -- Ругается.
            chartData.id = Math.random().toString();
          } else {
            this.removeChart(chartData.id);
          }

          // eslint-disable-next-line no-param-reassign -- Ругается.
          chartData.timestamp = new Date().valueOf();

          this.charts.push(chartData);

          return Promise.resolve(chartData.id);
        },

        getChartContent(id: number) {
          for (let i = 0; i < this.charts.length; i += 1) {
            if (this.charts[i].id === id) {
              return Promise.resolve(this.charts[i].content);
            }
          }

          // eslint-disable-next-line no-console -- Обработка исключения
          console.error('error');

          return Promise.reject();
        },

        removeStudyTemplate(studyTemplateData: Record<string, unknown>) {
          for (let i = 0; i < this.studyTemplates.length; i += 1) {
            if (this.studyTemplates[i].name === studyTemplateData.name) {
              this.studyTemplates.splice(i, 1);
              return Promise.resolve();
            }
          }

          return Promise.reject();
        },

        getStudyTemplateContent(studyTemplateData: Record<string, unknown>) {
          for (let i = 0; i < this.studyTemplates.length; i += 1) {
            if (this.studyTemplates[i].name === studyTemplateData.name) {
              return Promise.resolve(this.studyTemplates[i]);
            }
          }

          // eslint-disable-next-line no-console -- Обработка исключения
          console.error('st: error');

          return Promise.reject();
        },

        saveStudyTemplate(studyTemplateData: Record<string, unknown>) {
          for (let i = 0; i < this.studyTemplates.length; i += 1) {
            if (this.studyTemplates[i].name === studyTemplateData.name) {
              this.studyTemplates.splice(i, 1);
              break;
            }
          }

          this.studyTemplates.push(studyTemplateData);
          return Promise.resolve();
        },

        getAllStudyTemplates() {
          return Promise.resolve(this.studyTemplates);
        },

        removeDrawingTemplate(toolName: string, templateName: string) {
          for (let i = 0; i < this.drawingTemplates.length; i += 1) {
            if (this.drawingTemplates[i].name === templateName) {
              this.drawingTemplates.splice(i, 1);
              return Promise.resolve();
            }
          }

          return Promise.reject();
        },

        loadDrawingTemplate(toolName: string, templateName: string) {
          for (let i = 0; i < this.drawingTemplates.length; i += 1) {
            if (this.drawingTemplates[i].name === templateName) {
              return Promise.resolve(this.drawingTemplates[i].content);
            }
          }

          // eslint-disable-next-line no-console -- Обработка исключения
          console.error('drawing: error');

          return Promise.reject();
        },

        saveDrawingTemplate(toolName: string, templateName: string, content: unknown) {
          for (let i = 0; i < this.drawingTemplates.length; i += 1) {
            if (this.drawingTemplates[i].name === templateName) {
              this.drawingTemplates.splice(i, 1);
              break;
            }
          }

          this.drawingTemplates.push({ name: templateName, content });
          return Promise.resolve();
        },

        getDrawingTemplates() {
          return Promise.resolve(this.drawingTemplates.map((template: Record<string, unknown>) => template.name));
        },
        // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Ругается.
      } as any,
      overrides: {
        'mainSeriesProperties.lineStyle.color': '#576DDB',
        'paneProperties.legendProperties.showSeriesTitle': false,
      },
    };
    if (chartState && chartState.savedData) {
      try {
        widgetOptions.saved_data = JSON.parse(chartState.savedData);
      } catch {
        widgetOptions.saved_data = undefined;
      }
    }

    // eslint-disable-next-line new-cap -- Ругается
    const tvWidget = new widget(widgetOptions);
    tvWidget.onChartReady(() => {
      setChartIsReady(true);
      tvWidget.chart().getTimezoneApi().setTimezone('Europe/Moscow');
      tvWidget
        ?.activeChart()
        ?.onIntervalChanged()
        ?.subscribe(null, (newInterval) => {
          const oldProps = (getState().widgets.widgets.find(({ id }) => id === widgetId) as Widget)?.widgetContentProps;
          prevTimeframe.current = oldProps.chartState.interval;

          const now = Date.now();

          if (lastIntervalChangeTime.current) {
            timeDiffOfIntervalChangeTime.current = now - lastIntervalChangeTime.current;
          }

          lastIntervalChangeTime.current = now;

          if (oldProps) {
            dispatch(
              addContentPropsToWidget({
                id: widgetId,
                widgetContentProps: {
                  chartState: {
                    ...oldProps.chartState,
                    interval: newInterval,
                  },
                },
              }),
            );
          }
        });
      // tvWidget.chart().getTimezoneApi().setTimezone("Europe/Moscow");

      tvWidget
        .activeChart()
        .onSymbolChanged()
        .subscribe(null, () => {
          // если инструмент из индикативныз котировок то его не надо отдельно созранять через onInstrumentChange
          if (indicativeData) {
            return;
          }

          if (currentInstrument.current === tvWidget.activeChart().symbol()) {
            return;
          }

          // если инструмент не из привязки то обновляем значение и отвязываем привязку
          if (!isInstrumentChangedFromBinding.current) {
            onInstrumentChange(tvWidget.activeChart().symbol(), false, true);
          }
        });
    });

    setTvWidget(tvWidget);

    const counter = setInterval(() => {
      if (!chartContainerRef.current || !tvWidgetRef.current) {
        if (!chartContainerRef.current) {
          clearInterval(counter);
        }
        return;
      }
      tvWidget?.save((state) => {
        if (chartPrevStateRef.current && !isEqual(state, chartPrevStateRef.current)) {
          chartPrevStateRef.current = state;
          saveContentProps({
            interval: tvWidget.symbolInterval().interval,
            savedData: JSON.stringify(state),
          });
        }
      });
    }, 1000);
  };

  const [dropDownOpen, setDropdownOpen] = useState(false);

  useEffect(() => {
    const unsubscribe = communicator.listen({ messageType: CORPACTIONS_OPEN_EXESTED_WIDGET_EVENT }, (message) => {
      const issKey = (message as Record<number, string>)[widgetId];
      if (issKey) {
        addInstrumentFromModal([{ issKey }]);
      }
    });
    const unsubscribeHighlighter = communicator.listen({ messageType: HIGHLIGHT_WIDGET_EVENT }, (message) => {
      const typedMessage = message as Record<number, boolean>;

      if (Object.keys(typedMessage).includes(String(widgetId))) {
        setIsOver(typedMessage[widgetId]);
      }
    });
    return () => {
      unsubscribe();
      unsubscribeHighlighter();
    };
  }, [addInstrumentFromModal, widgetId]);

  useEffect(
    () => () => {
      Object.values(windowIntervalRequester).forEach(clearInterval);
    },
    [windowIntervalRequester],
  );

  useEffect(
    () => () => {
      if (savedTvWidget !== null) {
        savedTvWidget.remove();
        setTvWidget(null);
      }
    },
    // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO Разобраться с зависимостями
    [],
  );

  useEffect(() => {
    if (savedTvWidget?.getLanguage() === language) {
      return;
    }
    createWidget();
    // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO Разобраться с зависимостями
  }, [language]);

  /** Переключение инструмента и вида графика при indicativeData */
  useEffect(() => {
    const savedActiveChart = chartIsReady ? savedTvWidget?.activeChart?.() : false;

    if (!chartIsReady || !tvWidgetRef.current || !savedTvWidget || !savedActiveChart) {
      return;
    }

    onInstrumentChangeCallback(
      chartState?.savedInstrument ?? (DEFAULT_OPTIONS.symbol as string),
      currentInstrument.current,
    );

    if (indicativeData) {
      savedActiveChart.setSymbol(indicativeData.key);
      savedActiveChart.setChartType(SeriesStyle.Line);
      savedActiveChart.setResolution('60' as ResolutionString);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO Разобраться с зависимостями
  }, [chartIsReady, savedTvWidget, indicativeData]);

  return {
    chartContainerRef,
    savedTvWidget,
    dropDownOpen,
    setDropdownOpen,
    currentInstrument,
    handlerSaveAsExcel,
    isWidgetHeaderContextMenuOpen,
    setIsWidgetHeaderContextMenuOpen,
    onDropInstruments,
    addInstrumentFromModal,
    isOver,
  };
}



declare global {
  interface Window {
    CONTEXT_PATH: string | undefined;
    STATIC_CONTEXT_PATH: string | undefined;
    FORMALIZATION_CONTEXT_PATH: string | undefined;
    CURRENT_BACKEND_HOST: string;
    CURRENT_BACKEND_REST_HOST: string;
    CURRENT_BACKEND_WS_HOST: string;
    WS_QUERY_ACCESS_TOKEN_ENABLED: 'true' | 'false';
    ROCKET_CHAT_URL: string;
    DISPLAY_MARKETING_NOTIFICATION: string | undefined;
    TIME_REPEAT_ANIMATION_CHATS: string | undefined;

    FEATURE_WEBPUSH: 'true' | 'false';
    WEBPUSH_PUBLIC_KEY: string | undefined;
    FEATURE_FLAG_1: string | undefined;
    FEATURE_FLAG_2: string | undefined;
    FEATURE_FLAG_3: string | undefined;

    SPFI_HOST: string;
    SPFI_AUTH_HOST: string;
  }
}

type UIConfig = {
  contextPath: string;
  staticContextPath: string;

  formalizationContextPath: string;
  api: UIConfigAPI;

  showMarketingNotification: boolean;
  timeRepeatChatAnimation: number;

  featureWebpush: boolean;
  webpushPublicKey: string;
  featureFlag1: string;
  featureFlag2: string;
  featureFlag3: string;
};

type UIConfigAPI = {
  wsHost: string;
  restHost: string;
  spfiHost: string;
  spfiAuthHost: string;
  wsQueryAccessTokenEnable: boolean;
};

interface IUIConfigManager {
  backendRestUrl: string;
  spfiHostUrl: string;
  spfiAuthHostUrl: string;
  backendWSUrl: string;
  needToAddTokenQueryParamForWS: boolean;
  contextPath: string;
  formalizationContextPath: string;

  staticContextPath: string;
  showMarketingNotification: boolean;
  timeRepeatChatAnimation: number;
  featureWebpushEnabled: boolean;
  webpushPublicKey: string;
  featureFlag1: string;
  featureFlag2: string;
  featureFlag3: string;
}

const extractStringParam = (property: keyof Window): string => {
  let value = window[property] ?? '';
  if (typeof value !== 'string') {
    value = value.toString();
  }
  return value;
};

/** Изоляция скрипта конфигурации и повышение надёжности */
class UIConfigManager implements IUIConfigManager {
  private config: UIConfig;

  private isLegacyConfig: boolean;

  private static safeExtractWSTokenInjectParamRaw() {
    return extractStringParam('WS_QUERY_ACCESS_TOKEN_ENABLED');
  }

  private static isLegacyAPIConfig() {
    const rawValue = UIConfigManager.safeExtractWSTokenInjectParamRaw();
    const isModernConfig = rawValue === 'true' || rawValue === 'false';
    return !isModernConfig;
  }

  private extractWSQueryInjectParam() {
    if (this.isLegacyConfig) {
      return false;
    }
    if (typeof window.WS_QUERY_ACCESS_TOKEN_ENABLED === 'undefined') {
      return false;
    }
    if (window.WS_QUERY_ACCESS_TOKEN_ENABLED === 'true') {
      return true;
    }
    return false;
  }

  private extractRestHost(): string {
    return this.isLegacyConfig ? '' : extractStringParam('CURRENT_BACKEND_REST_HOST');
  }

  private static extractSPFIHost(): string {
    return extractStringParam('SPFI_HOST');
  }

  private static extractSPFIAuthHost(): string {
    return extractStringParam('SPFI_AUTH_HOST');
  }

  private static extractFeatureWebPush(): boolean {
    const webpushEnabled = extractStringParam('FEATURE_WEBPUSH');
    return webpushEnabled === 'true';
  }

  private extractWSHost(): string {
    const key: keyof Window = this.isLegacyConfig ? 'CURRENT_BACKEND_HOST' : 'CURRENT_BACKEND_WS_HOST';
    return extractStringParam(key);
  }

  private extractAPIConfig(): UIConfigAPI {
    return {
      restHost: this.extractRestHost(),
      spfiHost: UIConfigManager.extractSPFIHost(),
      spfiAuthHost: UIConfigManager.extractSPFIAuthHost(),
      wsHost: this.extractWSHost(),
      wsQueryAccessTokenEnable: this.extractWSQueryInjectParam(),
    };
  }

  private extractVariables(): UIConfig {
    const showMarketingNotification = extractStringParam('DISPLAY_MARKETING_NOTIFICATION');
    return {
      api: this.extractAPIConfig(),
      contextPath: extractStringParam('CONTEXT_PATH'),
      formalizationContextPath: extractStringParam('FORMALIZATION_CONTEXT_PATH'),
      staticContextPath: extractStringParam('STATIC_CONTEXT_PATH'),
      showMarketingNotification: showMarketingNotification === 'true',
      timeRepeatChatAnimation: Number(extractStringParam('TIME_REPEAT_ANIMATION_CHATS')),
      featureWebpush: UIConfigManager.extractFeatureWebPush(),
      webpushPublicKey: extractStringParam('WEBPUSH_PUBLIC_KEY'),
      featureFlag1: extractStringParam('FEATURE_FLAG_1'),
      featureFlag2: extractStringParam('FEATURE_FLAG_1'),
      featureFlag3: extractStringParam('FEATURE_FLAG_1'),
    };
  }

  get backendRestUrl() {
    return this.config.api.restHost;
  }

  get spfiHostUrl() {
    return this.config.api.spfiHost;
  }

  get spfiAuthHostUrl() {
    return this.config.api.spfiAuthHost;
  }

  get backendWSUrl() {
    return this.config.api.wsHost;
  }

  get needToAddTokenQueryParamForWS() {
    return this.config.api.wsQueryAccessTokenEnable;
  }

  get contextPath() {
    return this.config.contextPath;
  }

  get formalizationContextPath() {
    return this.config.formalizationContextPath;
  }

  get staticContextPath() {
    return this.config.staticContextPath;
  }

  get showMarketingNotification() {
    return this.config.showMarketingNotification;
  }

  get timeRepeatAnimationChats() {
    return this.config.timeRepeatChatAnimation;
  }

  get timeRepeatChatAnimation() {
    return this.config.timeRepeatChatAnimation;
  }

  get featureWebpushEnabled() {
    return this.config.featureWebpush;
  }

  get webpushPublicKey() {
    return this.config.webpushPublicKey;
  }

  get featureFlag1() {
    return this.config.featureFlag1;
  }

  get featureFlag2() {
    return this.config.featureFlag2;
  }

  get featureFlag3() {
    return this.config.featureFlag3;
  }

  constructor() {
    this.isLegacyConfig = UIConfigManager.isLegacyAPIConfig();
    this.config = this.extractVariables();
  }
}

export const uiConfig: IUIConfigManager = new UIConfigManager();