diff --git a/dailyjs/cloud-recording/.babelrc b/dailyjs/cloud-recording/.babelrc new file mode 100644 index 0000000..a6f4434 --- /dev/null +++ b/dailyjs/cloud-recording/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["next/babel"], + "plugins": ["inline-react-svg"] +} diff --git a/dailyjs/cloud-recording/README.md b/dailyjs/cloud-recording/README.md new file mode 100644 index 0000000..c5e7974 --- /dev/null +++ b/dailyjs/cloud-recording/README.md @@ -0,0 +1,41 @@ +# Live Streaming + +![Live Streaming](./image.png) + +### Live example + +**[See it in action here ➡️](https://dailyjs-live-streaming.vercel.app)** + +--- + +## What does this demo do? + +- Use [startLiveStreaming](https://docs.daily.co/reference#%EF%B8%8F-startlivestreaming) to send video and audio to specified RTMP endpoint +- Listen for stream started / stopped / error events +- Allows call owner to specify stream layout (grid, single participant or active speaker) and maximum cams +- Extends the basic call demo with a live streaming provider, tray button and modal +- Show a notification bubble at the top of the screen when live streaming is in progress + +Please note: this demo is not currently mobile optimised + +### Getting started + +``` +# set both DAILY_API_KEY and DAILY_DOMAIN +mv env.example .env.local + +yarn +yarn workspace @dailyjs/live-streaming dev +``` + +## How does this example work? + +In this example we extend the [basic call demo](../basic-call) with live streaming functionality. + +We pass a custom tray object, a custom app object (wrapping the original in a new `LiveStreamingProvider`) and a custom modal. We also symlink both the `public` and `pages/api` folders from the basic call. + +Single live streaming is only available to call owners, you must create a token when joining the call (for simplicity, we have disabled the abiltiy to join the call as a guest.) + +## Deploy your own on Vercel + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/daily-co/clone-flow?repository-url=https%3A%2F%2Fgithub.com%2Fdaily-demos%2Fexamples.git&env=DAILY_DOMAIN%2CDAILY_API_KEY&envDescription=Your%20Daily%20domain%20and%20API%20key%20can%20be%20found%20on%20your%20account%20dashboard&envLink=https%3A%2F%2Fdashboard.daily.co&project-name=daily-examples&repo-name=daily-examples) diff --git a/dailyjs/cloud-recording/components/App/App.js b/dailyjs/cloud-recording/components/App/App.js new file mode 100644 index 0000000..393d2ad --- /dev/null +++ b/dailyjs/cloud-recording/components/App/App.js @@ -0,0 +1,13 @@ +import React from 'react'; + +import App from '@dailyjs/basic-call/components/App'; +import { RecordingProvider } from '../../contexts/RecordingProvider'; + +// Extend our basic call app component with the recording context +export const AppWithRecording = () => ( + + + +); + +export default AppWithRecording; diff --git a/dailyjs/cloud-recording/components/App/index.js b/dailyjs/cloud-recording/components/App/index.js new file mode 100644 index 0000000..82a89dc --- /dev/null +++ b/dailyjs/cloud-recording/components/App/index.js @@ -0,0 +1 @@ +export { AppWithRecording as default } from './App'; diff --git a/dailyjs/cloud-recording/components/RecordingModal/RecordingModal.js b/dailyjs/cloud-recording/components/RecordingModal/RecordingModal.js new file mode 100644 index 0000000..27d96e1 --- /dev/null +++ b/dailyjs/cloud-recording/components/RecordingModal/RecordingModal.js @@ -0,0 +1,124 @@ +import React, { useEffect } from 'react'; +import { Button } from '@dailyjs/shared/components/Button'; +import { CardBody } from '@dailyjs/shared/components/Card'; +import Modal from '@dailyjs/shared/components/Modal'; +import Well from '@dailyjs/shared/components/Well'; +import { useCallState } from '@dailyjs/shared/contexts/CallProvider'; +import { useUIState } from '@dailyjs/shared/contexts/UIStateProvider'; +import { enable } from 'debug'; +import { + RECORDING_COUNTDOWN_1, + RECORDING_COUNTDOWN_2, + RECORDING_COUNTDOWN_3, + RECORDING_ERROR, + RECORDING_IDLE, + RECORDING_RECORDING, + RECORDING_SAVED, + RECORDING_TYPE_CLOUD, + RECORDING_UPLOADING, + useRecording, +} from '../../contexts/RecordingProvider'; + +export const RECORDING_MODAL = 'recording'; + +export const RecordingModal = () => { + const { currentModals, closeModal } = useUIState(); + const { enableRecording } = useCallState(); + const { + recordingStartedDate, + recordingState, + startRecordingWithCountdown, + stopRecording, + } = useRecording(); + + useEffect(() => { + if (recordingState === RECORDING_RECORDING) { + closeModal(RECORDING_MODAL); + } + }, [recordingState, closeModal]); + + const disabled = + enableRecording && + [RECORDING_IDLE, RECORDING_RECORDING].includes(recordingState); + + function renderButtonLabel() { + if (!enableRecording) { + return 'Recording disabled'; + } + + switch (recordingState) { + case RECORDING_COUNTDOWN_3: + return '3...'; + case RECORDING_COUNTDOWN_2: + return '2...'; + case RECORDING_COUNTDOWN_1: + return '1...'; + case RECORDING_RECORDING: + return 'Stop recording'; + case RECORDING_UPLOADING: + case RECORDING_SAVED: + return 'Stopping recording...'; + default: + return 'Start recording'; + } + } + + function handleRecordingClick() { + if (recordingState === RECORDING_IDLE) { + startRecordingWithCountdown(); + } else { + stopRecording(); + } + } + + return ( + closeModal(RECORDING_MODAL)} + actions={[ + , + , + ]} + > + + {!enableRecording ? ( + + Recording is not enabled for this room (or your browser does not + support it.) Please enabled recording when creating the room or via + the Daily dashboard. + + ) : ( +

