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