Изменение OpenDesktopAppScreen

Created on: September 28, 2024

Answered using OpenAI 01 Preview by OpenAI01

Question

Исходный код:
src\components\BeforeStreamStartCheckScenario\screens\OpenDesktopAppScreen\OpenDesktopAppScreen.tsx
import { MouseEvent, useCallback, useEffect, useRef, useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';

import { trackUserAction } from '@wbnr/frontend-shared/lib/api';
import { DownloadAppButton } from '@wbnr/frontend-shared/lib/components/DownloadAppButton';
import { ModalJwtRequestFailed } from '@wbnr/frontend-shared/lib/components/ModalJwtRequestFailed';
import {
isChrome,
isChromiumEdge,
isFirefox,
isOpera,
isSafari,
isYaBrowser,
os,
OS_LINUX,
} from '@wbnr/frontend-utils/lib/browser';
import { appLocalStorage } from '@wbnr/frontend-utils/lib/browserStorage';
import {
A,
ActionDialog,
Button,
CircularProgress,
createTestIdProps,
DialogContentText,
} from '@wbnr/ui';

import { ChromeLink } from 'components/appLinks/ChromeLink';
import { EdgeLink } from 'components/appLinks/EdgeLink';
import { YandexBrowserLink } from 'components/appLinks/YandexBrowserLink';
import { InfoScreen } from 'components/Interface/InfoScreen';
import { FEATURE_KEYS } from 'constants/features';
import streamController from 'controllers/stream';
import { useTryOpenApp } from 'hooks/useTryOpenApp';
import { streamFeatureSelector, currentParticipationSelector } from 'store/selectors/common';
import { desktopAppForMyParticipationSelector } from 'store/selectors/desktop';
import { isOnboardingMeetingSelector, streamSessionSelector } from 'store/selectors/stream';
import { getDesktopAppFileName } from 'utils/getDesktopAppUrl';
import { useTrackWithMixPanel } from 'utils/track';

import infoImage from '../../../../images/illustration_059.png';
import Popover from '../../../Popovers/Popover';
import { NewOpenDesktopAppScreen } from '../NewOpenDesktopAppScreen';

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

const STORAGE_KEY = 'download_launch';

const DOWNLOADING_SCREEN_TIMEOUT = 5000;
const baseTestId = 'OpenDesktopAppScreen';

type Props = {
onOpen?: () => void;
trackingData?: {
reason: string;
error?: string;
};
notSupportedBrowser?: boolean;
};

export enum STATE {
LOADING = 'loading',
DOWNLOAD_APP = 'download_app',
APP_DOWNLOADED = 'app_downloaded',
LAUNCH = 'launch',
}

export const OpenDesktopAppScreen = ({ onOpen, trackingData, notSupportedBrowser }: Props) => {
const { t } = useTranslation();
const [state, setState] = useState(STATE.LOADING);
const [isHintVisible, setHintVisible] = useState(false);
const [isModalOpened, setModalOpened] = useState(false);
const [isJwtModalOpened, setJwtModalOpened] = useState(false);
const jwtErrorTextRef = useRef<string | null>(null);
const isOnboardingMeeting = useSelector(isOnboardingMeetingSelector);
const offerForAll = !notSupportedBrowser;
const features = useSelector(streamFeatureSelector);
const desktopAppForMyParticipation = useSelector(desktopAppForMyParticipationSelector);

const desktopDisableAutoDownload = features[FEATURE_KEYS.DESKTOP_DISABLED_AUTO_DOWNLOAD];
const { track: trackShowScreen } = useTrackWithMixPanel(
trackingData ? 'rtc_bad_browser_screen' : 'open_desktop_app_screen',
isOnboardingMeeting,
);
const { track: trackAction } = useTrackWithMixPanel(
trackingData ? 'rtc_bad_browser_screen_picked_option' : 'open_desktop_app_screen_action',
isOnboardingMeeting,
);
const participant = useSelector(currentParticipationSelector);
const hasParticipantToken = Boolean(participant?.token || participant?.url);
const { tryOpenApp, downloadApp: baseDownloadApp } = useTryOpenApp();
const {
id: eventSessionId,
event: { id: eventId },
} = useSelector(streamSessionSelector);

useEffect(() => {
if (!features[FEATURE_KEYS.WITHOUT_WEBROOM_INIT]) {
streamController.screenSharing.setIgnoreComet(true);
return () => {
streamController.screenSharing.setIgnoreComet(false);
};
}
}, [features]);

useEffect(() => {
trackShowScreen();
}, [trackShowScreen]);

useEffect(() => {
if (eventSessionId && eventId) {
trackUserAction({
action: offerForAll
? 'event_session_desktop_page'
: 'event_session_unsupported_page_entered',
eventSessionId,
eventId,
});
}
}, [eventSessionId, eventId, offerForAll]);

const handleAnalyticsPickOption = useCallback(
(type: string) => {
let actionName;
switch (type) {
case LinkType.CHROME:
actionName = 'event_session_unsupported_page_open_Chrome_clicked';
break;
case LinkType.EDGE:
actionName = 'event_session_unsupported_page_open_Edge_clicked';
break;
case LinkType.YANDEX:
actionName = 'event_session_unsupported_page_open_Yandex_clicked';
break;
case LinkType.MOBILE_APP:
actionName = 'event_session_unsupported_page_open_mobile_clicked';
break;
default:
break;
}
if (actionName) {
trackUserAction({
action: actionName,
eventSessionId,
eventId,
});
}
if (trackingData) {
const { reason, error } = trackingData;
trackAction({
reason,
error,
pickedOption: type,
});
} else {
trackAction({
action: type,
});
}
},
[eventId, eventSessionId, trackAction, trackingData],
);

const handleTryOpenApp = useCallback(
(onFailed: () => void) => {
return tryOpenApp(
onFailed,
() => {
setState(STATE.LAUNCH);
},
(link: string) => {
handleAnalyticsPickOption('not_supported');
// браузер не поддерживается в custom-protocol-check, пытаемся открыть приложение
window.location.href = link;
setState(STATE.LAUNCH);
},
handleJwtError,
);
},
[tryOpenApp, handleAnalyticsPickOption],
);

const handleJwtError = (error: string) => {
jwtErrorTextRef.current = error;
setJwtModalOpened(true);
};

const handleOpenApp = useCallback(() => {
if (hasParticipantToken || state === STATE.LAUNCH) {
handleAnalyticsPickOption('desktop_app');
handleTryOpenApp(() => {
setModalOpened(true);
});
setHintVisible(false);
trackUserAction({
action: offerForAll
? 'event_session_desktop_page_enter_event_clicked'
: 'event_session_unsupported_page_open_desktop_clicked',
eventSessionId,
eventId,
});
}
}, [
hasParticipantToken,
handleAnalyticsPickOption,
handleTryOpenApp,
offerForAll,
eventSessionId,
eventId,
state,
]);

const handleOpenInBrowser = () => {
handleAnalyticsPickOption('open_in_browser');
if (onOpen) {
onOpen();
}
};

const downloadApp = useCallback(() => {
baseDownloadApp();
handleAnalyticsPickOption('desktop_app_download');
appLocalStorage.setItem(STORAGE_KEY, `${Number(new Date())}`);
}, [baseDownloadApp, handleAnalyticsPickOption]);

const handleDownloadApp = useCallback(() => {
downloadApp();
setHintVisible(false);
trackUserAction({
action: offerForAll
? 'event_session_desktop_page_download_desktop_clicked '
: 'event_session_unsupported_page_download_desktop_clicked',
eventSessionId,
eventId,
});
}, [downloadApp, eventId, offerForAll, eventSessionId]);

useEffect(() => {
let timerId: number;
if (hasParticipantToken) {
handleTryOpenApp(() => {
if (os === OS_LINUX || desktopDisableAutoDownload) {
setState(STATE.LAUNCH);
return;
}

const lastDownload = appLocalStorage.getItem(STORAGE_KEY);
if (lastDownload) {
setState(STATE.LAUNCH);
} else {
setState(STATE.DOWNLOAD_APP);
if (desktopAppForMyParticipation) {
handleAnalyticsPickOption('desktop_app_auto_download');
setHintVisible(true);
downloadApp();
timerId = window.setTimeout(
() => setState(STATE.APP_DOWNLOADED),
DOWNLOADING_SCREEN_TIMEOUT,
);
}
}
});
} else {
setState(STATE.LAUNCH);
}
return () => {
if (timerId) {
clearTimeout(timerId);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hasParticipantToken, desktopDisableAutoDownload]);

const openInBrowser = notSupportedBrowser ? (
<BrowserNotSupportedMessage onClick={handleAnalyticsPickOption} />
) : (
<EventOpenInBrowserButton
onClick={() => {
trackUserAction({
action: 'event_session_desktop_page_download_enter_event_browser_clicked',
eventSessionId,
eventId,
});
handleOpenInBrowser();
}}
/>
);

return (
<>
<DownloadingHint open={isHintVisible} />
{!desktopAppForMyParticipation && (
<NewOpenDesktopAppScreen
handleOpenApp={handleOpenApp}
handleOpenInBrowser={() => {
trackUserAction({
action: 'event_session_desktop_page_download_enter_event_browser_clicked',
eventSessionId,
eventId,
});
handleOpenInBrowser();
}}
handleDownloadApp={handleDownloadApp}
state={state}
notSupportedBrowser={notSupportedBrowser}
/>
)}
{desktopAppForMyParticipation && (
<InfoScreen img={<img src={infoImage} alt="" />}>
<div className={styles.text}>
{state === STATE.LOADING && <CircularProgress />}
{state === STATE.DOWNLOAD_APP && (
<>
<p>
<CircularProgress />
</p>
<p className={styles.marked}>
<Trans i18nKey="openDesktopApp.appDownloading" />
</p>
<p className={styles.appInstalledText}>
<span className={styles.secondaryText}>
<Trans i18nKey="openDesktopApp.appInstalled" />
</span>
<EventOpenInAppButton
disabled={false}
onClick={handleOpenApp}
notSupportedBrowser={!offerForAll}
/>
</p>
</>
)}
{state === STATE.APP_DOWNLOADED && (
<>
<p>
<Trans i18nKey="openDesktopApp.appDownloaded" />{' '}
<span className={styles.marked}>
<Trans
i18nKey={`openDesktopApp.${
offerForAll ? '' : 'notSupported.'
}eventOpen`}
/>
</span>{' '}
</p>
<EventOpenInAppButton
disabled={!hasParticipantToken}
onClick={handleOpenApp}
notSupportedBrowser={!offerForAll}
/>
<DownloadAppButton
onClick={handleDownloadApp}
baseTestId={baseTestId}
/>
{openInBrowser}
</>
)}
{state === STATE.LAUNCH && (
<>
<p>
<Trans
i18nKey={`openDesktopApp.${
offerForAll ? '' : 'notSupported.'
}pressOpenApp`}
components={{
bold: <span className={styles.marked} />,
}}
/>
</p>
{offerForAll && (
<p>
<Trans i18nKey="openDesktopApp.textOpen" />{' '}
<span className={styles.marked}>
<Trans i18nKey="openDesktopApp.eventOpen" />
</span>{' '}
<Trans i18nKey="openDesktopApp.down" />
</p>
)}
<EventOpenInAppButton
disabled={false}
onClick={handleOpenApp}
notSupportedBrowser={!offerForAll}
/>
<DownloadAppButton
baseTestId={baseTestId}
onClick={handleDownloadApp}
/>
{openInBrowser}
</>
)}
</div>
</InfoScreen>
)}
<ActionDialog
containedSubmit
title={t('openDesktopApp.modal.title')}
opened={isModalOpened}
submitText={t('openDesktopApp.modal.downloadBtn')}
cancelText={t('openDesktopApp.modal.cancelBtn')}
onSubmit={() => {
downloadApp();
setModalOpened(false);
}}
onClose={() => setModalOpened(false)}
>
<DialogContentText>
<Trans i18nKey="openDesktopApp.modal.text" />
</DialogContentText>
</ActionDialog>
<ModalJwtRequestFailed
open={isJwtModalOpened}
onClose={() => setJwtModalOpened(false)}
errorText={jwtErrorTextRef.current}
notSupportedBrowser={notSupportedBrowser}
contentRenderer={
notSupportedBrowser ? (
<DownloadAppButton baseTestId={baseTestId} onClick={handleDownloadApp} />
) : (
<EventOpenInBrowserButton
onClick={() => {
setJwtModalOpened(false);
handleOpenInBrowser();
}}
showHintText={false}
/>
)
}
/>
</>
);
};

const EventOpenInAppButton = ({
disabled,
onClick,
notSupportedBrowser,
}: {
disabled: boolean;
onClick: (event: MouseEvent<HTMLButtonElement>) => void;
notSupportedBrowser?: boolean;
}) => (
<p>
<Button
disabled={disabled}
variant="contained"
color="primary"
className={styles.eventOpenButton}
onClick={onClick}
{...createTestIdProps(baseTestId, 'eventOpen')}
>
<Trans
i18nKey={`openDesktopApp.${notSupportedBrowser ? 'notSupported.' : ''}eventOpen`}
/>
</Button>
</p>
);

const EventOpenInBrowserButton = ({
onClick,
showHintText = true,
}: {
onClick: () => void;
showHintText?: boolean;
}) => (
<p className={styles.secondaryText}>
{showHintText && <Trans i18nKey="openDesktopApp.hasProblems" />}
<br />
<A
color="primary"
onClick={onClick}
className={styles.boldLink}
{...createTestIdProps(baseTestId, 'openInBrowser')}
>
<Trans i18nKey="openDesktopApp.openInBrowser" />
</A>
</p>
);

enum LinkType {
CHROME = 'chrome',
YANDEX = 'yandex',
EDGE = 'edge',
MOBILE_APP = 'mobile_app',
}

type BrowserNotSupportedMessageProps = {
onClick: (type: LinkType) => void;
};

const BrowserNotSupportedMessage = ({ onClick }: BrowserNotSupportedMessageProps) => (
<p className={styles.notSupported}>
<Trans
i18nKey="openDesktopApp.notSupported.text"
components={{
chrome: (
<ChromeLink
onClick={() => onClick(LinkType.CHROME)}
{...createTestIdProps(baseTestId, 'chrome')}
/>
),
edge: (
<EdgeLink
onClick={() => onClick(LinkType.EDGE)}
{...createTestIdProps(baseTestId, 'edge')}
/>
),
yandex: (
<YandexBrowserLink
onClick={() => onClick(LinkType.YANDEX)}
{...createTestIdProps(baseTestId, 'yandex')}
/>
),
mobile: (
<A
color="primary"
href="https://help.webinar.ru/ru/articles/1518961-участникам-что-нужно-для-участия-в-вебинаре"
className={styles.link}
onClick={() => onClick(LinkType.MOBILE_APP)}
target="_blank"
rel="noopener noreferrer"
{...createTestIdProps(baseTestId, 'mobile')}
/>
),
}}
/>
</p>
);

const bold = { components: { bold: <span className={styles.marked} /> } };

const DownloadingHintTextChrome = () => (
<>
<Trans
i18nKey="openDesktopApp.hint.chrome"
components={{ bold: <span className={styles.marked} /> }}
/>
<br />
<Trans
i18nKey="openDesktopApp.hint.openApp"
values={{
appFileName: getDesktopAppFileName(),
}}
components={{ bold: <span className={styles.marked} /> }}
/>
</>
);

const DownloadingHintText = () => {
const isDoubleClick = isSafari || isOpera;
return (
<>
<Trans
i18nKey="openDesktopApp.hint.pressDownload"
values={{
downloads: (
<span className={styles.marked}>
<Trans i18nKey="openDesktopApp.hint.downloads" />
</span>
),
}}
{...bold}
/>
<br />
<Trans
i18nKey={`openDesktopApp.hint.openApp${isDoubleClick ? 'Dbl' : ''}`}
values={{
appFileName: getDesktopAppFileName(),
}}
{...bold}
/>
</>
);
};

const DownloadingHint = ({ open }: { open: boolean }) => {
const hintRef = useRef<HTMLDivElement>(null);
let TextComponent;
if (isChrome) {
TextComponent = DownloadingHintTextChrome;
} else if (isFirefox || isChromiumEdge || isSafari || isOpera || isYaBrowser) {
TextComponent = DownloadingHintText;
}
if (!TextComponent) {
return null;
}
return (
<>
<Popover open={Boolean(open && hintRef.current)} anchorEl={hintRef.current}>
<TextComponent />
</Popover>
<div
ref={hintRef}
className={isChrome ? styles.hintDownloadingBottom : styles.hintDownloading}
/>
</>
);
};
src\utils\customProtocolCheck.ts
import { isChrome, isFirefox, isIE, isEdge, os, OS_MAC } from './browser';

type OpenUriFunction = (uri: string, failCb: () => void, successCb: () => void) => void;

let DEFAULT_CUSTOM_PROTOCOL_FAIL_CALLBACK_TIMEOUT = 1000;

const isOSX = os === OS_MAC;

const registerEvent = (target: Window, eventType: string, cb: () => void) => {
target.addEventListener(eventType, cb);
return {
remove: function () {
target.removeEventListener(eventType, cb);
},
};
};

const createHiddenIframe = (target: HTMLElement, uri: string) => {
const iframe = document.createElement('iframe');
iframe.src = uri;
iframe.id = 'hiddenIframe';
iframe.style.display = 'none';
target.appendChild(iframe);
return iframe;
};

const openUriWithHiddenFrame: OpenUriFunction = (uri, failCb, successCb) => {
const timeout = setTimeout(function () {
failCb();
handler.remove();
}, DEFAULT_CUSTOM_PROTOCOL_FAIL_CALLBACK_TIMEOUT);

let iframe = document.querySelector('#hiddenIframe');
if (!iframe) {
iframe = createHiddenIframe(document.body, 'about:blank');
}

const onBlur = () => {
clearTimeout(timeout);
handler.remove();
successCb();
};
const handler = registerEvent(window, 'blur', onBlur);

if (iframe instanceof HTMLIFrameElement && iframe?.contentWindow) {
iframe.contentWindow.location.href = uri;
}
};

const openUriWithTimeoutHack: OpenUriFunction = (uri, failCb, successCb) => {
const timeout = setTimeout(function () {
failCb();
handler.remove();
}, DEFAULT_CUSTOM_PROTOCOL_FAIL_CALLBACK_TIMEOUT);

let target: Window = window;
while (target.parent && target !== target.parent) {
target = target.parent;
}

const onBlur = () => {
clearTimeout(timeout);
handler.remove();
successCb();
};

const handler = registerEvent(target, 'blur', onBlur);

window.location.href = uri;
};

const openUriUsingFirefox: OpenUriFunction = (uri, failCb, successCb) => {
let iframe = document.querySelector('#hiddenIframe');

if (!iframe) {
iframe = createHiddenIframe(document.body, 'about:blank');
}

try {
if (iframe instanceof HTMLIFrameElement && iframe?.contentWindow) {
iframe.contentWindow.location.href = uri;
successCb();
}
} catch (error: unknown) {
if (error instanceof Error && error.name === 'NS_ERROR_UNKNOWN_PROTOCOL') {
failCb();
}
}
};

const openUriWithMsLaunchUri: OpenUriFunction = (uri, failCb, successCb) => {
const navigatorWithMsLaunchUri = navigator as Navigator & {
msLaunchUri?: (
uri: string,
successCallback?: () => void,
noHandlerCallback?: () => void,
) => void;
};
if (navigatorWithMsLaunchUri.msLaunchUri) {
navigatorWithMsLaunchUri.msLaunchUri(uri, successCb, failCb);
}
};

const getBrowserVersion = (): number => {
const ua = window.navigator.userAgent;
let tem,
M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];
if (/trident/i.test(M[1])) {
tem = /\brv[ :]+(\d+)/g.exec(ua) || [];
return parseFloat(tem[1]) || 0;
}
if (M[1] === 'Chrome') {
tem = ua.match(/\b(OPR|Edge)\/(\d+)/);
if (tem != null) {
return parseFloat(tem[2]);
}
}
M = M[2] ? [M[1], M[2]] : [window.navigator.appName, window.navigator.appVersion, '-?'];
if ((tem = ua.match(/version\/(\d+)/i)) != null) {
M.splice(1, 1, tem[1]);
}
return parseFloat(M[1]);
};

export const protocolCheck = (
uri: string,
failCb?: () => void,
successCb?: () => void,
timeout = 2000,
unsupportedCb?: () => void,
) => {
const failCallback = () => {
failCb && failCb();
};

const successCallback = () => {
successCb && successCb();
};

const unsupportedCallback = () => {
unsupportedCb && unsupportedCb();
};

const openUri = () => {
if (isFirefox) {
const browserVersion = getBrowserVersion();
if (browserVersion >= 64) {
openUriWithHiddenFrame(uri, failCallback, successCallback);
} else {
openUriUsingFirefox(uri, failCallback, successCallback);
}
} else if (isChrome) {
openUriWithTimeoutHack(uri, failCallback, successCallback);
} else if (isOSX) {
openUriWithHiddenFrame(uri, failCallback, successCallback);
} else {
unsupportedCallback();
}
};

if (timeout) {
DEFAULT_CUSTOM_PROTOCOL_FAIL_CALLBACK_TIMEOUT = timeout;
}

if (isEdge || isIE) {
openUriWithMsLaunchUri(uri, failCallback, successCallback);
} else {
openUri();
}
};
src\components\BeforeStreamStartCheckScenario\screens\NewOpenDesktopAppScreen\translations.ts
import { injectTranslations } from 'i18n';

const translations = {
ru: {
title: {
joinMeeting: 'Подключение к встрече',
joinWebinar: 'Подключение к мероприятию',
unsupportedBrowser: 'К сожалению, МТС Линк не работает в\nвашем браузере',
},
description: {
joinMeeting:
'Подключитесь к встрече с помощью приложения МТС Линк или общайтесь прямо в браузере',
joinWebinar:
'Подключитесь к мероприятию с помощью приложения МТС Линк или общайтесь прямо в браузере',
downloadApp:
'Скачайте и установите приложение МТС Линк для вашего компьютера с помощью кнопки ниже или воспользуйтесь такими браузерами, как Google Chrome, Яндекс Браузер или Microsoft Edge',
unsupportedBrowserMeeting:
'Присоединитесь к встрече с помощью приложения МТС Линк или воспользуйтесь такими браузерами, как Google Chrome, Яндекс Браузер или Microsoft Edge',
unsupportedBrowserWebinar:
'Присоединитесь к мероприятию с помощью приложения МТС Линк или воспользуйтесь такими браузерами, как Google Chrome, Яндекс Браузер или Microsoft Edge',
},
mainButton: {
openInApp: 'Присоединиться в приложении МТС ЛИНК',
downloadApp: 'Скачать приложение МТС Линк',
},
secondaryButton: {
openInBrowser: 'Продолжить в браузере',
},
helper: {
downloadApp:
'Если приложение не установлено, <downloadApp>скачайте его сейчас</downloadApp>',
openInApp:
'Если приложение уже установлено, <openInApp>откройте его сейчас</openInApp>',
},
downloadModal: {
meetingTitle: 'Открыть встречу в приложении',
webinarTitle: 'Открыть мероприятие в приложении',
firstStep: 'Дождитесь окончания загрузки приложения',
secondStep: 'Установите его',
thirdStepMeeting: '<openInApp>Присоединитесь</openInApp> к встрече',
thirdStepWebinar: '<openInApp>Присоединитесь</openInApp> к мероприятию',
},
},
en: {
title: {
joinMeeting: 'Connecting to the meeting',
joinWebinar: 'Connecting to the event',
unsupportedBrowser: "Unfortunately, MTS Link doesn't work in your browser",
},
description: {
joinMeeting:
'Connect to the meeting using the MTS Link app or communicate directly in your browser',
joinWebinar:
'Connect to the event using the MTS Link app or communicate directly in the browser',
downloadApp:
'Install the MTS Link app for your computer using the button below or use browsers like Google Chrome, Yandex Browser or Microsoft Edge',
unsupportedBrowserMeeting:
'Join the meeting using the MTS Link app or use browsers like Google Chrome, Yandex Browser or Microsoft Edge',
unsupportedBrowserWebinar:
'Join the event using the MTS Link app or use browsers like Google Chrome, Yandex Browser or Microsoft Edge',
},
mainButton: {
openInApp: 'Join in the MTS Link app',
downloadApp: 'Download the MTS Link app',
},
secondaryButton: {
openInBrowser: 'Continue in the browser',
},
helper: {
downloadApp:
"If you don't have the app installed, <downloadApp>download it now</downloadApp>",
openInApp: 'If the app is already installed, <openInApp>open it now</openInApp>',
},
downloadModal: {
meetingTitle: 'Open the meeting in the app',
webinarTitle: 'Open the event in the app',
firstStep: 'Wait for the app to finish downloading',
secondStep: 'Install it',
thirdStepMeeting: '<openInApp>Join</openInApp> the meeting',
thirdStepWebinar: '<openInApp>Join</openInApp> the event',
},
},
};

export const TRANS_NS = 'NewOpenDesktopAppScreen';

injectTranslations(TRANS_NS, translations);

src\components\BeforeStreamStartCheckScenario\screens\NewOpenDesktopAppScreen\NewOpenDesktopAppScreen.tsx
import { useEffect, useState, ReactNode, useCallback } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';

import { EventLandingTopbar } from '@wbnr/frontend-shared/lib/components/EventLandingView';
import { OS_LINUX, OS_MAC, OS_WIN, os } from '@wbnr/frontend-utils/lib/browser';
import { AppleIcon, LinuxIcon, WindowsIcon } from '@wbnr/icons';
import { A, Button, createTestIdProps, DarkTheme, Paper, Typography } from '@wbnr/ui';

import WaitLoad from 'components/WaitLoad';
import {
brandingInterfaceLogoLinkSelector,
brandingLandingLogoImageSelector,
} from 'store/selectors/branding';
import { isMeetingSelector } from 'store/selectors/stream';

import { STATE } from '../OpenDesktopAppScreen';

import { useLaunchMeetingInAppDialog } from './LaunchMeetingInAppDialog';
import styles from './NewOpenDesktopAppScreen.module.scss';
import { TRANS_NS } from './translations';

const baseTestId = 'NewOpenDesktopAppScreen';

type NewOpenDesktopAppScreenProps = {
handleOpenApp: () => void;
handleOpenInBrowser: () => void;
handleDownloadApp: () => void;
state: STATE;
notSupportedBrowser?: boolean;
};

type StateDescription = {
title: string;
description: string;
mainButton: string;
secondaryButton: string | null;
helper: string;
onMainButtonClick: () => void;
onSecondaryButtonClick: null | (() => void);
startIcon?: ReactNode;
};

export const NewOpenDesktopAppScreen = ({
handleOpenApp,
handleOpenInBrowser,
handleDownloadApp,
state,
notSupportedBrowser,
}: NewOpenDesktopAppScreenProps) => {
const { t } = useTranslation(TRANS_NS);
const [stateDescription, setStateDescription] = useState<StateDescription | null>(null);
const isMeeting = useSelector(isMeetingSelector);
const brandingLogoImage = useSelector(brandingLandingLogoImageSelector);
const brandingLogoUrl = useSelector(brandingInterfaceLogoLinkSelector);
const [openDialog] = useLaunchMeetingInAppDialog({ handleOpenApp });
const getBrowserIcon = () => {
if (os === OS_WIN) {
return <WindowsIcon />;
}
if (os === OS_MAC) {
return <AppleIcon />;
}
if (os === OS_LINUX) {
return <LinuxIcon />;
}
};

const onDownloadAppClick = useCallback(() => {
handleDownloadApp();
openDialog();
}, [handleDownloadApp, openDialog]);

useEffect(() => {
if (state === STATE.LAUNCH && notSupportedBrowser) {
setStateDescription({
title: 'title.unsupportedBrowser',
description: isMeeting
? 'description.unsupportedBrowserMeeting'
: 'description.unsupportedBrowserWebinar',
mainButton: 'mainButton.openInApp',
secondaryButton: null,
helper: 'helper.downloadApp',
onMainButtonClick: handleOpenApp,
onSecondaryButtonClick: null,
});
}
if (state === STATE.LAUNCH && !notSupportedBrowser) {
setStateDescription({
title: isMeeting ? 'title.joinMeeting' : 'title.joinWebinar',
description: isMeeting ? 'description.joinMeeting' : 'description.joinWebinar',
mainButton: 'mainButton.openInApp',
secondaryButton: 'secondaryButton.openInBrowser',
helper: 'helper.downloadApp',
onMainButtonClick: handleOpenApp,
onSecondaryButtonClick: handleOpenInBrowser,
});
}
if (state === STATE.DOWNLOAD_APP && notSupportedBrowser) {
setStateDescription({
title: 'title.unsupportedBrowser',
description: 'description.downloadApp',
mainButton: 'mainButton.downloadApp',
secondaryButton: null,
helper: 'helper.openInApp',
onMainButtonClick: onDownloadAppClick,
onSecondaryButtonClick: null,
startIcon: getBrowserIcon(),
});
}
if (state === STATE.DOWNLOAD_APP && !notSupportedBrowser) {
setStateDescription({
title: isMeeting ? 'title.joinMeeting' : 'title.joinWebinar',
description: isMeeting ? 'description.joinMeeting' : 'description.joinWebinar',
mainButton: 'mainButton.downloadApp',
secondaryButton: 'secondaryButton.openInBrowser',
helper: 'helper.openInApp',
onMainButtonClick: onDownloadAppClick,
onSecondaryButtonClick: handleOpenInBrowser,
startIcon: getBrowserIcon(),
});
}
}, [
state,
handleOpenApp,
handleOpenInBrowser,
onDownloadAppClick,
notSupportedBrowser,
isMeeting,
]);

return (
<WaitLoad loading={state === STATE.LOADING || !stateDescription}>
{() => (
<DarkTheme>
<EventLandingTopbar
logoUrl={brandingLogoUrl || 'https://mts-link.ru'}
logoImageUrl={brandingLogoImage}
/>
<div className={styles.wrapper}>
{stateDescription && (
<>
<Paper className={styles.paper}>
<Typography variant="h5">
{t(stateDescription.title)}
</Typography>
<Typography variant="subtitle2" className={styles.description}>
{t(stateDescription.description)}
</Typography>
<Button
variant="contained"
color="primary"
fullWidth
className={styles.mainButton}
onClick={stateDescription.onMainButtonClick}
startIcon={stateDescription.startIcon || null}
{...createTestIdProps(baseTestId, 'mainButton')}
>
{t(stateDescription.mainButton)}
</Button>
{stateDescription.secondaryButton &&
stateDescription.onSecondaryButtonClick && (
<Button
variant="text"
fullWidth
onClick={stateDescription.onSecondaryButtonClick}
{...createTestIdProps(
baseTestId,
'secondaryButton',
)}
>
{t(stateDescription.secondaryButton)}
</Button>
)}
</Paper>
<Typography variant="body2" className={styles.downloadAppText}>
<Trans
t={t}
i18nKey={t(stateDescription.helper)}
components={{
downloadApp: (
<A
onClick={handleDownloadApp}
className={styles.appLink}
{...createTestIdProps(
baseTestId,
'downloadApp',
)}
/>
),
openInApp: (
<A
onClick={handleOpenApp}
className={styles.appLink}
{...createTestIdProps(baseTestId, 'openInApp')}
/>
),
}}
/>
</Typography>
</>
)}
</div>
</DarkTheme>
)}
</WaitLoad>
);
};
src\components\BeforeStreamStartCheckScenario\screens\NewOpenDesktopAppScreen\LaunchMeetingInAppDialog\useLaunchMeetingInAppDialog.tsx
import { useCustomDialog } from '@wbnr/frontend-shared/lib/dialogs';

import { LaunchMeetingInAppDialog, DevicesAccessHelpDialogData } from './LaunchMeetingInAppDialog';

export const useLaunchMeetingInAppDialog = (defaultProps: DevicesAccessHelpDialogData) => {
return useCustomDialog(
({ opened, closeDialog, props }) => (
<LaunchMeetingInAppDialog opened={opened} onClose={closeDialog} {...props} />
),
defaultProps,
);
};
src\components\BeforeStreamStartCheckScenario\screens\NewOpenDesktopAppScreen\LaunchMeetingInAppDialog\LaunchMeetingInAppDialog.tsx
import { FC } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';

import { ActionDialogBase, Typography, Grid, A } from '@wbnr/ui';

import { isMeetingSelector } from 'store/selectors/stream';

import workTableImg from '../../../../../images/workTable.png';
import { TRANS_NS } from '../translations';

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

export interface DevicesAccessHelpDialogData {
handleOpenApp: () => void;
}

interface DevicesAccessHelpDialogProps extends DevicesAccessHelpDialogData {
opened: boolean;
onClose: () => void;
}

export const LaunchMeetingInAppDialog: FC<DevicesAccessHelpDialogProps> = ({
opened,
onClose,
handleOpenApp,
}) => {
const { t } = useTranslation(TRANS_NS);
const isMeeting = useSelector(isMeetingSelector);

return (
<ActionDialogBase maxWidth="lg" opened={opened} closeButton onClose={onClose}>
<div className={styles.container}>
<img className={styles.img} src={workTableImg} />
<Grid container direction="column" justifyContent="center">
<Typography variant={'h6'}>
{isMeeting
? t('downloadModal.meetingTitle')
: t('downloadModal.webinarTitle')}
</Typography>
<ol>
<li>
<Typography variant={'body1'}>
{t('downloadModal.firstStep')}
</Typography>
</li>
<li>
<Typography variant={'body1'}>
{t('downloadModal.secondStep')}
</Typography>
</li>
<li>
<Typography variant={'body1'}>
<Trans
t={t}
i18nKey={
isMeeting
? t('downloadModal.thirdStepMeeting')
: t('downloadModal.thirdStepWebinar')
}
components={{
openInApp: (
<A
onClick={handleOpenApp}
className={styles.downloadAppLink}
/>
),
}}
/>
</Typography>
</li>
</ol>
</Grid>
</div>
</ActionDialogBase>
);
};
import { useMemo, useEffect, useCallback } from 'react';

import { getUniqueDialogId } from '../getUniqueDialogId';
import { useCustomDialogCallbacks } from './useCustomDialogCallbacks';
import {
DialogDescription,
DialogRenderFunc,
OpenDialogOptions,
UseCustomDialogProps,
} from './types';

type OpenCustomDialog<P> = (props?: Partial<P>, options?: OpenDialogOptions) => void;
type CloseCustomDialog = () => void;

const DEFAULT_PROPS: DialogDescription['defaultProps'] = {};

export const useCustomDialog = <P extends UseCustomDialogProps>(
render: DialogRenderFunc<P>,
defaultProps?: Partial<P>,
) => {
const { registerDialog, unregisterDialog, openDialog, closeDialog } =
useCustomDialogCallbacks();

const id = useMemo(getUniqueDialogId, []);

useEffect(() => {
registerDialog({
id,
defaultProps: defaultProps || DEFAULT_PROPS,
render,
});
}, [defaultProps, id, registerDialog, render]);

useEffect(
() => () => {
unregisterDialog(id);
},
[unregisterDialog, id],
);

const openCustomDialog = useCallback<OpenCustomDialog<P>>(
(props, options) => {
openDialog(id, props, options);
},
[openDialog, id],
);

const closeCustomDialog = useCallback<CloseCustomDialog>(() => {
closeDialog(id);
}, [closeDialog, id]);

return [openCustomDialog, closeCustomDialog] as const;
};




Исправь логику для открытия модалки и инстукции. Мне нужно что бы инструция была отдельно. и снизу не было ничего, отдельный роут нельзя.

Вот как сейчас " import { MouseEvent, useCallback, useEffect, useRef, useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';

import { trackUserAction } from '@wbnr/frontend-shared/lib/api';
import { DownloadAppButton } from '@wbnr/frontend-shared/lib/components/DownloadAppButton';
import { ModalJwtRequestFailed } from '@wbnr/frontend-shared/lib/components/ModalJwtRequestFailed';
import {
isChrome,
isChromiumEdge,
isFirefox,
isOpera,
isSafari,
isYaBrowser,
os,
OS_LINUX,
} from '@wbnr/frontend-utils/lib/browser';
import { appLocalStorage } from '@wbnr/frontend-utils/lib/browserStorage';
import {
A,
ActionDialog,
Button,
CircularProgress,
createTestIdProps,
DialogContentText,
} from '@wbnr/ui';

import { ChromeLink } from 'components/appLinks/ChromeLink';
import { EdgeLink } from 'components/appLinks/EdgeLink';
import { YandexBrowserLink } from 'components/appLinks/YandexBrowserLink';
import { InstructionDialog } from 'components/BeforeStreamStartCheckScenario/screens/InstructionDialog/InstructionDialog';
import { InfoScreen } from 'components/Interface/InfoScreen';
import { FEATURE_KEYS } from 'constants/features';
import streamController from 'controllers/stream';
import { useTryOpenApp } from 'hooks/useTryOpenApp';
import { streamFeatureSelector, currentParticipationSelector } from 'store/selectors/common';
import { desktopAppForMyParticipationSelector } from 'store/selectors/desktop';
import { isOnboardingMeetingSelector, streamSessionSelector } from 'store/selectors/stream';
import { getDesktopAppFileName } from 'utils/getDesktopAppUrl';
import { useTrackWithMixPanel } from 'utils/track';

import infoImage from '../../../../images/illustration_059.png';
import Popover from '../../../Popovers/Popover';
import { NewOpenDesktopAppScreen } from '../NewOpenDesktopAppScreen';

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

const STORAGE_KEY = 'download_launch';

const DOWNLOADING_SCREEN_TIMEOUT = 5000;
const baseTestId = 'OpenDesktopAppScreen';

type Props = {
onOpen?: () => void;
trackingData?: {
reason: string;
error?: string;
};
notSupportedBrowser?: boolean;
};

export enum STATE {
LOADING = 'loading',
DOWNLOAD_APP = 'download_app',
APP_DOWNLOADED = 'app_downloaded',
LAUNCH = 'launch',
}

export const OpenDesktopAppScreen = ({ onOpen, trackingData, notSupportedBrowser }: Props) => {
const { t } = useTranslation();
const [state, setState] = useState(STATE.LOADING);
const [isHintVisible, setHintVisible] = useState(false);
const [isModalOpened, setModalOpened] = useState(false);
const [isJwtModalOpened, setJwtModalOpened] = useState(false);
const [showInstructionScreen, setShowInstructionScreen] = useState(false);
const jwtErrorTextRef = useRef<string | null>(null);
const isOnboardingMeeting = useSelector(isOnboardingMeetingSelector);
const offerForAll = !notSupportedBrowser;
const features = useSelector(streamFeatureSelector);
const desktopAppForMyParticipation = useSelector(desktopAppForMyParticipationSelector);

const desktopDisableAutoDownload = features[FEATURE_KEYS.DESKTOP_DISABLED_AUTO_DOWNLOAD];
const { track: trackShowScreen } = useTrackWithMixPanel(
trackingData ? 'rtc_bad_browser_screen' : 'open_desktop_app_screen',
isOnboardingMeeting,
);
const { track: trackAction } = useTrackWithMixPanel(
trackingData ? 'rtc_bad_browser_screen_picked_option' : 'open_desktop_app_screen_action',
isOnboardingMeeting,
);
const participant = useSelector(currentParticipationSelector);
const hasParticipantToken = Boolean(participant?.token || participant?.url);
const { tryOpenApp, downloadApp: baseDownloadApp } = useTryOpenApp();
const {
id: eventSessionId,
event: { id: eventId },
} = useSelector(streamSessionSelector);

useEffect(() => {
if (!features[FEATURE_KEYS.WITHOUT_WEBROOM_INIT]) {
streamController.screenSharing.setIgnoreComet(true);
return () => {
streamController.screenSharing.setIgnoreComet(false);
};
}
}, [features]);

useEffect(() => {
trackShowScreen();
}, [trackShowScreen]);

useEffect(() => {
if (eventSessionId && eventId) {
trackUserAction({
action: offerForAll
? 'event_session_desktop_page'
: 'event_session_unsupported_page_entered',
eventSessionId,
eventId,
});
}
}, [eventSessionId, eventId, offerForAll]);

const handleAnalyticsPickOption = useCallback(
(type: string) => {
let actionName;
switch (type) {
case LinkType.CHROME:
actionName = 'event_session_unsupported_page_open_Chrome_clicked';
break;
case LinkType.EDGE:
actionName = 'event_session_unsupported_page_open_Edge_clicked';
break;
case LinkType.YANDEX:
actionName = 'event_session_unsupported_page_open_Yandex_clicked';
break;
case LinkType.MOBILE_APP:
actionName = 'event_session_unsupported_page_open_mobile_clicked';
break;
default:
break;
}
if (actionName) {
trackUserAction({
action: actionName,
eventSessionId,
eventId,
});
}
if (trackingData) {
const { reason, error } = trackingData;
trackAction({
reason,
error,
pickedOption: type,
});
} else {
trackAction({
action: type,
});
}
},
[eventId, eventSessionId, trackAction, trackingData],
);

const handleTryOpenApp = useCallback(
(onFailed: () => void) => {
return tryOpenApp(
onFailed,
() => {
setState(STATE.LAUNCH);
},
(link: string) => {
handleAnalyticsPickOption('not_supported');
// браузер не поддерживается в custom-protocol-check, пытаемся открыть приложение
window.location.href = link;
setState(STATE.LAUNCH);
},
handleJwtError,
);
},
[tryOpenApp, handleAnalyticsPickOption],
);

const handleJwtError = (error: string) => {
jwtErrorTextRef.current = error;
setJwtModalOpened(true);
};

const handleOpenApp = useCallback(() => {
if (hasParticipantToken || state === STATE.LAUNCH) {
handleAnalyticsPickOption('desktop_app');
handleTryOpenApp(() => {
setModalOpened(true);
});
setHintVisible(false);
trackUserAction({
action: offerForAll
? 'event_session_desktop_page_enter_event_clicked'
: 'event_session_unsupported_page_open_desktop_clicked',
eventSessionId,
eventId,
});
}
}, [
hasParticipantToken,
handleAnalyticsPickOption,
handleTryOpenApp,
offerForAll,
eventSessionId,
eventId,
state,
]);

const handleOpenInBrowser = () => {
handleAnalyticsPickOption('open_in_browser');
if (onOpen) {
onOpen();
}
};

const downloadApp = useCallback(() => {
baseDownloadApp();
handleAnalyticsPickOption('desktop_app_download');
appLocalStorage.setItem(STORAGE_KEY, `${Number(new Date())}`);
}, [baseDownloadApp, handleAnalyticsPickOption]);

const handleDownloadApp = useCallback(() => {
downloadApp();
setHintVisible(false);
trackUserAction({
action: offerForAll
? 'event_session_desktop_page_download_desktop_clicked '
: 'event_session_unsupported_page_download_desktop_clicked',
eventSessionId,
eventId,
});
}, [downloadApp, eventId, offerForAll, eventSessionId]);

useEffect(() => {
let timerId: number;
if (hasParticipantToken) {
handleTryOpenApp(() => {
if (os === OS_LINUX || desktopDisableAutoDownload) {
setState(STATE.LAUNCH);
return;
}

const lastDownload = appLocalStorage.getItem(STORAGE_KEY);
if (lastDownload) {
setState(STATE.LAUNCH);
} else {
setState(STATE.DOWNLOAD_APP);
if (desktopAppForMyParticipation) {
handleAnalyticsPickOption('desktop_app_auto_download');
setHintVisible(true);
downloadApp();
timerId = window.setTimeout(
() => setState(STATE.APP_DOWNLOADED),
DOWNLOADING_SCREEN_TIMEOUT,
);
}
}
});
} else {
setState(STATE.LAUNCH);
}
return () => {
if (timerId) {
clearTimeout(timerId);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hasParticipantToken, desktopDisableAutoDownload]);

const openInBrowser = notSupportedBrowser ? (
<BrowserNotSupportedMessage onClick={handleAnalyticsPickOption} />
) : (
<EventOpenInBrowserButton
onClick={() => {
trackUserAction({
action: 'event_session_desktop_page_download_enter_event_browser_clicked',
eventSessionId,
eventId,
});
handleOpenInBrowser();
}}
/>
);

return (
<>
<DownloadingHint open={isHintVisible} />
{!desktopAppForMyParticipation && (
<NewOpenDesktopAppScreen
handleOpenApp={handleOpenApp}
handleOpenInBrowser={() => {
trackUserAction({
action: 'event_session_desktop_page_download_enter_event_browser_clicked',
eventSessionId,
eventId,
});
handleOpenInBrowser();
}}
handleDownloadApp={handleDownloadApp}
state={state}
notSupportedBrowser={notSupportedBrowser}
/>
)}
{desktopAppForMyParticipation && (
<InfoScreen img={<img src={infoImage} alt="" />}>
<div className={styles.text}>
{state === STATE.LOADING && <CircularProgress />}
{state === STATE.DOWNLOAD_APP && (
<>
<p>
<CircularProgress />
</p>
<p className={styles.marked}>
<Trans i18nKey="openDesktopApp.appDownloading" />
</p>
<p className={styles.appInstalledText}>
<span className={styles.secondaryText}>
<Trans i18nKey="openDesktopApp.appInstalled" />
</span>
<EventOpenInAppButton
disabled={false}
onClick={handleOpenApp}
notSupportedBrowser={!offerForAll}
/>
</p>
</>
)}
{state === STATE.APP_DOWNLOADED && (
<>
<p>
<Trans i18nKey="openDesktopApp.appDownloaded" />{' '}
<span className={styles.marked}>
<Trans
i18nKey={`openDesktopApp.${
offerForAll ? '' : 'notSupported.'
}eventOpen`}
/>
</span>{' '}
</p>
<EventOpenInAppButton
disabled={!hasParticipantToken}
onClick={handleOpenApp}
notSupportedBrowser={!offerForAll}
/>
<DownloadAppButton
onClick={handleDownloadApp}
baseTestId={baseTestId}
/>
{openInBrowser}
</>
)}
{state === STATE.LAUNCH && (
<>
<p>
<Trans
i18nKey={`openDesktopApp.${
offerForAll ? '' : 'notSupported.'
}pressOpenApp`}
components={{
bold: <span className={styles.marked} />,
}}
/>
</p>
{offerForAll && (
<p>
<Trans i18nKey="openDesktopApp.textOpen" />{' '}
<span className={styles.marked}>
<Trans i18nKey="openDesktopApp.eventOpen" />
</span>{' '}
<Trans i18nKey="openDesktopApp.down" />
</p>
)}
<EventOpenInAppButton
disabled={false}
onClick={handleOpenApp}
notSupportedBrowser={!offerForAll}
/>
<DownloadAppButton
baseTestId={baseTestId}
onClick={handleDownloadApp}
/>
{openInBrowser}
</>
)}
</div>
</InfoScreen>
)}
{!showInstructionScreen ? (
<ActionDialog
containedSubmit
title="Приложение МТС Линк не установлено"
opened={isModalOpened}
submitText="Скачать приложение"
cancelText="Отмена"
classes={{ dialog: styles.ActionDialog }}
onSubmit={() => {
downloadApp();
setModalOpened(false);
setShowInstructionScreen(true);
}}
onClose={() => setModalOpened(false)}
>
<DialogContentText>Скачайте и установите приложение МТС Линк</DialogContentText>
</ActionDialog>
) : (
<InstructionDialog
opened={showInstructionScreen}
onClose={() => setShowInstructionScreen(false)}
onJoinViaApp={handleOpenApp}
onBack={() => setShowInstructionScreen(false)}
/>
)}

{/* <ActionDialog
containedSubmit
title={t('openDesktopApp.modal.title')}
opened={isModalOpened}
submitText={t('openDesktopApp.modal.downloadBtn')}
cancelText={t('openDesktopApp.modal.cancelBtn')}
classes={{ dialog: styles.ActionDialog }}
onSubmit={() => {
downloadApp();
setModalOpened(false);
}}
onClose={() => setModalOpened(false)}
>
<DialogContentText>
<Trans i18nKey="openDesktopApp.modal.text" />
</DialogContentText>
</ActionDialog> */}
<ModalJwtRequestFailed
open={isJwtModalOpened}
onClose={() => setJwtModalOpened(false)}
errorText={jwtErrorTextRef.current}
notSupportedBrowser={notSupportedBrowser}
contentRenderer={
notSupportedBrowser ? (
<DownloadAppButton baseTestId={baseTestId} onClick={handleDownloadApp} />
) : (
<EventOpenInBrowserButton
onClick={() => {
setJwtModalOpened(false);
handleOpenInBrowser();
}}
showHintText={false}
/>
)
}
/>
</>
);
};

const EventOpenInAppButton = ({
disabled,
onClick,
notSupportedBrowser,
}: {
disabled: boolean;
onClick: (event: MouseEvent<HTMLButtonElement>) => void;
notSupportedBrowser?: boolean;
}) => (
<p>
<Button
disabled={disabled}
variant="contained"
color="primary"
className={styles.eventOpenButton}
onClick={onClick}
{...createTestIdProps(baseTestId, 'eventOpen')}
>
<Trans
i18nKey={`openDesktopApp.${notSupportedBrowser ? 'notSupported.' : ''}eventOpen`}
/>
</Button>
</p>
);

const EventOpenInBrowserButton = ({
onClick,
showHintText = true,
}: {
onClick: () => void;
showHintText?: boolean;
}) => (
<p className={styles.secondaryText}>
{showHintText && <Trans i18nKey="openDesktopApp.hasProblems" />}
<br />
<A
color="primary"
onClick={onClick}
className={styles.boldLink}
{...createTestIdProps(baseTestId, 'openInBrowser')}
>
<Trans i18nKey="openDesktopApp.openInBrowser" />
</A>
</p>
);

enum LinkType {
CHROME = 'chrome',
YANDEX = 'yandex',
EDGE = 'edge',
MOBILE_APP = 'mobile_app',
}

type BrowserNotSupportedMessageProps = {
onClick: (type: LinkType) => void;
};

const BrowserNotSupportedMessage = ({ onClick }: BrowserNotSupportedMessageProps) => (
<p className={styles.notSupported}>
<Trans
i18nKey="openDesktopApp.notSupported.text"
components={{
chrome: (
<ChromeLink
onClick={() => onClick(LinkType.CHROME)}
{...createTestIdProps(baseTestId, 'chrome')}
/>
),
edge: (
<EdgeLink
onClick={() => onClick(LinkType.EDGE)}
{...createTestIdProps(baseTestId, 'edge')}
/>
),
yandex: (
<YandexBrowserLink
onClick={() => onClick(LinkType.YANDEX)}
{...createTestIdProps(baseTestId, 'yandex')}
/>
),
mobile: (
<A
color="primary"
href="https://help.webinar.ru/ru/articles/1518961-участникам-что-нужно-для-участия-в-вебинаре"
className={styles.link}
onClick={() => onClick(LinkType.MOBILE_APP)}
target="_blank"
rel="noopener noreferrer"
{...createTestIdProps(baseTestId, 'mobile')}
/>
),
}}
/>
</p>
);

const bold = { components: { bold: <span className={styles.marked} /> } };

const DownloadingHintTextChrome = () => (
<>
<Trans
i18nKey="openDesktopApp.hint.chrome"
components={{ bold: <span className={styles.marked} /> }}
/>
<br />
<Trans
i18nKey="openDesktopApp.hint.openApp"
values={{
appFileName: getDesktopAppFileName(),
}}
components={{ bold: <span className={styles.marked} /> }}
/>
</>
);

const DownloadingHintText = () => {
const isDoubleClick = isSafari || isOpera;
return (
<>
<Trans
i18nKey="openDesktopApp.hint.pressDownload"
values={{
downloads: (
<span className={styles.marked}>
<Trans i18nKey="openDesktopApp.hint.downloads" />
</span>
),
}}
{...bold}
/>
<br />
<Trans
i18nKey={`openDesktopApp.hint.openApp${isDoubleClick ? 'Dbl' : ''}`}
values={{
appFileName: getDesktopAppFileName(),
}}
{...bold}
/>
</>
);
};

const DownloadingHint = ({ open }: { open: boolean }) => {
const hintRef = useRef<HTMLDivElement>(null);
let TextComponent;
if (isChrome) {
TextComponent = DownloadingHintTextChrome;
} else if (isFirefox || isChromiumEdge || isSafari || isOpera || isYaBrowser) {
TextComponent = DownloadingHintText;
}
if (!TextComponent) {
return null;
}
return (
<>
<Popover open={Boolean(open && hintRef.current)} anchorEl={hintRef.current}>
<TextComponent />
</Popover>
<div
ref={hintRef}
className={isChrome ? styles.hintDownloadingBottom : styles.hintDownloading}
/>
</>
);
};
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Typography } from '@wbnr/ui';

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

export const InstructionDialog = ({ opened, onClose, onJoinViaApp, onBack }) => (
<Dialog
open={opened}
onClose={onClose}
classes={{ paper: styles.instructionDialog }}
fullWidth
maxWidth="sm"
>
<DialogTitle className={styles.instructionDialogTitle}>
Запуск встречи в приложении
</DialogTitle>
<DialogContent className={styles.instructionDialogContent}>
<Typography variant="body1" className={styles.instructionDialogText}>
Дождитесь окончания загрузки приложения
</Typography>
<Typography variant="body1" className={styles.instructionDialogText}>
Установите его
</Typography>
<Typography variant="body1" className={styles.instructionDialogText}>
Присоединитесь к встрече через приложение
</Typography>
<Typography variant="body1" className={styles.instructionDialogText}>
Рекомендуем присоединиться к встрече через браузер, если вы опаздываете
</Typography>
</DialogContent>
<DialogActions className={styles.instructionDialogActions}>
<Button
variant="contained"
color="primary"
onClick={onJoinViaApp}
className={styles.joinViaAppButton}
>
Присоединиться через приложение
</Button>
<Button variant="text" onClick={onBack} className={styles.backButton}>
Назад
</Button>
</DialogActions>
</Dialog>
);
"

Thought

Редактирование логики

Я исправляю логику открытия модального окна и инструкции, выделяя инструкцию в отдельный элемент без добавления дополнительной секции.

Изучаю изменения

Оригинальный код включает действие и инструкцию в одной модалке, а в измененном коде адаптирует инструкцию в отдельной модалке и добавляет кнопку на основе состояния видимости.

Пересматриваю логику

Я разбираюсь в логике открытия модального окна и инструкции, чтобы отделить их и устранить необходимость использования отдельного маршрута.

Изучаю детали

Пользователь хочет, чтобы инструкционные сообщения отображались как полноэкранный наложение или настраиваемое состояние, чтобы все остальное исчезло.

Изменение логики рендеринга

Я думаю о модификации кода для отображения диалога инструкции, скрывая остальной контент. Это требует изменений в функции рендера компонента OpenDesktopAppScreen.

Уточняю условия

Я работаю над оптимизацией функции рендеринга, чтобы при true showInstructionScreen показывался только компонент InstructionDialog, исключая остальное содержимое.

Обновление условий отображения

Интересно, что если showInstructionScreen активирован, отображается только InstructionDialog, без лишнего контента, а isModalOpened должен быть выключен.

Ищу возможность

Открываю Render функцию OpenDesktopAppScreen, чтобы отобразить только InstructionDialog, когда showInstructionScreen true.

Проверяю отображение инструкции. Если showInstructionScreen активен, показываю InstructionDialog. Using Dialog component to cover content.

Фокусирую внимание

Предлагаю изменить компонент OpenDesktopAppScreen так, чтобы когда showInstructionScreen активен, показывался только InstructionDialog.

Добавляю диалоги

Окей, я добавляю диалоги для скачивания приложения и ошибок JWT, чтобы улучшить пользовательский опыт.

Answer

Конечно, вы можете изменить логику компонента OpenDesktopAppScreen таким образом, чтобы при отображении инструкции не было ничего ниже, и инструкция была отдельной. Для этого вы можете использовать ранний возврат в вашем компоненте, чтобы при определенном состоянии отображалась только инструкция.

Вот как вы можете изменить код:

export const OpenDesktopAppScreen = ({ onOpen, trackingData, notSupportedBrowser }: Props) => { // ... ваш существующий код if (showInstructionScreen) { return ( <InstructionDialog opened={showInstructionScreen} onClose={() => setShowInstructionScreen(false)} onJoinViaApp={handleOpenApp} onBack={() => setShowInstructionScreen(false)} /> ); } return ( <> <DownloadingHint open={isHintVisible} /> {!desktopAppForMyParticipation && ( <NewOpenDesktopAppScreen handleOpenApp={handleOpenApp} handleOpenInBrowser={() => { trackUserAction({ action: 'event_session_desktop_page_download_enter_event_browser_clicked', eventSessionId, eventId, }); handleOpenInBrowser(); }} handleDownloadApp={handleDownloadApp} state={state} notSupportedBrowser={notSupportedBrowser} /> )} {desktopAppForMyParticipation && ( <InfoScreen img={<img src={infoImage} alt="" />}> {/* ... существующее содержимое */} </InfoScreen> )} <ActionDialog containedSubmit title="Приложение МТС Линк не установлено" opened={isModalOpened} submitText="Скачать приложение" cancelText="Отмена" classes={{ dialog: styles.ActionDialog }} onSubmit={() => { downloadApp(); setModalOpened(false); setShowInstructionScreen(true); }} onClose={() => setModalOpened(false)} > <DialogContentText>Скачайте и установите приложение МТС Линк</DialogContentText> </ActionDialog> <ModalJwtRequestFailed open={isJwtModalOpened} onClose={() => setJwtModalOpened(false)} errorText={jwtErrorTextRef.current} notSupportedBrowser={notSupportedBrowser} contentRenderer={ notSupportedBrowser ? ( <DownloadAppButton baseTestId={baseTestId} onClick={handleDownloadApp} /> ) : ( <EventOpenInBrowserButton onClick={() => { setJwtModalOpened(false); handleOpenInBrowser(); }} showHintText={false} /> ) } /> </> ); };

Объяснение изменений:

  1. Ранний возврат инструкции: Добавив проверку if (showInstructionScreen), вы можете сделать так, чтобы при значении true для showInstructionScreen компонент сразу возвращал только InstructionDialog, и остальная часть компонента не рендерилась.

  2. Удаление условного рендеринга инструкции из JSX: Поскольку мы теперь возвращаем InstructionDialog отдельно при определенном состоянии, можно убрать его из основного JSX кода в конце компонента.

  3. Обновление логики открытия инструкции: В обработчике onSubmit для ActionDialog, после скачивания приложения и закрытия модального окна, мы устанавливаем setShowInstructionScreen(true), что переключит компонент в состояние отображения инструкции.

Таким образом, при установке showInstructionScreen в true, компонент OpenDesktopAppScreen будет отображать только инструкцию, и под ней не будет ничего лишнего. Это решает вашу задачу без необходимости использования отдельного маршрута.

Пример обновленного компонента InstructionDialog:

import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Typography } from '@wbnr/ui'; import styles from './InstructionDialog.module.scss'; export const InstructionDialog = ({ opened, onClose, onJoinViaApp, onBack }) => ( <Dialog open={opened} onClose={onClose} classes={{ paper: styles.instructionDialog }} fullWidth maxWidth="sm" > <DialogTitle className={styles.instructionDialogTitle}> Запуск встречи в приложении </DialogTitle> <DialogContent className={styles.instructionDialogContent}> <Typography variant="body1" className={styles.instructionDialogText}> Дождитесь окончания загрузки приложения </Typography> <Typography variant="body1" className={styles.instructionDialogText}> Установите его </Typography> <Typography variant="body1" className={styles.instructionDialogText}> Присоединитесь к встрече через приложение </Typography> <Typography variant="body1" className={styles.instructionDialogText}> Рекомендуем присоединиться к встрече через браузер, если вы опаздываете </Typography> </DialogContent> <DialogActions className={styles.instructionDialogActions}> <Button variant="contained" color="primary" onClick={onJoinViaApp} className={styles.joinViaAppButton} > Присоединиться через приложение </Button> <Button variant="text" onClick={onBack} className={styles.backButton}> Назад </Button> </DialogActions> </Dialog> );

Теперь ваша инструкция будет отображаться отдельно, без элементов под ней, и логика открытия модального окна и инструкции исправлена в соответствии с вашими требованиями.

Share this Q&A