Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Use ALF for the embed consent modal (#3336)

authored by

Samuel Newman and committed by
GitHub
a49a5a35 2bc20b17

+250 -281
+119
src/components/dialogs/EmbedConsent.tsx
··· 1 + import React, {useCallback} from 'react' 2 + import {View} from 'react-native' 3 + import {msg, Trans} from '@lingui/macro' 4 + import {useLingui} from '@lingui/react' 5 + 6 + import { 7 + type EmbedPlayerSource, 8 + embedPlayerSources, 9 + externalEmbedLabels, 10 + } from '#/lib/strings/embed-player' 11 + import {useSetExternalEmbedPref} from '#/state/preferences' 12 + import {atoms as a, useBreakpoints, useTheme} from '#/alf' 13 + import * as Dialog from '#/components/Dialog' 14 + import {Button, ButtonText} from '../Button' 15 + import {Text} from '../Typography' 16 + 17 + export function EmbedConsentDialog({ 18 + control, 19 + source, 20 + onAccept, 21 + }: { 22 + control: Dialog.DialogControlProps 23 + source: EmbedPlayerSource 24 + onAccept: () => void 25 + }) { 26 + const {_} = useLingui() 27 + const t = useTheme() 28 + const setExternalEmbedPref = useSetExternalEmbedPref() 29 + const {gtMobile} = useBreakpoints() 30 + 31 + const onShowAllPress = useCallback(() => { 32 + for (const key of embedPlayerSources) { 33 + setExternalEmbedPref(key, 'show') 34 + } 35 + onAccept() 36 + control.close() 37 + }, [control, onAccept, setExternalEmbedPref]) 38 + 39 + const onShowPress = useCallback(() => { 40 + setExternalEmbedPref(source, 'show') 41 + onAccept() 42 + control.close() 43 + }, [control, onAccept, setExternalEmbedPref, source]) 44 + 45 + const onHidePress = useCallback(() => { 46 + setExternalEmbedPref(source, 'hide') 47 + control.close() 48 + }, [control, setExternalEmbedPref, source]) 49 + 50 + return ( 51 + <Dialog.Outer control={control}> 52 + <Dialog.Handle /> 53 + 54 + <Dialog.ScrollableInner 55 + label={_(msg`External Media`)} 56 + style={[gtMobile ? {width: 'auto', maxWidth: 400} : a.w_full]}> 57 + <View style={a.gap_sm}> 58 + <Text style={[a.text_2xl, a.font_bold]}> 59 + <Trans>External Media</Trans> 60 + </Text> 61 + 62 + <View style={[a.mt_sm, a.mb_2xl, a.gap_lg]}> 63 + <Text> 64 + <Trans> 65 + This content is hosted by {externalEmbedLabels[source]}. Do you 66 + want to enable external media? 67 + </Trans> 68 + </Text> 69 + 70 + <Text style={t.atoms.text_contrast_medium}> 71 + <Trans> 72 + External media may allow websites to collect information about 73 + you and your device. No information is sent or requested until 74 + you press the "play" button. 75 + </Trans> 76 + </Text> 77 + </View> 78 + </View> 79 + <View style={a.gap_md}> 80 + <Button 81 + style={gtMobile && a.flex_1} 82 + label={_(msg`Enable external media`)} 83 + onPress={onShowAllPress} 84 + onAccessibilityEscape={control.close} 85 + color="primary" 86 + size="medium" 87 + variant="solid"> 88 + <ButtonText> 89 + <Trans>Enable external media</Trans> 90 + </ButtonText> 91 + </Button> 92 + <Button 93 + style={gtMobile && a.flex_1} 94 + label={_(msg`Enable this source only`)} 95 + onPress={onShowPress} 96 + onAccessibilityEscape={control.close} 97 + color="secondary" 98 + size="medium" 99 + variant="solid"> 100 + <ButtonText> 101 + <Trans>Enable {externalEmbedLabels[source]} only</Trans> 102 + </ButtonText> 103 + </Button> 104 + <Button 105 + label={_(msg`No thanks`)} 106 + onAccessibilityEscape={control.close} 107 + onPress={onHidePress} 108 + color="secondary" 109 + size="medium" 110 + variant="ghost"> 111 + <ButtonText> 112 + <Trans>No thanks</Trans> 113 + </ButtonText> 114 + </Button> 115 + </View> 116 + </Dialog.ScrollableInner> 117 + </Dialog.Outer> 118 + ) 119 + }
-8
src/state/modals/index.tsx
··· 3 3 import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api' 4 4 5 5 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 6 - import {EmbedPlayerSource} from '#/lib/strings/embed-player' 7 6 import {GalleryModel} from '#/state/models/media/gallery' 8 7 import {ImageModel} from '#/state/models/media/image' 9 8 import {ThreadgateSetting} from '../queries/threadgate' ··· 125 124 share?: boolean 126 125 } 127 126 128 - export interface EmbedConsentModal { 129 - name: 'embed-consent' 130 - source: EmbedPlayerSource 131 - onAccept: () => void 132 - } 133 - 134 127 export interface InAppBrowserConsentModal { 135 128 name: 'in-app-browser-consent' 136 129 href: string ··· 169 162 170 163 // Generic 171 164 | LinkWarningModal 172 - | EmbedConsentModal 173 165 | InAppBrowserConsentModal 174 166 175 167 const ModalContext = React.createContext<{
-154
src/view/com/modals/EmbedConsent.tsx
··· 1 - import React from 'react' 2 - import {StyleSheet, TouchableOpacity, View} from 'react-native' 3 - import {LinearGradient} from 'expo-linear-gradient' 4 - import {msg, Trans} from '@lingui/macro' 5 - import {useLingui} from '@lingui/react' 6 - 7 - import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 8 - import { 9 - EmbedPlayerSource, 10 - embedPlayerSources, 11 - externalEmbedLabels, 12 - } from '#/lib/strings/embed-player' 13 - import {useModalControls} from '#/state/modals' 14 - import {useSetExternalEmbedPref} from '#/state/preferences/external-embeds-prefs' 15 - import {usePalette} from 'lib/hooks/usePalette' 16 - import {colors, gradients, s} from 'lib/styles' 17 - import {Text} from '../util/text/Text' 18 - import {ScrollView} from './util' 19 - 20 - export const snapPoints = [450] 21 - 22 - export function Component({ 23 - onAccept, 24 - source, 25 - }: { 26 - onAccept: () => void 27 - source: EmbedPlayerSource 28 - }) { 29 - const pal = usePalette('default') 30 - const {closeModal} = useModalControls() 31 - const {_} = useLingui() 32 - const setExternalEmbedPref = useSetExternalEmbedPref() 33 - const {isMobile} = useWebMediaQueries() 34 - 35 - const onShowAllPress = React.useCallback(() => { 36 - for (const key of embedPlayerSources) { 37 - setExternalEmbedPref(key, 'show') 38 - } 39 - onAccept() 40 - closeModal() 41 - }, [closeModal, onAccept, setExternalEmbedPref]) 42 - 43 - const onShowPress = React.useCallback(() => { 44 - setExternalEmbedPref(source, 'show') 45 - onAccept() 46 - closeModal() 47 - }, [closeModal, onAccept, setExternalEmbedPref, source]) 48 - 49 - const onHidePress = React.useCallback(() => { 50 - setExternalEmbedPref(source, 'hide') 51 - closeModal() 52 - }, [closeModal, setExternalEmbedPref, source]) 53 - 54 - return ( 55 - <ScrollView 56 - testID="embedConsentModal" 57 - style={[ 58 - s.flex1, 59 - pal.view, 60 - isMobile 61 - ? {paddingHorizontal: 20, paddingTop: 10} 62 - : {paddingHorizontal: 30}, 63 - ]}> 64 - <Text style={[pal.text, styles.title]}> 65 - <Trans>External Media</Trans> 66 - </Text> 67 - 68 - <Text style={pal.text}> 69 - <Trans> 70 - This content is hosted by {externalEmbedLabels[source]}. Do you want 71 - to enable external media? 72 - </Trans> 73 - </Text> 74 - <View style={[s.mt10]} /> 75 - <Text style={pal.textLight}> 76 - <Trans> 77 - External media may allow websites to collect information about you and 78 - your device. No information is sent or requested until you press the 79 - "play" button. 80 - </Trans> 81 - </Text> 82 - <View style={[s.mt20]} /> 83 - <TouchableOpacity 84 - testID="enableAllBtn" 85 - onPress={onShowAllPress} 86 - accessibilityRole="button" 87 - accessibilityLabel={_( 88 - msg`Show embeds from ${externalEmbedLabels[source]}`, 89 - )} 90 - accessibilityHint="" 91 - onAccessibilityEscape={closeModal}> 92 - <LinearGradient 93 - colors={[gradients.blueLight.start, gradients.blueLight.end]} 94 - start={{x: 0, y: 0}} 95 - end={{x: 1, y: 1}} 96 - style={[styles.btn]}> 97 - <Text style={[s.white, s.bold, s.f18]}> 98 - <Trans>Enable External Media</Trans> 99 - </Text> 100 - </LinearGradient> 101 - </TouchableOpacity> 102 - <View style={[s.mt10]} /> 103 - <TouchableOpacity 104 - testID="enableSourceBtn" 105 - onPress={onShowPress} 106 - accessibilityRole="button" 107 - accessibilityLabel={_( 108 - msg`Never load embeds from ${externalEmbedLabels[source]}`, 109 - )} 110 - accessibilityHint="" 111 - onAccessibilityEscape={closeModal}> 112 - <View style={[styles.btn, pal.btn]}> 113 - <Text style={[pal.text, s.bold, s.f18]}> 114 - <Trans>Enable {externalEmbedLabels[source]} only</Trans> 115 - </Text> 116 - </View> 117 - </TouchableOpacity> 118 - <View style={[s.mt10]} /> 119 - <TouchableOpacity 120 - testID="disableSourceBtn" 121 - onPress={onHidePress} 122 - accessibilityRole="button" 123 - accessibilityLabel={_( 124 - msg`Never load embeds from ${externalEmbedLabels[source]}`, 125 - )} 126 - accessibilityHint="" 127 - onAccessibilityEscape={closeModal}> 128 - <View style={[styles.btn, pal.btn]}> 129 - <Text style={[pal.text, s.bold, s.f18]}> 130 - <Trans>No thanks</Trans> 131 - </Text> 132 - </View> 133 - </TouchableOpacity> 134 - </ScrollView> 135 - ) 136 - } 137 - 138 - const styles = StyleSheet.create({ 139 - title: { 140 - textAlign: 'center', 141 - fontWeight: 'bold', 142 - fontSize: 24, 143 - marginBottom: 12, 144 - }, 145 - btn: { 146 - flexDirection: 'row', 147 - alignItems: 'center', 148 - justifyContent: 'center', 149 - width: '100%', 150 - borderRadius: 32, 151 - padding: 14, 152 - backgroundColor: colors.gray1, 153 - }, 154 - })
-4
src/view/com/modals/Modal.tsx
··· 15 15 import * as CreateOrEditListModal from './CreateOrEditList' 16 16 import * as DeleteAccountModal from './DeleteAccount' 17 17 import * as EditProfileModal from './EditProfile' 18 - import * as EmbedConsentModal from './EmbedConsent' 19 18 import * as InAppBrowserConsentModal from './InAppBrowserConsent' 20 19 import * as InviteCodesModal from './InviteCodes' 21 20 import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings' ··· 116 115 } else if (activeModal?.name === 'link-warning') { 117 116 snapPoints = LinkWarningModal.snapPoints 118 117 element = <LinkWarningModal.Component {...activeModal} /> 119 - } else if (activeModal?.name === 'embed-consent') { 120 - snapPoints = EmbedConsentModal.snapPoints 121 - element = <EmbedConsentModal.Component {...activeModal} /> 122 118 } else if (activeModal?.name === 'in-app-browser-consent') { 123 119 snapPoints = InAppBrowserConsentModal.snapPoints 124 120 element = <InAppBrowserConsentModal.Component {...activeModal} />
+18 -21
src/view/com/modals/Modal.web.tsx
··· 1 1 import React from 'react' 2 - import {TouchableWithoutFeedback, StyleSheet, View} from 'react-native' 2 + import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native' 3 3 import Animated, {FadeIn, FadeOut} from 'react-native-reanimated' 4 + 5 + import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock' 6 + import type {Modal as ModalIface} from '#/state/modals' 7 + import {useModalControls, useModals} from '#/state/modals' 4 8 import {usePalette} from 'lib/hooks/usePalette' 5 9 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 6 - import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock' 7 - 8 - import {useModals, useModalControls} from '#/state/modals' 9 - import type {Modal as ModalIface} from '#/state/modals' 10 - import * as EditProfileModal from './EditProfile' 10 + import * as AddAppPassword from './AddAppPasswords' 11 + import * as AltTextImageModal from './AltImage' 12 + import * as ChangeEmailModal from './ChangeEmail' 13 + import * as ChangeHandleModal from './ChangeHandle' 14 + import * as ChangePasswordModal from './ChangePassword' 11 15 import * as CreateOrEditListModal from './CreateOrEditList' 12 - import * as UserAddRemoveLists from './UserAddRemoveLists' 13 - import * as ListAddUserModal from './ListAddRemoveUsers' 16 + import * as CropImageModal from './crop-image/CropImage.web' 14 17 import * as DeleteAccountModal from './DeleteAccount' 15 - import * as RepostModal from './Repost' 16 - import * as SelfLabelModal from './SelfLabel' 17 - import * as ThreadgateModal from './Threadgate' 18 - import * as CropImageModal from './crop-image/CropImage.web' 19 - import * as AltTextImageModal from './AltImage' 20 18 import * as EditImageModal from './EditImage' 21 - import * as ChangeHandleModal from './ChangeHandle' 19 + import * as EditProfileModal from './EditProfile' 22 20 import * as InviteCodesModal from './InviteCodes' 23 - import * as AddAppPassword from './AddAppPasswords' 24 21 import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings' 25 22 import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings' 26 - import * as VerifyEmailModal from './VerifyEmail' 27 - import * as ChangeEmailModal from './ChangeEmail' 28 - import * as ChangePasswordModal from './ChangePassword' 29 23 import * as LinkWarningModal from './LinkWarning' 30 - import * as EmbedConsentModal from './EmbedConsent' 24 + import * as ListAddUserModal from './ListAddRemoveUsers' 25 + import * as RepostModal from './Repost' 26 + import * as SelfLabelModal from './SelfLabel' 27 + import * as ThreadgateModal from './Threadgate' 28 + import * as UserAddRemoveLists from './UserAddRemoveLists' 29 + import * as VerifyEmailModal from './VerifyEmail' 31 30 32 31 export function ModalsContainer() { 33 32 const {isModalActive, activeModals} = useModals() ··· 112 111 element = <ChangePasswordModal.Component /> 113 112 } else if (modal.name === 'link-warning') { 114 113 element = <LinkWarningModal.Component {...modal} /> 115 - } else if (modal.name === 'embed-consent') { 116 - element = <EmbedConsentModal.Component {...modal} /> 117 114 } else { 118 115 return null 119 116 }
+67 -54
src/view/com/util/post-embeds/ExternalGifEmbed.tsx
··· 1 - import {EmbedPlayerParams, getGifDims} from 'lib/strings/embed-player' 2 1 import React from 'react' 3 - import {Image, ImageLoadEventData} from 'expo-image' 4 2 import { 5 3 ActivityIndicator, 6 4 GestureResponderEvent, ··· 9 7 StyleSheet, 10 8 View, 11 9 } from 'react-native' 12 - import {isIOS, isNative, isWeb} from '#/platform/detection' 10 + import {Image, ImageLoadEventData} from 'expo-image' 11 + import {AppBskyEmbedExternal} from '@atproto/api' 13 12 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 14 - import {useExternalEmbedsPrefs} from 'state/preferences' 15 - import {useModalControls} from 'state/modals' 16 - import {useLingui} from '@lingui/react' 17 13 import {msg} from '@lingui/macro' 18 - import {AppBskyEmbedExternal} from '@atproto/api' 14 + import {useLingui} from '@lingui/react' 15 + 16 + import {EmbedPlayerParams, getGifDims} from '#/lib/strings/embed-player' 17 + import {isIOS, isNative, isWeb} from '#/platform/detection' 18 + import {useExternalEmbedsPrefs} from '#/state/preferences' 19 + import {useDialogControl} from '#/components/Dialog' 20 + import {EmbedConsentDialog} from '#/components/dialogs/EmbedConsent' 19 21 20 22 export function ExternalGifEmbed({ 21 23 link, ··· 25 27 params: EmbedPlayerParams 26 28 }) { 27 29 const externalEmbedsPrefs = useExternalEmbedsPrefs() 28 - const {openModal} = useModalControls() 30 + 29 31 const {_} = useLingui() 32 + const consentDialogControl = useDialogControl() 30 33 31 34 const thumbHasLoaded = React.useRef(false) 32 35 const viewWidth = React.useRef(0) ··· 57 60 58 61 // Show consent if this is the first load 59 62 if (externalEmbedsPrefs?.[params.source] === undefined) { 60 - openModal({ 61 - name: 'embed-consent', 62 - source: params.source, 63 - onAccept: load, 64 - }) 63 + consentDialogControl.open() 65 64 return 66 65 } 67 66 // If the player isn't active, we want to activate it and prefetch the gif ··· 84 83 } 85 84 }) 86 85 }, 87 - [externalEmbedsPrefs, isPlayerActive, load, openModal, params.source], 86 + [ 87 + consentDialogControl, 88 + externalEmbedsPrefs, 89 + isPlayerActive, 90 + load, 91 + params.source, 92 + ], 88 93 ) 89 94 90 95 const onLoad = React.useCallback((e: ImageLoadEventData) => { ··· 98 103 }, []) 99 104 100 105 return ( 101 - <Pressable 102 - style={[ 103 - {height: imageDims.height}, 104 - styles.topRadius, 105 - styles.gifContainer, 106 - ]} 107 - onPress={onPlayPress} 108 - onLayout={onLayout} 109 - accessibilityRole="button" 110 - accessibilityHint={_(msg`Plays the GIF`)} 111 - accessibilityLabel={_(msg`Play ${link.title}`)}> 112 - {(!isPrefetched || !isAnimating) && ( // If we have not loaded or are not animating, show the overlay 113 - <View style={[styles.layer, styles.overlayLayer]}> 114 - <View style={[styles.overlayContainer, styles.topRadius]}> 115 - {!isAnimating || !isPlayerActive ? ( // Play button when not animating or not active 116 - <FontAwesomeIcon icon="play" size={42} color="white" /> 117 - ) : ( 118 - // Activity indicator while gif loads 119 - <ActivityIndicator size="large" color="white" /> 120 - )} 106 + <> 107 + <EmbedConsentDialog 108 + control={consentDialogControl} 109 + source={params.source} 110 + onAccept={load} 111 + /> 112 + 113 + <Pressable 114 + style={[ 115 + {height: imageDims.height}, 116 + styles.topRadius, 117 + styles.gifContainer, 118 + ]} 119 + onPress={onPlayPress} 120 + onLayout={onLayout} 121 + accessibilityRole="button" 122 + accessibilityHint={_(msg`Plays the GIF`)} 123 + accessibilityLabel={_(msg`Play ${link.title}`)}> 124 + {(!isPrefetched || !isAnimating) && ( // If we have not loaded or are not animating, show the overlay 125 + <View style={[styles.layer, styles.overlayLayer]}> 126 + <View style={[styles.overlayContainer, styles.topRadius]}> 127 + {!isAnimating || !isPlayerActive ? ( // Play button when not animating or not active 128 + <FontAwesomeIcon icon="play" size={42} color="white" /> 129 + ) : ( 130 + // Activity indicator while gif loads 131 + <ActivityIndicator size="large" color="white" /> 132 + )} 133 + </View> 121 134 </View> 122 - </View> 123 - )} 124 - <Image 125 - source={{ 126 - uri: 127 - !isPrefetched || (isWeb && !isAnimating) 128 - ? link.thumb 129 - : params.playerUri, 130 - }} // Web uses the thumb to control playback 131 - style={{flex: 1}} 132 - ref={imageRef} 133 - onLoad={onLoad} 134 - autoplay={isAnimating} 135 - contentFit="contain" 136 - accessibilityIgnoresInvertColors 137 - accessibilityLabel={link.title} 138 - accessibilityHint={link.title} 139 - cachePolicy={isIOS ? 'disk' : 'memory-disk'} // cant control playback with memory-disk on ios 140 - /> 141 - </Pressable> 135 + )} 136 + <Image 137 + source={{ 138 + uri: 139 + !isPrefetched || (isWeb && !isAnimating) 140 + ? link.thumb 141 + : params.playerUri, 142 + }} // Web uses the thumb to control playback 143 + style={{flex: 1}} 144 + ref={imageRef} 145 + onLoad={onLoad} 146 + autoplay={isAnimating} 147 + contentFit="contain" 148 + accessibilityIgnoresInvertColors 149 + accessibilityLabel={link.title} 150 + accessibilityHint={link.title} 151 + cachePolicy={isIOS ? 'disk' : 'memory-disk'} // cant control playback with memory-disk on ios 152 + /> 153 + </Pressable> 154 + </> 142 155 ) 143 156 } 144 157
+46 -40
src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx
··· 13 13 useAnimatedRef, 14 14 useFrameCallback, 15 15 } from 'react-native-reanimated' 16 + import {useSafeAreaInsets} from 'react-native-safe-area-context' 17 + import {WebView} from 'react-native-webview' 16 18 import {Image} from 'expo-image' 17 - import {WebView} from 'react-native-webview' 18 - import {useSafeAreaInsets} from 'react-native-safe-area-context' 19 + import {AppBskyEmbedExternal} from '@atproto/api' 19 20 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 20 21 import {msg} from '@lingui/macro' 21 22 import {useLingui} from '@lingui/react' 22 23 import {useNavigation} from '@react-navigation/native' 23 - import {AppBskyEmbedExternal} from '@atproto/api' 24 - import {EmbedPlayerParams, getPlayerAspect} from 'lib/strings/embed-player' 24 + 25 + import {NavigationProp} from '#/lib/routes/types' 26 + import {EmbedPlayerParams, getPlayerAspect} from '#/lib/strings/embed-player' 27 + import {isNative} from '#/platform/detection' 28 + import {useExternalEmbedsPrefs} from '#/state/preferences' 29 + import {atoms as a} from '#/alf' 30 + import {useDialogControl} from '#/components/Dialog' 31 + import {EmbedConsentDialog} from '#/components/dialogs/EmbedConsent' 25 32 import {EventStopper} from '../EventStopper' 26 - import {isNative} from 'platform/detection' 27 - import {NavigationProp} from 'lib/routes/types' 28 - import {useExternalEmbedsPrefs} from 'state/preferences' 29 - import {useModalControls} from 'state/modals' 30 33 31 34 interface ShouldStartLoadRequest { 32 35 url: string ··· 48 51 if (isPlayerActive && !isLoading) return null 49 52 50 53 return ( 51 - <View style={[styles.layer, styles.overlayLayer]}> 54 + <View style={[a.absolute, a.inset_0, styles.overlayLayer]}> 52 55 <Pressable 53 56 accessibilityRole="button" 54 57 accessibilityLabel={_(msg`Play Video`)} ··· 89 92 if (!isPlayerActive) return null 90 93 91 94 return ( 92 - <EventStopper style={[styles.layer, styles.playerLayer]}> 95 + <EventStopper style={[a.absolute, a.inset_0, styles.playerLayer]}> 93 96 <WebView 94 97 javaScriptEnabled={true} 95 98 onShouldStartLoadWithRequest={onShouldStartLoadWithRequest} ··· 119 122 const insets = useSafeAreaInsets() 120 123 const windowDims = useWindowDimensions() 121 124 const externalEmbedsPrefs = useExternalEmbedsPrefs() 122 - const {openModal} = useModalControls() 125 + const consentDialogControl = useDialogControl() 123 126 124 127 const [isPlayerActive, setPlayerActive] = React.useState(false) 125 128 const [isLoading, setIsLoading] = React.useState(true) ··· 187 190 event.preventDefault() 188 191 189 192 if (externalEmbedsPrefs?.[params.source] === undefined) { 190 - openModal({ 191 - name: 'embed-consent', 192 - source: params.source, 193 - onAccept: () => { 194 - setPlayerActive(true) 195 - }, 196 - }) 193 + consentDialogControl.open() 197 194 return 198 195 } 199 196 200 197 setPlayerActive(true) 201 198 }, 202 - [externalEmbedsPrefs, openModal, params.source], 199 + [externalEmbedsPrefs, consentDialogControl, params.source], 203 200 ) 201 + 202 + const onAcceptConsent = React.useCallback(() => { 203 + setPlayerActive(true) 204 + }, []) 204 205 205 206 return ( 206 - <Animated.View ref={viewRef} collapsable={false} style={[aspect]}> 207 - {link.thumb && (!isPlayerActive || isLoading) && ( 208 - <Image 209 - style={[{flex: 1}, styles.topRadius]} 210 - source={{uri: link.thumb}} 211 - accessibilityIgnoresInvertColors 207 + <> 208 + <EmbedConsentDialog 209 + control={consentDialogControl} 210 + source={params.source} 211 + onAccept={onAcceptConsent} 212 + /> 213 + 214 + <Animated.View ref={viewRef} collapsable={false} style={[aspect]}> 215 + {link.thumb && (!isPlayerActive || isLoading) && ( 216 + <Image 217 + style={[a.flex_1, styles.topRadius]} 218 + source={{uri: link.thumb}} 219 + accessibilityIgnoresInvertColors 220 + /> 221 + )} 222 + <PlaceholderOverlay 223 + isLoading={isLoading} 224 + isPlayerActive={isPlayerActive} 225 + onPress={onPlayPress} 226 + /> 227 + <Player 228 + isPlayerActive={isPlayerActive} 229 + params={params} 230 + onLoad={onLoad} 212 231 /> 213 - )} 214 - <PlaceholderOverlay 215 - isLoading={isLoading} 216 - isPlayerActive={isPlayerActive} 217 - onPress={onPlayPress} 218 - /> 219 - <Player isPlayerActive={isPlayerActive} params={params} onLoad={onLoad} /> 220 - </Animated.View> 232 + </Animated.View> 233 + </> 221 234 ) 222 235 } 223 236 ··· 225 238 topRadius: { 226 239 borderTopLeftRadius: 6, 227 240 borderTopRightRadius: 6, 228 - }, 229 - layer: { 230 - position: 'absolute', 231 - top: 0, 232 - left: 0, 233 - right: 0, 234 - bottom: 0, 235 241 }, 236 242 overlayContainer: { 237 243 flex: 1,