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} 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}