forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
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}