+ Recording type enabled: {enableRecording} +

+ )} + + {recordingStartedDate && ( +

Recording started: {recordingStartedDate.toString()}

+ )} + + {enableRecording === RECORDING_TYPE_CLOUD && ( + <> +
+ +

+ Cloud recordings can be accessed via the Daily dashboard under the + "Recordings" section. +

+ + )} +
+
+ ); +}; + +export default RecordingModal; diff --git a/dailyjs/cloud-recording/components/RecordingModal/index.js b/dailyjs/cloud-recording/components/RecordingModal/index.js new file mode 100644 index 0000000..0a0deca --- /dev/null +++ b/dailyjs/cloud-recording/components/RecordingModal/index.js @@ -0,0 +1,3 @@ +export { RecordingModal as default } from './RecordingModal'; +export { RecordingModal } from './RecordingModal'; +export { RECORDING_MODAL } from './RecordingModal'; diff --git a/dailyjs/cloud-recording/components/Tray/Tray.js b/dailyjs/cloud-recording/components/Tray/Tray.js new file mode 100644 index 0000000..c26f253 --- /dev/null +++ b/dailyjs/cloud-recording/components/Tray/Tray.js @@ -0,0 +1,47 @@ +import React, { useEffect } from 'react'; + +import { TrayButton } from '@dailyjs/shared/components/Tray'; +import { useUIState } from '@dailyjs/shared/contexts/UIStateProvider'; +import { ReactComponent as IconRecord } from '@dailyjs/shared/icons/record-md.svg'; + +import { + RECORDING_ERROR, + RECORDING_RECORDING, + RECORDING_SAVED, + RECORDING_UPLOADING, + useRecording, +} from '../../contexts/RecordingProvider'; +import { RECORDING_MODAL } from '../RecordingModal'; + +export const Tray = () => { + const { openModal } = useUIState(); + const { recordingState } = useRecording(); + + useEffect(() => { + console.log(`⏺️ Recording state: ${recordingState}`); + + if (recordingState === RECORDING_ERROR) { + // show error modal here + } + }, [recordingState]); + + const isRecording = [ + RECORDING_RECORDING, + RECORDING_UPLOADING, + RECORDING_SAVED, + ].includes(recordingState); + + return ( + <> + openModal(RECORDING_MODAL)} + > + + + + ); +}; + +export default Tray; diff --git a/dailyjs/cloud-recording/components/Tray/index.js b/dailyjs/cloud-recording/components/Tray/index.js new file mode 100644 index 0000000..100bcc8 --- /dev/null +++ b/dailyjs/cloud-recording/components/Tray/index.js @@ -0,0 +1 @@ +export { Tray as default } from './Tray'; diff --git a/dailyjs/cloud-recording/contexts/RecordingProvider.js b/dailyjs/cloud-recording/contexts/RecordingProvider.js new file mode 100644 index 0000000..e56a3f0 --- /dev/null +++ b/dailyjs/cloud-recording/contexts/RecordingProvider.js @@ -0,0 +1,304 @@ +import React, { + createContext, + useCallback, + useContext, + useEffect, + useState, +} from 'react'; + +import { useCallState } from '@dailyjs/shared/contexts/CallProvider'; +import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider'; +import { useUIState } from '@dailyjs/shared/contexts/UIStateProvider'; +import { + CALL_STATE_REDIRECTING, + CALL_STATE_JOINED, +} from '@dailyjs/shared/contexts/useCallMachine'; +import PropTypes from 'prop-types'; +import { useDeepCompareEffect } from 'use-deep-compare'; + +export const RECORDING_ERROR = 'error'; +export const RECORDING_SAVED = 'saved'; +export const RECORDING_RECORDING = 'recording'; +export const RECORDING_UPLOADING = 'uploading'; +export const RECORDING_COUNTDOWN_1 = 'starting1'; +export const RECORDING_COUNTDOWN_2 = 'starting2'; +export const RECORDING_COUNTDOWN_3 = 'starting3'; +export const RECORDING_IDLE = 'idle'; + +export const RECORDING_TYPE_CLOUD = 'cloud'; +export const RECORDING_TYPE_LOCAL = 'local'; + +const RecordingContext = createContext({ + isRecordingLocally: false, + recordingStartedDate: null, + recordingState: RECORDING_IDLE, + startRecording: null, + stopRecording: null, +}); + +export const RecordingProvider = ({ children }) => { + const { callObject, enableRecording, startCloudRecording, state } = + useCallState(); + const { participants } = useParticipants(); + const [recordingStartedDate, setRecordingStartedDate] = useState(null); + const [recordingState, setRecordingState] = useState(RECORDING_IDLE); + const [isRecordingLocally, setIsRecordingLocally] = useState(false); + const [hasRecordingStarted, setHasRecordingStarted] = useState(false); + const { setCustomCapsule } = useUIState(); + + const handleOnUnload = useCallback( + () => 'Unsaved recording in progress. Do you really want to leave?', + [] + ); + + useEffect(() => { + if ( + !enableRecording || + !isRecordingLocally || + recordingState !== RECORDING_RECORDING || + state === CALL_STATE_REDIRECTING + ) + return false; + const prev = window.onbeforeunload; + window.onbeforeunload = handleOnUnload; + return () => { + window.onbeforeunload = prev; + }; + }, [ + enableRecording, + handleOnUnload, + recordingState, + isRecordingLocally, + state, + ]); + + useEffect(() => { + if (!callObject || !enableRecording) return false; + + const handleAppMessage = (ev) => { + switch (ev?.data?.event) { + case 'recording-starting': + setRecordingState(RECORDING_COUNTDOWN_3); + break; + default: + break; + } + }; + + const handleRecordingUploadCompleted = () => { + setRecordingState(RECORDING_SAVED); + }; + + callObject.on('app-message', handleAppMessage); + callObject.on('recording-upload-completed', handleRecordingUploadCompleted); + + return () => { + callObject.off('app-message', handleAppMessage); + callObject.off( + 'recording-upload-completed', + handleRecordingUploadCompleted + ); + }; + }, [callObject, enableRecording]); + + /** + * Automatically start cloud recording, if startCloudRecording is set. + */ + useEffect(() => { + if ( + hasRecordingStarted || + !callObject || + !startCloudRecording || + enableRecording !== 'cloud' || + state !== CALL_STATE_JOINED + ) + return false; + + // Small timeout, in case other participants are already in-call. + const timeout = setTimeout(() => { + const isSomebodyRecording = participants.some((p) => p.isRecording); + if (!isSomebodyRecording) { + callObject.startRecording(); + setIsRecordingLocally(true); + setHasRecordingStarted(true); + } else { + setHasRecordingStarted(true); + } + }, 500); + + return () => { + clearTimeout(timeout); + }; + }, [ + callObject, + enableRecording, + hasRecordingStarted, + participants, + startCloudRecording, + state, + ]); + + /** + * Handle participant updates to sync recording state. + */ + useDeepCompareEffect(() => { + if (isRecordingLocally || recordingState === RECORDING_SAVED) return; + if (participants.some(({ isRecording }) => isRecording)) { + setRecordingState(RECORDING_RECORDING); + } else { + setRecordingState(RECORDING_IDLE); + } + }, [isRecordingLocally, participants, recordingState]); + + /** + * Handle recording started. + */ + const handleRecordingStarted = useCallback( + (event) => { + if (recordingState === RECORDING_RECORDING) return; + if (event.local) { + // Recording started locally, either through UI or programmatically + setIsRecordingLocally(true); + if (!recordingStartedDate) setRecordingStartedDate(new Date()); + } + setRecordingState(RECORDING_RECORDING); + }, + [recordingState, recordingStartedDate] + ); + + useEffect(() => { + if (!callObject || !enableRecording) return false; + + callObject.on('recording-started', handleRecordingStarted); + return () => callObject.off('recording-started', handleRecordingStarted); + }, [callObject, enableRecording, handleRecordingStarted]); + + /** + * Handle recording stopped. + */ + useEffect(() => { + if (!callObject || !enableRecording) return false; + + const handleRecordingStopped = () => { + if (isRecordingLocally) return; + setRecordingState(RECORDING_IDLE); + setRecordingStartedDate(null); + }; + + callObject.on('recording-stopped', handleRecordingStopped); + return () => callObject.off('recording-stopped', handleRecordingStopped); + }, [callObject, enableRecording, isRecordingLocally]); + + /** + * Handle recording error. + */ + const handleRecordingError = useCallback(() => { + if (isRecordingLocally) setRecordingState(RECORDING_ERROR); + setIsRecordingLocally(false); + }, [isRecordingLocally]); + + useEffect(() => { + if (!callObject || !enableRecording) return false; + + callObject.on('recording-error', handleRecordingError); + return () => callObject.off('recording-error', handleRecordingError); + }, [callObject, enableRecording, handleRecordingError]); + + const startRecording = useCallback(() => { + if (!callObject || !isRecordingLocally) return; + callObject.startRecording(); + }, [callObject, isRecordingLocally]); + + useEffect(() => { + let timeout; + switch (recordingState) { + case RECORDING_COUNTDOWN_3: + timeout = setTimeout(() => { + setRecordingState(RECORDING_COUNTDOWN_2); + }, 1000); + break; + case RECORDING_COUNTDOWN_2: + timeout = setTimeout(() => { + setRecordingState(RECORDING_COUNTDOWN_1); + }, 1000); + break; + case RECORDING_COUNTDOWN_1: + startRecording(); + break; + case RECORDING_ERROR: + case RECORDING_SAVED: + timeout = setTimeout(() => { + setRecordingState(RECORDING_IDLE); + setIsRecordingLocally(false); + }, 5000); + break; + default: + break; + } + return () => { + clearTimeout(timeout); + }; + }, [recordingState, startRecording]); + + // Show a custom capsule when recording in progress + useEffect(() => { + if (recordingState !== RECORDING_RECORDING) { + setCustomCapsule(null); + } else { + setCustomCapsule({ variant: 'recording', label: 'Recording' }); + } + }, [recordingState, setCustomCapsule]); + + const startRecordingWithCountdown = useCallback(() => { + if (!callObject || !enableRecording) return; + setIsRecordingLocally(true); + setRecordingState(RECORDING_COUNTDOWN_3); + callObject?.sendAppMessage({ + event: 'recording-starting', + }); + }, [callObject, enableRecording]); + + const stopRecording = useCallback(() => { + if (!callObject || !enableRecording || !isRecordingLocally) return; + if (recordingState === RECORDING_RECORDING) { + switch (enableRecording) { + case RECORDING_TYPE_LOCAL: + setRecordingState(RECORDING_SAVED); + setIsRecordingLocally(false); + break; + case RECORDING_TYPE_CLOUD: + setRecordingState(RECORDING_UPLOADING); + break; + default: + break; + } + } else if (recordingState === RECORDING_IDLE) { + return; + } else { + setIsRecordingLocally(false); + setRecordingState(RECORDING_IDLE); + } + setRecordingStartedDate(null); + callObject.stopRecording(); + }, [callObject, enableRecording, isRecordingLocally, recordingState]); + + return ( + + {children} + + ); +}; + +RecordingProvider.propTypes = { + children: PropTypes.node, +}; + +export const useRecording = () => useContext(RecordingContext); diff --git a/dailyjs/cloud-recording/env.example b/dailyjs/cloud-recording/env.example new file mode 100644 index 0000000..5ab7e03 --- /dev/null +++ b/dailyjs/cloud-recording/env.example @@ -0,0 +1,8 @@ +# Domain excluding 'https://' and 'daily.co' e.g. 'somedomain' +DAILY_DOMAIN= + +# Obtained from https://dashboard.daily.co/developers +DAILY_API_KEY= + +# Daily REST API endpoint +DAILY_REST_DOMAIN=https://api.daily.co/v1 diff --git a/dailyjs/cloud-recording/image.png b/dailyjs/cloud-recording/image.png new file mode 100644 index 0000000..9781261 Binary files /dev/null and b/dailyjs/cloud-recording/image.png differ diff --git a/dailyjs/cloud-recording/next.config.js b/dailyjs/cloud-recording/next.config.js new file mode 100644 index 0000000..9a0a6ee --- /dev/null +++ b/dailyjs/cloud-recording/next.config.js @@ -0,0 +1,13 @@ +const withPlugins = require('next-compose-plugins'); +const withTM = require('next-transpile-modules')([ + '@dailyjs/shared', + '@dailyjs/basic-call', +]); + +const packageJson = require('./package.json'); + +module.exports = withPlugins([withTM], { + env: { + PROJECT_TITLE: packageJson.description, + }, +}); diff --git a/dailyjs/cloud-recording/package.json b/dailyjs/cloud-recording/package.json new file mode 100644 index 0000000..8949ed8 --- /dev/null +++ b/dailyjs/cloud-recording/package.json @@ -0,0 +1,25 @@ +{ + "name": "@dailyjs/cloud-recording", + "description": "Basic Call + Cloud Recording", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@dailyjs/shared": "*", + "@dailyjs/basic-call": "*", + "next": "^11.0.0", + "pluralize": "^8.0.0", + "react": "^17.0.2", + "react-dom": "^17.0.2" + }, + "devDependencies": { + "babel-plugin-module-resolver": "^4.1.0", + "next-compose-plugins": "^2.2.1", + "next-transpile-modules": "^8.0.0" + } +} diff --git a/dailyjs/cloud-recording/pages/_app.js b/dailyjs/cloud-recording/pages/_app.js new file mode 100644 index 0000000..df733a8 --- /dev/null +++ b/dailyjs/cloud-recording/pages/_app.js @@ -0,0 +1,12 @@ +import React from 'react'; +import App from '@dailyjs/basic-call/pages/_app'; +import AppWithRecording from '../components/App'; + +import { RecordingModal } from '../components/RecordingModal'; +import Tray from '../components/Tray'; + +App.modals = [RecordingModal]; +App.customAppComponent = ; +App.customTrayComponent = ; + +export default App; diff --git a/dailyjs/cloud-recording/pages/api b/dailyjs/cloud-recording/pages/api new file mode 120000 index 0000000..999f604 --- /dev/null +++ b/dailyjs/cloud-recording/pages/api @@ -0,0 +1 @@ +../../basic-call/pages/api \ No newline at end of file diff --git a/dailyjs/cloud-recording/pages/index.js b/dailyjs/cloud-recording/pages/index.js new file mode 100644 index 0000000..2668138 --- /dev/null +++ b/dailyjs/cloud-recording/pages/index.js @@ -0,0 +1,16 @@ +import Index from '@dailyjs/basic-call/pages'; +import getDemoProps from '@dailyjs/shared/lib/demoProps'; + +export async function getStaticProps() { + const defaultProps = getDemoProps(); + + return { + props: { + ...defaultProps, + forceFetchToken: true, + forceOwner: true, + }, + }; +} + +export default Index; diff --git a/dailyjs/cloud-recording/public b/dailyjs/cloud-recording/public new file mode 120000 index 0000000..33a6e67 --- /dev/null +++ b/dailyjs/cloud-recording/public @@ -0,0 +1 @@ +../basic-call/public \ No newline at end of file diff --git a/dailyjs/shared/contexts/CallProvider.js b/dailyjs/shared/contexts/CallProvider.js index b884666..a24d3f4 100644 --- a/dailyjs/shared/contexts/CallProvider.js +++ b/dailyjs/shared/contexts/CallProvider.js @@ -12,6 +12,7 @@ import React, { useEffect, useState, } from 'react'; +import Bowser from 'bowser'; import PropTypes from 'prop-types'; import { ACCESS_STATE_LOBBY, @@ -31,6 +32,8 @@ export const CallProvider = ({ }) => { const [videoQuality, setVideoQuality] = useState(VIDEO_QUALITY_AUTO); const [preJoinNonAuthorized, setPreJoinNonAuthorized] = useState(false); + const [enableRecording, setEnableRecording] = useState(null); + const [startCloudRecording, setStartCloudRecording] = useState(false); // Daily CallMachine hook (primarily handles status of the call) const { daily, leave, state, setRedirectOnLeave } = useCallMachine({ @@ -40,6 +43,32 @@ export const CallProvider = ({ subscribeToTracksAutomatically, }); + // Feature detection taken from daily room object and client browser support + useEffect(() => { + if (!daily) return; + const updateRoomConfigState = async () => { + const roomConfig = await daily.room(); + if (!('config' in roomConfig)) return; + + const browser = Bowser.parse(window.navigator.userAgent); + const supportsRecording = + browser.platform.type === 'desktop' && browser.engine.name === 'Blink'; + // recording and screen sharing is hidden in owner_only_broadcast for non-owners + if (supportsRecording) { + const recordingType = + roomConfig?.tokenConfig?.enable_recording ?? + roomConfig?.config?.enable_recording; + if (['local', 'cloud'].includes(recordingType)) { + setEnableRecording(recordingType); + setStartCloudRecording( + roomConfig?.tokenConfig?.start_cloud_recording ?? false + ); + } + } + }; + updateRoomConfigState(); + }, [state, daily]); + // Convience wrapper for adding a fake participant to the call const addFakeParticipant = useCallback(() => { daily.addFakeParticipant(); @@ -72,9 +101,11 @@ export const CallProvider = ({ preJoinNonAuthorized, leave, videoQuality, + enableRecording, setVideoQuality, setBandwidth, setRedirectOnLeave, + startCloudRecording, subscribeToTracksAutomatically, }} > diff --git a/dailyjs/shared/icons/record-md.svg b/dailyjs/shared/icons/record-md.svg new file mode 100644 index 0000000..e4fdad5 --- /dev/null +++ b/dailyjs/shared/icons/record-md.svg @@ -0,0 +1,4 @@ + + + + diff --git a/dailyjs/shared/package.json b/dailyjs/shared/package.json index 10cf3b9..ed9c59a 100644 --- a/dailyjs/shared/package.json +++ b/dailyjs/shared/package.json @@ -5,6 +5,7 @@ "main": "index.js", "dependencies": { "@daily-co/daily-js": "^0.15.0", + "bowser": "^2.11.0", "classnames": "^2.3.1", "debounce": "^1.2.1", "nanoid": "^3.1.23", diff --git a/yarn.lock b/yarn.lock index 6c4dc2f..61dd260 100644 --- a/yarn.lock +++ b/yarn.lock @@ -264,11 +264,6 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@popperjs/core@^2.9.2": - version "2.9.2" - resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.2.tgz#adea7b6953cbb34651766b0548468e743c6a2353" - integrity sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q== - "@rushstack/eslint-patch@^1.0.6": version "1.0.6" resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.0.6.tgz#023d72a5c4531b4ce204528971700a78a85a0c50" @@ -589,7 +584,7 @@ boolbase@^1.0.0: resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= -bowser@^2.8.1: +bowser@^2.11.0, bowser@^2.8.1: version "2.11.0" resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f" integrity sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA== @@ -2209,7 +2204,7 @@ lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: +loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -2902,11 +2897,6 @@ react-dom@^17.0.2: object-assign "^4.1.1" scheduler "^0.20.2" -react-fast-compare@^3.0.1: - version "3.2.0" - resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" - integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA== - react-is@17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" @@ -2917,14 +2907,6 @@ react-is@^16.8.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -react-popper@^2.2.5: - version "2.2.5" - resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.2.5.tgz#1214ef3cec86330a171671a4fbcbeeb65ee58e96" - integrity sha512-kxGkS80eQGtLl18+uig1UIf9MKixFSyPxglsgLBxlYnyDf65BiY9B3nZSc6C9XUNDgStROB0fMQlTEz1KxGddw== - dependencies: - react-fast-compare "^3.0.1" - warning "^4.0.2" - react-refresh@0.8.3: version "0.8.3" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f" @@ -3634,13 +3616,6 @@ vm-browserify@1.1.2, vm-browserify@^1.0.1: resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== -warning@^4.0.2: - version "4.0.3" - resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" - integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== - dependencies: - loose-envify "^1.0.0" - watchpack@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.1.1.tgz#e99630550fca07df9f90a06056987baa40a689c7"