Bluesky app fork with some witchin' additions 💫

[Video] Lexicon implementation (#4881)

* implement AppBskyEmbedVideo lexicon in player

* add alt to native player

* add prerelease package

* update prerelease

* add video embed view manually from record

* fix type error on example video

* black bg + use aspect ratio on web

* add video to feeds

* fix video overflowing aspect ratio

* remove prerelease package

---------

Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>

authored by samuel.fm

Samuel Newman and committed by
GitHub
d92731b1 b136c442

+211 -91
+1 -1
package.json
··· 52 52 "open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web" 53 53 }, 54 54 "dependencies": { 55 - "@atproto/api": "0.13.3", 55 + "@atproto/api": "0.13.5", 56 56 "@bam.tech/react-native-image-resizer": "^3.0.4", 57 57 "@braintree/sanitize-url": "^6.0.2", 58 58 "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
+1
src/view/com/post-thread/PostThread.tsx
··· 428 428 (item.ctx.depth < 0 && !!item.parent) || item.ctx.depth > 1 429 429 const hasUnrevealedParents = 430 430 index === 0 && skeleton?.parents && maxParents < skeleton.parents.length 431 + 431 432 return ( 432 433 <View 433 434 ref={item.ctx.isHighlightedPost ? highlightedPostRef : undefined}
+22 -18
src/view/com/posts/FeedItem.tsx
··· 17 17 import {useLingui} from '@lingui/react' 18 18 import {useQueryClient} from '@tanstack/react-query' 19 19 20 + import {isReasonFeedSource, ReasonFeedSource} from '#/lib/api/feed/types' 21 + import {MAX_POST_LINES} from '#/lib/constants' 22 + import {usePalette} from '#/lib/hooks/usePalette' 23 + import {makeProfileLink} from '#/lib/routes/links' 20 24 import {useGate} from '#/lib/statsig/statsig' 25 + import {sanitizeDisplayName} from '#/lib/strings/display-names' 26 + import {sanitizeHandle} from '#/lib/strings/handles' 27 + import {countLines} from '#/lib/strings/helpers' 28 + import {s} from '#/lib/styles' 21 29 import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow' 22 30 import {useFeedFeedbackContext} from '#/state/feed-feedback' 31 + import {precacheProfile} from '#/state/queries/profile' 23 32 import {useSession} from '#/state/session' 24 33 import {useComposerControls} from '#/state/shell/composer' 25 34 import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' 26 - import {isReasonFeedSource, ReasonFeedSource} from 'lib/api/feed/types' 27 - import {MAX_POST_LINES} from 'lib/constants' 28 - import {usePalette} from 'lib/hooks/usePalette' 29 - import {makeProfileLink} from 'lib/routes/links' 30 - import {sanitizeDisplayName} from 'lib/strings/display-names' 31 - import {sanitizeHandle} from 'lib/strings/handles' 32 - import {countLines} from 'lib/strings/helpers' 33 - import {s} from 'lib/styles' 34 - import {precacheProfile} from 'state/queries/profile' 35 + import {FeedNameText} from '#/view/com/util/FeedInfoText' 36 + import {PostCtrls} from '#/view/com/util/post-ctrls/PostCtrls' 37 + import {PostEmbeds} from '#/view/com/util/post-embeds' 38 + import {PostMeta} from '#/view/com/util/PostMeta' 39 + import {Text} from '#/view/com/util/text/Text' 40 + import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' 35 41 import {atoms as a} from '#/alf' 36 42 import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost' 37 43 import {ContentHider} from '#/components/moderation/ContentHider' 44 + import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' 45 + import {PostAlerts} from '#/components/moderation/PostAlerts' 38 46 import {AppModerationCause} from '#/components/Pills' 39 47 import {ProfileHoverCard} from '#/components/ProfileHoverCard' 40 48 import {RichText} from '#/components/RichText' 41 - import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe' 42 - import {PostAlerts} from '../../../components/moderation/PostAlerts' 43 - import {FeedNameText} from '../util/FeedInfoText' 44 49 import {Link, TextLink, TextLinkOnWebOnly} from '../util/Link' 45 - import {PostCtrls} from '../util/post-ctrls/PostCtrls' 46 - import {PostEmbeds} from '../util/post-embeds' 47 50 import {VideoEmbed} from '../util/post-embeds/VideoEmbed' 48 - import {PostMeta} from '../util/PostMeta' 49 - import {Text} from '../util/text/Text' 50 - import {PreviewableUserAvatar} from '../util/UserAvatar' 51 51 import {AviFollowButton} from './AviFollowButton' 52 52 53 53 interface FeedItemProps { ··· 571 571 572 572 return ( 573 573 <VideoEmbed 574 - source={`https://lumi.jazco.dev/watch/did:plc:q6gjnaw2blty4crticxkmujt/Qmc8w93UpTa2adJHg4ZhnDPrBs1EsbzrekzPcqF5SwusuZ/playlist.m3u8?ignore_me_just_testing_frontend_stuff=${id}`} 574 + embed={{ 575 + playlist: `https://lumi.jazco.dev/watch/did:plc:q6gjnaw2blty4crticxkmujt/Qmc8w93UpTa2adJHg4ZhnDPrBs1EsbzrekzPcqF5SwusuZ/playlist.m3u8?ignore_me_just_testing_frontend_stuff=${id}`, 576 + cid: 'Qmc8w93UpTa2adJHg4ZhnDPrBs1EsbzrekzPcqF5SwusuZ', 577 + aspectRatio: {height: 9, width: 16}, 578 + }} 575 579 /> 576 580 ) 577 581 }
+47 -21
src/view/com/util/post-embeds/VideoEmbed.tsx
··· 1 1 import React, {useCallback, useState} from 'react' 2 2 import {View} from 'react-native' 3 + import {Image} from 'expo-image' 4 + import {AppBskyEmbedVideo} from '@atproto/api' 3 5 import {msg, Trans} from '@lingui/macro' 4 6 import {useLingui} from '@lingui/react' 5 7 6 - import {VideoEmbedInnerNative} from 'view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative' 8 + import {clamp} from '#/lib/numbers' 9 + import {useGate} from '#/lib/statsig/statsig' 10 + import {VideoEmbedInnerNative} from '#/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative' 7 11 import {atoms as a, useTheme} from '#/alf' 8 - import {Button, ButtonIcon} from '#/components/Button' 12 + import {Button} from '#/components/Button' 9 13 import {Play_Filled_Corner2_Rounded as PlayIcon} from '#/components/icons/Play' 10 14 import {VisibilityView} from '../../../../../modules/expo-bluesky-swiss-army' 11 15 import {ErrorBoundary} from '../ErrorBoundary' 12 16 import {useActiveVideoNative} from './ActiveVideoNativeContext' 13 17 import * as VideoFallback from './VideoEmbedInner/VideoFallback' 14 18 15 - export function VideoEmbed({source}: {source: string}) { 19 + export function VideoEmbed({embed}: {embed: AppBskyEmbedVideo.View}) { 16 20 const t = useTheme() 17 21 const {activeSource, setActiveSource} = useActiveVideoNative() 18 - const isActive = source === activeSource 22 + const isActive = embed.playlist === activeSource 19 23 const {_} = useLingui() 20 24 21 25 const [key, setKey] = useState(0) ··· 25 29 ), 26 30 [key], 27 31 ) 32 + const gate = useGate() 33 + 34 + if (!gate('videos')) { 35 + return null 36 + } 37 + 38 + let aspectRatio = 16 / 9 39 + 40 + if (embed.aspectRatio) { 41 + const {width, height} = embed.aspectRatio 42 + aspectRatio = width / height 43 + aspectRatio = clamp(aspectRatio, 1 / 1, 3 / 1) 44 + } 28 45 29 46 return ( 30 47 <View 31 48 style={[ 32 49 a.w_full, 33 50 a.rounded_sm, 34 - {aspectRatio: 16 / 9}, 35 51 a.overflow_hidden, 36 - t.atoms.bg_contrast_25, 52 + {aspectRatio}, 53 + {backgroundColor: t.palette.black}, 37 54 a.my_xs, 38 55 ]}> 39 56 <ErrorBoundary renderError={renderError} key={key}> 40 57 <VisibilityView 41 58 enabled={true} 42 - onChangeStatus={isActive => { 43 - if (isActive) { 44 - setActiveSource(source) 59 + onChangeStatus={isVisible => { 60 + if (isVisible) { 61 + setActiveSource(embed.playlist) 45 62 } 46 63 }}> 47 64 {isActive ? ( 48 - <VideoEmbedInnerNative /> 65 + <VideoEmbedInnerNative embed={embed} /> 49 66 ) : ( 50 - <Button 51 - style={[a.flex_1, t.atoms.bg_contrast_25]} 52 - onPress={() => { 53 - setActiveSource(source) 54 - }} 55 - label={_(msg`Play video`)} 56 - variant="ghost" 57 - color="secondary" 58 - size="large"> 59 - <ButtonIcon icon={PlayIcon} /> 60 - </Button> 67 + <> 68 + <Image 69 + source={{uri: embed.thumbnail}} 70 + alt={embed.alt} 71 + style={a.flex_1} 72 + contentFit="contain" 73 + accessibilityIgnoresInvertColors 74 + /> 75 + <Button 76 + style={[a.absolute, a.inset_0]} 77 + onPress={() => { 78 + setActiveSource(embed.playlist) 79 + }} 80 + label={_(msg`Play video`)} 81 + variant="ghost" 82 + color="secondary" 83 + size="large"> 84 + <PlayIcon width={48} fill={t.palette.white} /> 85 + </Button> 86 + </> 61 87 )} 62 88 </VisibilityView> 63 89 </ErrorBoundary>
+22 -5
src/view/com/util/post-embeds/VideoEmbed.web.tsx
··· 1 1 import React, {useCallback, useEffect, useRef, useState} from 'react' 2 2 import {View} from 'react-native' 3 + import {AppBskyEmbedVideo} from '@atproto/api' 3 4 import {Trans} from '@lingui/macro' 4 5 6 + import {clamp} from '#/lib/numbers' 7 + import {useGate} from '#/lib/statsig/statsig' 5 8 import { 6 9 HLSUnsupportedError, 7 10 VideoEmbedInnerWeb, 8 - } from 'view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb' 11 + } from '#/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb' 9 12 import {atoms as a, useTheme} from '#/alf' 10 13 import {ErrorBoundary} from '../ErrorBoundary' 11 14 import {useActiveVideoWeb} from './ActiveVideoWebContext' 12 15 import * as VideoFallback from './VideoEmbedInner/VideoFallback' 13 16 14 - export function VideoEmbed({source}: {source: string}) { 17 + export function VideoEmbed({embed}: {embed: AppBskyEmbedVideo.View}) { 15 18 const t = useTheme() 16 19 const ref = useRef<HTMLDivElement>(null) 20 + const gate = useGate() 17 21 const {active, setActive, sendPosition, currentActiveView} = 18 22 useActiveVideoWeb() 19 23 const [onScreen, setOnScreen] = useState(false) ··· 43 47 [key], 44 48 ) 45 49 50 + if (!gate('videos')) { 51 + return null 52 + } 53 + 54 + let aspectRatio = 16 / 9 55 + 56 + if (embed.aspectRatio) { 57 + const {width, height} = embed.aspectRatio 58 + // min: 3/1, max: square 59 + aspectRatio = clamp(width / height, 1 / 1, 3 / 1) 60 + } 61 + 46 62 return ( 47 63 <View 48 64 style={[ 49 65 a.w_full, 50 - {aspectRatio: 16 / 9}, 51 - t.atoms.bg_contrast_25, 66 + {aspectRatio}, 67 + {backgroundColor: t.palette.black}, 68 + a.relative, 52 69 a.rounded_sm, 53 70 a.my_xs, 54 71 ]}> ··· 61 78 sendPosition={sendPosition} 62 79 isAnyViewActive={currentActiveView !== null}> 63 80 <VideoEmbedInnerWeb 64 - source={source} 81 + embed={embed} 65 82 active={active} 66 83 setActive={setActive} 67 84 onScreen={onScreen}
+25 -4
src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx
··· 2 2 import {Pressable, View} from 'react-native' 3 3 import Animated, {FadeInDown} from 'react-native-reanimated' 4 4 import {VideoPlayer, VideoView} from 'expo-video' 5 + import {AppBskyEmbedVideo} from '@atproto/api' 5 6 import {msg} from '@lingui/macro' 6 7 import {useLingui} from '@lingui/react' 7 8 import {useIsFocused} from '@react-navigation/native' 8 9 9 10 import {HITSLOP_30} from '#/lib/constants' 10 11 import {useAppState} from '#/lib/hooks/useAppState' 12 + import {clamp} from '#/lib/numbers' 11 13 import {logger} from '#/logger' 12 14 import {useActiveVideoNative} from 'view/com/util/post-embeds/ActiveVideoNativeContext' 13 15 import {atoms as a, useTheme} from '#/alf' ··· 19 21 } from '../../../../../../modules/expo-bluesky-swiss-army' 20 22 import {TimeIndicator} from './TimeIndicator' 21 23 22 - export function VideoEmbedInnerNative() { 24 + export function VideoEmbedInnerNative({ 25 + embed, 26 + }: { 27 + embed: AppBskyEmbedVideo.View 28 + }) { 29 + const {_} = useLingui() 23 30 const {player} = useActiveVideoNative() 24 31 const ref = useRef<VideoView>(null) 25 32 const isScreenFocused = useIsFocused() ··· 47 54 ref.current?.enterFullscreen() 48 55 }, []) 49 56 57 + let aspectRatio = 16 / 9 58 + 59 + if (embed.aspectRatio) { 60 + const {width, height} = embed.aspectRatio 61 + aspectRatio = width / height 62 + aspectRatio = clamp(aspectRatio, 1 / 1, 3 / 1) 63 + } 64 + 50 65 return ( 51 - <View style={[a.flex_1, a.relative]}> 66 + <View style={[a.flex_1, a.relative, {aspectRatio}]}> 52 67 <VideoView 53 68 ref={ref} 54 69 player={player} 55 70 style={[a.flex_1, a.rounded_sm]} 71 + contentFit="contain" 56 72 nativeControls={true} 73 + accessibilityIgnoresInvertColors 57 74 onEnterFullscreen={() => { 58 75 PlatformInfo.setAudioCategory(AudioCategory.Playback) 59 76 PlatformInfo.setAudioActive(true) ··· 65 82 player.muted = true 66 83 if (!player.playing) player.play() 67 84 }} 85 + accessibilityLabel={ 86 + embed.alt ? _(msg`Video: ${embed.alt}`) : _(msg`Video`) 87 + } 88 + accessibilityHint="" 68 89 /> 69 - <Controls player={player} enterFullscreen={enterFullscreen} /> 90 + <VideoControls player={player} enterFullscreen={enterFullscreen} /> 70 91 </View> 71 92 ) 72 93 } 73 94 74 - function Controls({ 95 + function VideoControls({ 75 96 player, 76 97 enterFullscreen, 77 98 }: {
+41 -34
src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx
··· 1 - import React, {useEffect, useRef, useState} from 'react' 1 + import React, {useEffect, useId, useRef, useState} from 'react' 2 2 import {View} from 'react-native' 3 + import {AppBskyEmbedVideo} from '@atproto/api' 3 4 import Hls from 'hls.js' 4 5 5 6 import {atoms as a} from '#/alf' 6 7 import {Controls} from './VideoWebControls' 7 8 8 9 export function VideoEmbedInnerWeb({ 9 - source, 10 + embed, 10 11 active, 11 12 setActive, 12 13 onScreen, 13 14 }: { 14 - source: string 15 - active?: boolean 16 - setActive?: () => void 17 - onScreen?: boolean 15 + embed: AppBskyEmbedVideo.View 16 + active: boolean 17 + setActive: () => void 18 + onScreen: boolean 18 19 }) { 19 - if (active == null || setActive == null || onScreen == null) { 20 - throw new Error( 21 - 'active, setActive, and onScreen are required VideoEmbedInner props on web.', 22 - ) 23 - } 24 - 25 20 const containerRef = useRef<HTMLDivElement>(null) 26 21 const ref = useRef<HTMLVideoElement>(null) 27 22 const [focused, setFocused] = useState(false) 28 23 const [hasSubtitleTrack, setHasSubtitleTrack] = useState(false) 24 + const figId = useId() 29 25 30 26 const hlsRef = useRef<Hls | undefined>(undefined) 31 27 ··· 37 33 hlsRef.current = hls 38 34 39 35 hls.attachMedia(ref.current) 40 - hls.loadSource(source) 36 + hls.loadSource(embed.playlist) 41 37 42 38 // initial value, later on it's managed by Controls 43 39 hls.autoLevelCapping = 0 ··· 53 49 hls.detachMedia() 54 50 hls.destroy() 55 51 } 56 - }, [source]) 52 + }, [embed.playlist]) 57 53 58 54 return ( 59 - <View 60 - style={[ 61 - a.w_full, 62 - a.rounded_sm, 63 - // TODO: get from embed metadata 64 - // max should be 1 / 1 65 - {aspectRatio: 16 / 9}, 66 - a.overflow_hidden, 67 - ]}> 68 - <div 69 - ref={containerRef} 70 - style={{width: '100%', height: '100%', display: 'flex'}}> 71 - <video 72 - ref={ref} 73 - style={{width: '100%', height: '100%', objectFit: 'contain'}} 74 - playsInline 75 - preload="none" 76 - loop 77 - muted={!focused} 78 - /> 55 + <View style={[a.flex_1, a.rounded_sm, a.overflow_hidden]}> 56 + <div ref={containerRef} style={{height: '100%', width: '100%'}}> 57 + <figure style={{margin: 0, position: 'absolute', inset: 0}}> 58 + <video 59 + ref={ref} 60 + poster={embed.thumbnail} 61 + style={{width: '100%', height: '100%', objectFit: 'contain'}} 62 + playsInline 63 + preload="none" 64 + loop 65 + muted={!focused} 66 + aria-labelledby={embed.alt ? figId : undefined} 67 + /> 68 + {embed.alt && ( 69 + <figcaption 70 + id={figId} 71 + style={{ 72 + position: 'absolute', 73 + width: 1, 74 + height: 1, 75 + padding: 0, 76 + margin: -1, 77 + overflow: 'hidden', 78 + clip: 'rect(0, 0, 0, 0)', 79 + whiteSpace: 'nowrap', 80 + borderWidth: 0, 81 + }}> 82 + {embed.alt} 83 + </figcaption> 84 + )} 85 + </figure> 79 86 <Controls 80 87 videoRef={ref} 81 88 hlsRef={hlsRef}
+11
src/view/com/util/post-embeds/index.tsx
··· 13 13 AppBskyEmbedImages, 14 14 AppBskyEmbedRecord, 15 15 AppBskyEmbedRecordWithMedia, 16 + AppBskyEmbedVideo, 16 17 AppBskyFeedDefs, 17 18 AppBskyGraphDefs, 18 19 moderateFeedGenerator, ··· 33 34 import {ImageLayoutGrid} from '../images/ImageLayoutGrid' 34 35 import {ExternalLinkEmbed} from './ExternalLinkEmbed' 35 36 import {MaybeQuoteEmbed} from './QuoteEmbed' 37 + import {VideoEmbed} from './VideoEmbed' 36 38 37 39 type Embed = 38 40 | AppBskyEmbedRecord.View 39 41 | AppBskyEmbedImages.View 42 + | AppBskyEmbedVideo.View 40 43 | AppBskyEmbedExternal.View 41 44 | AppBskyEmbedRecordWithMedia.View 42 45 | {$type: string; [k: string]: unknown} ··· 171 174 onOpen={onOpen} 172 175 style={[styles.container, style]} 173 176 /> 177 + </ContentHider> 178 + ) 179 + } 180 + 181 + if (AppBskyEmbedVideo.isView(embed)) { 182 + return ( 183 + <ContentHider modui={moderation?.ui('contentMedia')}> 184 + <VideoEmbed embed={embed} /> 174 185 </ContentHider> 175 186 ) 176 187 }
+41 -8
yarn.lock
··· 72 72 resolved "https://registry.yarnpkg.com/@atproto-labs/simple-store/-/simple-store-0.1.1.tgz#e743a2722b5d8732166f0a72aca8bd10e9bff106" 73 73 integrity sha512-WKILW2b3QbAYKh+w5U2x6p5FqqLl0nAeLwGeDY+KjX01K4Dq3vQTR9b/qNp0jZm48CabPQVrqCv0PPU9LgRRRg== 74 74 75 - "@atproto/api@0.13.3": 76 - version "0.13.3" 77 - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.13.3.tgz#d84f2a0e25f38cca59b69d178901634f2d20b4ff" 78 - integrity sha512-/PEVTTEQXICOjZCujAPsjArhwR0tR3LiF0SxxpZlWOjaqjVbqnBI/j0MNmddBFgeljC4/DcBobcDJ9HkILn4yQ== 75 + "@atproto/api@0.13.5": 76 + version "0.13.5" 77 + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.13.5.tgz#04305cdb0a467ba366305c5e95cebb7ce0d39735" 78 + integrity sha512-yT/YimcKYkrI0d282Zxo7O30OSYR+KDW89f81C6oYZfDRBcShC1aniVV8kluP5LrEAg8O27yrOSnBgx2v7XPew== 79 79 dependencies: 80 80 "@atproto/common-web" "^0.3.0" 81 81 "@atproto/lexicon" "^0.4.1" 82 82 "@atproto/syntax" "^0.3.0" 83 - "@atproto/xrpc" "^0.6.0" 83 + "@atproto/xrpc" "^0.6.1" 84 84 await-lock "^2.2.2" 85 85 multiformats "^9.9.0" 86 86 tlds "^1.234.0" ··· 439 439 version "0.6.0" 440 440 resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.6.0.tgz#668c3262e67e2afa65951ea79a03bfe3720ddf5c" 441 441 integrity sha512-5BbhBTv5j6MC3iIQ4+vYxQE7nLy2dDGQ+LYJrH8PptOCUdq0Pwg6aRccQ3y52kUZlhE/mzOTZ8Ngiy9pSAyfVQ== 442 + dependencies: 443 + "@atproto/lexicon" "^0.4.1" 444 + zod "^3.23.8" 445 + 446 + "@atproto/xrpc@^0.6.1": 447 + version "0.6.1" 448 + resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.6.1.tgz#dcd1315c8c60eef5af2db7fa4e35a38ebc6d79d5" 449 + integrity sha512-Zy5ydXEdk6sY7FDUZcEVfCL1jvbL4tXu5CcdPqbEaW6LQtk9GLds/DK1bCX9kswTGaBC88EMuqQMfkxOhp2t4A== 442 450 dependencies: 443 451 "@atproto/lexicon" "^0.4.1" 444 452 zod "^3.23.8" ··· 20681 20689 resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" 20682 20690 integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== 20683 20691 20684 - "string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: 20692 + "string-width-cjs@npm:string-width@^4.2.0": 20693 + version "4.2.3" 20694 + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" 20695 + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== 20696 + dependencies: 20697 + emoji-regex "^8.0.0" 20698 + is-fullwidth-code-point "^3.0.0" 20699 + strip-ansi "^6.0.1" 20700 + 20701 + string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: 20685 20702 version "4.2.3" 20686 20703 resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" 20687 20704 integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== ··· 20790 20807 is-obj "^1.0.1" 20791 20808 is-regexp "^1.0.0" 20792 20809 20793 - "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: 20810 + "strip-ansi-cjs@npm:strip-ansi@^6.0.1": 20794 20811 version "6.0.1" 20795 20812 resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" 20796 20813 integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== ··· 20803 20820 integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== 20804 20821 dependencies: 20805 20822 ansi-regex "^4.1.0" 20823 + 20824 + strip-ansi@^6.0.0, strip-ansi@^6.0.1: 20825 + version "6.0.1" 20826 + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" 20827 + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== 20828 + dependencies: 20829 + ansi-regex "^5.0.1" 20806 20830 20807 20831 strip-ansi@^7.0.1: 20808 20832 version "7.1.0" ··· 22536 22560 "@types/trusted-types" "^2.0.2" 22537 22561 workbox-core "6.6.1" 22538 22562 22539 - "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: 22563 + "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": 22540 22564 version "7.0.0" 22541 22565 resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" 22542 22566 integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== ··· 22549 22573 version "6.2.0" 22550 22574 resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" 22551 22575 integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== 22576 + dependencies: 22577 + ansi-styles "^4.0.0" 22578 + string-width "^4.1.0" 22579 + strip-ansi "^6.0.0" 22580 + 22581 + wrap-ansi@^7.0.0: 22582 + version "7.0.0" 22583 + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" 22584 + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== 22552 22585 dependencies: 22553 22586 ansi-styles "^4.0.0" 22554 22587 string-width "^4.1.0"