Bluesky app fork with some witchin' additions 馃挮
at main 227 lines 7.1 kB view raw
1import {Suspense, useRef, useState} from 'react' 2import {View} from 'react-native' 3import type ViewShot from 'react-native-view-shot' 4import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker' 5import {createAssetAsync} from 'expo-media-library' 6import * as Sharing from 'expo-sharing' 7import {type AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api' 8import {msg} from '@lingui/core/macro' 9import {useLingui} from '@lingui/react' 10import {Trans} from '@lingui/react/macro' 11 12import {logger} from '#/logger' 13import {atoms as a, useBreakpoints} from '#/alf' 14import {Button, ButtonIcon, ButtonText} from '#/components/Button' 15import * as Dialog from '#/components/Dialog' 16import {type DialogControlProps} from '#/components/Dialog' 17import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ShareIcon} from '#/components/icons/ArrowOutOfBox' 18import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink' 19import {FloppyDisk_Stroke2_Corner0_Rounded as FloppyDiskIcon} from '#/components/icons/FloppyDisk' 20import {Loader} from '#/components/Loader' 21import {QrCode} from '#/components/StarterPack/QrCode' 22import * as Toast from '#/components/Toast' 23import {useAnalytics} from '#/analytics' 24import {IS_NATIVE, IS_WEB} from '#/env' 25import * as bsky from '#/types/bsky' 26 27export function QrCodeDialog({ 28 starterPack, 29 link, 30 control, 31}: { 32 starterPack: AppBskyGraphDefs.StarterPackView 33 link?: string 34 control: DialogControlProps 35}) { 36 const {_} = useLingui() 37 const ax = useAnalytics() 38 const {gtMobile} = useBreakpoints() 39 const [isSaveProcessing, setIsSaveProcessing] = useState(false) 40 const [isCopyProcessing, setIsCopyProcessing] = useState(false) 41 42 const ref = useRef<ViewShot>(null) 43 44 const getCanvas = (base64: string): Promise<HTMLCanvasElement> => { 45 return new Promise(resolve => { 46 const image = new Image() 47 image.onload = () => { 48 const canvas = document.createElement('canvas') 49 canvas.width = image.width 50 canvas.height = image.height 51 52 const ctx = canvas.getContext('2d') 53 ctx?.drawImage(image, 0, 0) 54 resolve(canvas) 55 } 56 image.src = base64 57 }) 58 } 59 60 const onSavePress = async () => { 61 ref.current?.capture?.().then(async (uri: string) => { 62 if (IS_NATIVE) { 63 const res = await requestMediaLibraryPermissionsAsync() 64 65 if (!res.granted) { 66 Toast.show( 67 _( 68 msg`You must grant access to your photo library to save a QR code`, 69 ), 70 ) 71 return 72 } 73 74 // Incase of a FS failure, don't crash the app 75 try { 76 await createAssetAsync(`file://${uri}`) 77 } catch (e: unknown) { 78 Toast.show(_(msg`An error occurred while saving the QR code!`), { 79 type: 'error', 80 }) 81 logger.error('Failed to save QR code', {error: e}) 82 return 83 } 84 } else { 85 setIsSaveProcessing(true) 86 87 if ( 88 !bsky.validate( 89 starterPack.record, 90 AppBskyGraphStarterpack.validateRecord, 91 ) 92 ) { 93 return 94 } 95 96 const canvas = await getCanvas(uri) 97 const imgHref = canvas 98 .toDataURL('image/png') 99 .replace('image/png', 'image/octet-stream') 100 101 const link = document.createElement('a') 102 link.setAttribute( 103 'download', 104 `${starterPack.record.name.replaceAll(' ', '_')}_Share_Card.png`, 105 ) 106 link.setAttribute('href', imgHref) 107 link.click() 108 } 109 110 ax.metric('starterPack:share', { 111 starterPack: starterPack.uri, 112 shareType: 'qrcode', 113 qrShareType: 'save', 114 }) 115 setIsSaveProcessing(false) 116 Toast.show( 117 IS_WEB 118 ? _(msg`QR code has been downloaded!`) 119 : _(msg`QR code saved to your camera roll!`), 120 ) 121 control.close() 122 }) 123 } 124 125 const onCopyPress = async () => { 126 setIsCopyProcessing(true) 127 ref.current?.capture?.().then(async (uri: string) => { 128 const canvas = await getCanvas(uri) 129 // @ts-expect-error web only 130 canvas.toBlob((blob: Blob) => { 131 const item = new ClipboardItem({'image/png': blob}) 132 navigator.clipboard.write([item]) 133 }) 134 135 ax.metric('starterPack:share', { 136 starterPack: starterPack.uri, 137 shareType: 'qrcode', 138 qrShareType: 'copy', 139 }) 140 Toast.show(_(msg`QR code copied to your clipboard!`)) 141 setIsCopyProcessing(false) 142 control.close() 143 }) 144 } 145 146 const onSharePress = async () => { 147 ref.current?.capture?.().then(async (uri: string) => { 148 control.close(() => { 149 Sharing.shareAsync(uri, {mimeType: 'image/png', UTI: 'image/png'}).then( 150 () => { 151 ax.metric('starterPack:share', { 152 starterPack: starterPack.uri, 153 shareType: 'qrcode', 154 qrShareType: 'share', 155 }) 156 }, 157 ) 158 }) 159 }) 160 } 161 162 return ( 163 <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}> 164 <Dialog.Handle /> 165 <Dialog.ScrollableInner 166 label={_(msg`Create a QR code for a starter pack`)}> 167 <View style={[a.flex_1, a.align_center, a.gap_5xl]}> 168 <Suspense fallback={<Loading />}> 169 {!link ? ( 170 <Loading /> 171 ) : ( 172 <> 173 <QrCode starterPack={starterPack} link={link} ref={ref} /> 174 <View 175 style={[ 176 a.w_full, 177 a.gap_md, 178 gtMobile && [a.flex_row, a.justify_center, a.flex_wrap], 179 ]}> 180 <Button 181 label={_(msg`Copy QR code`)} 182 color="primary_subtle" 183 size="large" 184 onPress={IS_WEB ? onCopyPress : onSharePress}> 185 <ButtonIcon 186 icon={ 187 isCopyProcessing 188 ? Loader 189 : IS_WEB 190 ? ChainLinkIcon 191 : ShareIcon 192 } 193 /> 194 <ButtonText> 195 {IS_WEB ? <Trans>Copy</Trans> : <Trans>Share</Trans>} 196 </ButtonText> 197 </Button> 198 <Button 199 label={_(msg`Save QR code`)} 200 color="secondary" 201 size="large" 202 onPress={onSavePress}> 203 <ButtonIcon 204 icon={isSaveProcessing ? Loader : FloppyDiskIcon} 205 /> 206 <ButtonText> 207 <Trans>Save</Trans> 208 </ButtonText> 209 </Button> 210 </View> 211 </> 212 )} 213 </Suspense> 214 </View> 215 <Dialog.Close /> 216 </Dialog.ScrollableInner> 217 </Dialog.Outer> 218 ) 219} 220 221function Loading() { 222 return ( 223 <View style={[a.align_center, a.justify_center, {minHeight: 400}]}> 224 <Loader size="xl" /> 225 </View> 226 ) 227}