tangled
alpha
login
or
join now
ansxor.ca
/
witchsky.app
forked from
jollywhoppers.com/witchsky.app
1
fork
atom
Bluesky app fork with some witchin' additions 💫
1
fork
atom
overview
issues
pulls
pipelines
WIP, avi not working on web
Eric Bailey
2 years ago
eaf00816
3c8b3b47
+266
-176
3 changed files
expand all
collapse all
unified
split
src
components
dialogs
nudges
TenMillion.tsx
lib
canvas.ts
view
com
util
UserAvatar.tsx
+247
-176
src/components/dialogs/nudges/TenMillion.tsx
···
1
1
import React from 'react'
2
2
import {View} from 'react-native'
3
3
import ViewShot from 'react-native-view-shot'
4
4
+
import {Image} from 'expo-image'
4
5
import {moderateProfile} from '@atproto/api'
5
6
import {msg, Trans} from '@lingui/macro'
6
7
import {useLingui} from '@lingui/react'
7
8
9
9
+
import {getCanvas} from '#/lib/canvas'
8
10
import {sanitizeDisplayName} from '#/lib/strings/display-names'
9
11
import {sanitizeHandle} from '#/lib/strings/handles'
10
12
import {isNative} from '#/platform/detection'
···
32
34
import {Loader} from '#/components/Loader'
33
35
import {Text} from '#/components/Typography'
34
36
37
37
+
const DEBUG = false
35
38
const RATIO = 8 / 10
36
39
const WIDTH = 2000
37
40
const HEIGHT = WIDTH * RATIO
···
47
50
}
48
51
}
49
52
53
53
+
function Frame({children}: {children: React.ReactNode}) {
54
54
+
return (
55
55
+
<View
56
56
+
style={[
57
57
+
a.relative,
58
58
+
a.w_full,
59
59
+
a.overflow_hidden,
60
60
+
{
61
61
+
paddingTop: '80%',
62
62
+
},
63
63
+
]}>
64
64
+
{children}
65
65
+
</View>
66
66
+
)
67
67
+
}
68
68
+
50
69
export function TenMillion() {
51
70
const t = useTheme()
52
71
const lightTheme = useTheme('light')
···
54
73
const {controls} = useContext()
55
74
const {gtMobile} = useBreakpoints()
56
75
const {openComposer} = useComposerControls()
57
57
-
const imageRef = React.useRef<ViewShot>(null)
58
76
const {currentAccount} = useSession()
59
77
const {isLoading: isProfileLoading, data: profile} = useProfileQuery({
60
78
did: currentAccount!.did,
···
65
83
? moderateProfile(profile, moderationOpts)
66
84
: undefined
67
85
}, [profile, moderationOpts])
86
86
+
const [uri, setUri] = React.useState<string | null>(null)
68
87
69
69
-
const isLoading = isProfileLoading || !moderation || !profile
88
88
+
const isLoadingData = isProfileLoading || !moderation || !profile
89
89
+
const isLoadingImage = !uri
70
90
71
71
-
const userNumber = 56738
91
91
+
const userNumber = 56738 // TODO
92
92
+
93
93
+
const captureInProgress = React.useRef(false)
94
94
+
const imageRef = React.useRef<ViewShot>(null)
72
95
73
96
const share = () => {
74
74
-
if (imageRef.current && imageRef.current.capture) {
75
75
-
imageRef.current.capture().then(uri => {
76
76
-
controls.tenMillion.close(() => {
77
77
-
setTimeout(() => {
78
78
-
openComposer({
79
79
-
text: '10 milly, babyyy',
80
80
-
imageUris: [
81
81
-
{
82
82
-
uri,
83
83
-
width: WIDTH,
84
84
-
height: HEIGHT,
85
85
-
},
86
86
-
],
87
87
-
})
88
88
-
}, 1e3)
89
89
-
})
97
97
+
if (uri) {
98
98
+
controls.tenMillion.close(() => {
99
99
+
setTimeout(() => {
100
100
+
openComposer({
101
101
+
text: '10 milly, babyyy',
102
102
+
imageUris: [
103
103
+
{
104
104
+
uri,
105
105
+
width: WIDTH,
106
106
+
height: HEIGHT,
107
107
+
},
108
108
+
],
109
109
+
})
110
110
+
}, 1e3)
90
111
})
91
112
}
92
113
}
93
114
94
94
-
return (
95
95
-
<Dialog.Outer control={controls.tenMillion}>
96
96
-
<Dialog.Handle />
115
115
+
const onCanvasReady = async () => {
116
116
+
if (
117
117
+
imageRef.current &&
118
118
+
imageRef.current.capture &&
119
119
+
!captureInProgress.current
120
120
+
) {
121
121
+
captureInProgress.current = true
122
122
+
const uri = await imageRef.current.capture()
123
123
+
setUri(uri)
124
124
+
}
125
125
+
}
126
126
+
127
127
+
const download = async () => {
128
128
+
if (uri) {
129
129
+
const canvas = await getCanvas(uri)
130
130
+
const imgHref = canvas
131
131
+
.toDataURL('image/png')
132
132
+
.replace('image/png', 'image/octet-stream')
133
133
+
const link = document.createElement('a')
134
134
+
link.setAttribute('download', `Bluesky 10M Users.png`)
135
135
+
link.setAttribute('href', imgHref)
136
136
+
link.click()
137
137
+
}
138
138
+
}
97
139
98
98
-
<Dialog.ScrollableInner
99
99
-
label={_(msg`Ten Million`)}
100
100
-
style={[
101
101
-
{
102
102
-
padding: 0,
103
103
-
},
104
104
-
// gtMobile ? {width: 'auto', maxWidth: 400, minWidth: 200} : a.w_full,
105
105
-
]}>
106
106
-
<View
107
107
-
style={[
108
108
-
a.rounded_md,
109
109
-
a.overflow_hidden,
110
110
-
isNative && {
111
111
-
borderTopLeftRadius: 40,
112
112
-
borderTopRightRadius: 40,
140
140
+
const canvas = isLoadingData ? null : (
141
141
+
<View
142
142
+
style={[
143
143
+
a.absolute,
144
144
+
a.overflow_hidden,
145
145
+
DEBUG
146
146
+
? {
147
147
+
width: 600,
148
148
+
height: 600 * RATIO,
149
149
+
}
150
150
+
: {
151
151
+
width: 1,
152
152
+
height: 1,
113
153
},
114
114
-
]}>
115
115
-
<ThemeProvider theme="light">
116
116
-
<View
117
117
-
style={[
118
118
-
a.relative,
119
119
-
a.w_full,
120
120
-
a.overflow_hidden,
121
121
-
{
122
122
-
paddingTop: '80%',
123
123
-
},
124
124
-
]}>
125
125
-
<ViewShot
126
126
-
ref={imageRef}
127
127
-
options={{width: WIDTH, height: HEIGHT}}
128
128
-
style={[a.absolute, a.inset_0]}>
154
154
+
]}>
155
155
+
<View style={{width: 600}}>
156
156
+
<ThemeProvider theme="light">
157
157
+
<Frame>
158
158
+
<ViewShot
159
159
+
ref={imageRef}
160
160
+
options={{width: WIDTH, height: HEIGHT}}
161
161
+
style={[a.absolute, a.inset_0]}>
162
162
+
<View
163
163
+
style={[
164
164
+
a.absolute,
165
165
+
a.inset_0,
166
166
+
a.align_center,
167
167
+
a.justify_center,
168
168
+
{
169
169
+
top: -1,
170
170
+
bottom: -1,
171
171
+
left: -1,
172
172
+
right: -1,
173
173
+
paddingVertical: 32,
174
174
+
paddingHorizontal: 48,
175
175
+
},
176
176
+
]}>
177
177
+
<GradientFill gradient={tokens.gradients.bonfire} />
178
178
+
129
179
<View
130
180
style={[
131
131
-
a.absolute,
132
132
-
a.inset_0,
181
181
+
a.flex_1,
182
182
+
a.w_full,
133
183
a.align_center,
134
184
a.justify_center,
185
185
+
a.rounded_md,
135
186
{
136
136
-
top: -1,
137
137
-
bottom: -1,
138
138
-
left: -1,
139
139
-
right: -1,
140
140
-
paddingVertical: 32,
141
141
-
paddingHorizontal: 48,
187
187
+
backgroundColor: 'white',
188
188
+
shadowRadius: 32,
189
189
+
shadowOpacity: 0.1,
190
190
+
elevation: 24,
191
191
+
shadowColor: tokens.gradients.bonfire.values[0][1],
142
192
},
143
193
]}>
144
144
-
<GradientFill gradient={tokens.gradients.bonfire} />
194
194
+
<View
195
195
+
style={[
196
196
+
a.absolute,
197
197
+
a.px_xl,
198
198
+
a.py_xl,
199
199
+
{
200
200
+
top: 0,
201
201
+
left: 0,
202
202
+
},
203
203
+
]}>
204
204
+
<Logomark fill={t.palette.primary_500} width={36} />
205
205
+
</View>
145
206
146
146
-
{isLoading ? (
147
147
-
<Loader size="xl" fill="white" />
148
148
-
) : (
149
149
-
<View
207
207
+
{/* Centered content */}
208
208
+
<View
209
209
+
style={[
210
210
+
{
211
211
+
paddingBottom: 48,
212
212
+
},
213
213
+
]}>
214
214
+
<Text
150
215
style={[
151
151
-
a.flex_1,
152
152
-
a.w_full,
153
153
-
a.align_center,
154
154
-
a.justify_center,
155
155
-
a.rounded_md,
216
216
+
a.text_md,
217
217
+
a.font_bold,
218
218
+
a.text_center,
219
219
+
a.pb_xs,
220
220
+
lightTheme.atoms.text_contrast_medium,
221
221
+
]}>
222
222
+
<Trans>
223
223
+
Celebrating {formatCount(i18n, 10000000)} users
224
224
+
</Trans>{' '}
225
225
+
🎉
226
226
+
</Text>
227
227
+
<Text
228
228
+
style={[
229
229
+
a.relative,
230
230
+
a.text_center,
156
231
{
157
157
-
backgroundColor: 'white',
158
158
-
shadowRadius: 32,
159
159
-
shadowOpacity: 0.1,
160
160
-
elevation: 24,
161
161
-
shadowColor: tokens.gradients.bonfire.values[0][1],
232
232
+
fontStyle: 'italic',
233
233
+
fontSize: getFontSize(userNumber),
234
234
+
fontWeight: '900',
235
235
+
letterSpacing: -2,
162
236
},
163
237
]}>
164
164
-
<View
238
238
+
<Text
165
239
style={[
166
240
a.absolute,
167
167
-
a.px_xl,
168
168
-
a.py_xl,
169
241
{
170
170
-
top: 0,
171
171
-
left: 0,
242
242
+
color: t.palette.primary_500,
243
243
+
fontSize: 32,
244
244
+
left: -18,
245
245
+
top: 8,
172
246
},
173
247
]}>
174
174
-
<Logomark fill={t.palette.primary_500} width={36} />
175
175
-
</View>
248
248
+
#
249
249
+
</Text>
250
250
+
{i18n.number(userNumber)}
251
251
+
</Text>
252
252
+
</View>
253
253
+
{/* End centered content */}
176
254
177
177
-
{/* Centered content */}
178
178
-
<View
179
179
-
style={[
180
180
-
{
181
181
-
paddingBottom: 48,
182
182
-
},
183
183
-
]}>
184
184
-
<Text
185
185
-
style={[
186
186
-
a.text_md,
187
187
-
a.font_bold,
188
188
-
a.text_center,
189
189
-
a.pb_xs,
190
190
-
lightTheme.atoms.text_contrast_medium,
191
191
-
]}>
192
192
-
<Trans>
193
193
-
Celebrating {formatCount(i18n, 10000000)} users
194
194
-
</Trans>{' '}
195
195
-
🎉
255
255
+
<View
256
256
+
style={[
257
257
+
a.absolute,
258
258
+
a.px_xl,
259
259
+
a.py_xl,
260
260
+
{
261
261
+
bottom: 0,
262
262
+
left: 0,
263
263
+
right: 0,
264
264
+
},
265
265
+
]}>
266
266
+
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
267
267
+
<UserAvatar
268
268
+
size={36}
269
269
+
avatar={profile.avatar}
270
270
+
moderation={moderation.ui('avatar')}
271
271
+
onLoad={onCanvasReady}
272
272
+
/>
273
273
+
<View style={[a.gap_2xs, a.flex_1]}>
274
274
+
<Text style={[a.text_sm, a.font_bold]}>
275
275
+
{sanitizeDisplayName(
276
276
+
profile.displayName ||
277
277
+
sanitizeHandle(profile.handle),
278
278
+
moderation.ui('displayName'),
279
279
+
)}
196
280
</Text>
197
197
-
<Text
198
198
-
style={[
199
199
-
a.relative,
200
200
-
a.text_center,
201
201
-
{
202
202
-
fontStyle: 'italic',
203
203
-
fontSize: getFontSize(userNumber),
204
204
-
fontWeight: '900',
205
205
-
letterSpacing: -2,
206
206
-
},
207
207
-
]}>
281
281
+
<View style={[a.flex_row, a.justify_between]}>
208
282
<Text
209
283
style={[
210
210
-
a.absolute,
211
211
-
{
212
212
-
color: t.palette.primary_500,
213
213
-
fontSize: 32,
214
214
-
left: -18,
215
215
-
top: 8,
216
216
-
},
284
284
+
a.text_sm,
285
285
+
a.font_semibold,
286
286
+
lightTheme.atoms.text_contrast_medium,
217
287
]}>
218
218
-
#
288
288
+
{sanitizeHandle(profile.handle, '@')}
219
289
</Text>
220
220
-
{i18n.number(userNumber)}
221
221
-
</Text>
222
222
-
</View>
223
223
-
{/* End centered content */}
224
290
225
225
-
<View
226
226
-
style={[
227
227
-
a.absolute,
228
228
-
a.px_xl,
229
229
-
a.py_xl,
230
230
-
{
231
231
-
bottom: 0,
232
232
-
left: 0,
233
233
-
right: 0,
234
234
-
},
235
235
-
]}>
236
236
-
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
237
237
-
<UserAvatar
238
238
-
size={36}
239
239
-
avatar={profile.avatar}
240
240
-
moderation={moderation.ui('avatar')}
241
241
-
/>
242
242
-
<View style={[a.gap_2xs, a.flex_1]}>
243
243
-
<Text style={[a.text_sm, a.font_bold]}>
244
244
-
{sanitizeDisplayName(
245
245
-
profile.displayName ||
246
246
-
sanitizeHandle(profile.handle),
247
247
-
moderation.ui('displayName'),
248
248
-
)}
291
291
+
{profile.createdAt && (
292
292
+
<Text
293
293
+
style={[
294
294
+
a.text_sm,
295
295
+
a.font_semibold,
296
296
+
lightTheme.atoms.text_contrast_low,
297
297
+
]}>
298
298
+
{i18n.date(profile.createdAt, {
299
299
+
dateStyle: 'long',
300
300
+
})}
249
301
</Text>
250
250
-
<View style={[a.flex_row, a.justify_between]}>
251
251
-
<Text
252
252
-
style={[
253
253
-
a.text_sm,
254
254
-
a.font_semibold,
255
255
-
lightTheme.atoms.text_contrast_medium,
256
256
-
]}>
257
257
-
{sanitizeHandle(profile.handle, '@')}
258
258
-
</Text>
259
259
-
260
260
-
{profile.createdAt && (
261
261
-
<Text
262
262
-
style={[
263
263
-
a.text_sm,
264
264
-
a.font_semibold,
265
265
-
lightTheme.atoms.text_contrast_low,
266
266
-
]}>
267
267
-
{i18n.date(profile.createdAt, {
268
268
-
dateStyle: 'long',
269
269
-
})}
270
270
-
</Text>
271
271
-
)}
272
272
-
</View>
273
273
-
</View>
302
302
+
)}
274
303
</View>
275
304
</View>
276
305
</View>
277
277
-
)}
306
306
+
</View>
278
307
</View>
279
279
-
</ViewShot>
308
308
+
</View>
309
309
+
</ViewShot>
310
310
+
</Frame>
311
311
+
</ThemeProvider>
312
312
+
</View>
313
313
+
</View>
314
314
+
)
315
315
+
316
316
+
return (
317
317
+
<Dialog.Outer control={controls.tenMillion}>
318
318
+
<Dialog.Handle />
319
319
+
320
320
+
<Dialog.ScrollableInner
321
321
+
label={_(msg`Ten Million`)}
322
322
+
style={[
323
323
+
{
324
324
+
padding: 0,
325
325
+
},
326
326
+
]}>
327
327
+
<View
328
328
+
style={[
329
329
+
a.rounded_md,
330
330
+
a.overflow_hidden,
331
331
+
isNative && {
332
332
+
borderTopLeftRadius: 40,
333
333
+
borderTopRightRadius: 40,
334
334
+
},
335
335
+
]}>
336
336
+
<Frame>
337
337
+
<View
338
338
+
style={[a.absolute, a.inset_0, a.align_center, a.justify_center]}>
339
339
+
<GradientFill gradient={tokens.gradients.bonfire} />
340
340
+
{isLoadingData || isLoadingImage ? (
341
341
+
<Loader size="xl" fill="white" />
342
342
+
) : (
343
343
+
<Image
344
344
+
accessibilityIgnoresInvertColors
345
345
+
source={{uri}}
346
346
+
style={[a.w_full, a.h_full]}
347
347
+
/>
348
348
+
)}
280
349
</View>
281
281
-
</ThemeProvider>
350
350
+
</Frame>
351
351
+
352
352
+
{canvas}
282
353
283
354
<View style={[gtMobile ? a.p_2xl : a.p_xl]}>
284
355
<Text
···
321
392
variant="solid"
322
393
color="secondary"
323
394
shape="square"
324
324
-
onPress={share}>
395
395
+
onPress={download}>
325
396
<ButtonIcon icon={Share} />
326
397
</Button>
327
398
<Button
+15
src/lib/canvas.ts
···
1
1
+
export const getCanvas = (base64: string): Promise<HTMLCanvasElement> => {
2
2
+
return new Promise(resolve => {
3
3
+
const image = new Image()
4
4
+
image.onload = () => {
5
5
+
const canvas = document.createElement('canvas')
6
6
+
canvas.width = image.width
7
7
+
canvas.height = image.height
8
8
+
9
9
+
const ctx = canvas.getContext('2d')
10
10
+
ctx?.drawImage(image, 0, 0)
11
11
+
resolve(canvas)
12
12
+
}
13
13
+
image.src = base64
14
14
+
})
15
15
+
}
+4
src/view/com/util/UserAvatar.tsx
···
43
43
interface UserAvatarProps extends BaseUserAvatarProps {
44
44
moderation?: ModerationUI
45
45
usePlainRNImage?: boolean
46
46
+
onLoad?: () => void
46
47
}
47
48
48
49
interface EditableUserAvatarProps extends BaseUserAvatarProps {
···
174
175
avatar,
175
176
moderation,
176
177
usePlainRNImage = false,
178
178
+
onLoad,
177
179
}: UserAvatarProps): React.ReactNode => {
178
180
const pal = usePalette('default')
179
181
const backgroundColor = pal.colors.backgroundLight
···
224
226
uri: hackModifyThumbnailPath(avatar, size < 90),
225
227
}}
226
228
blurRadius={moderation?.blur ? BLUR_AMOUNT : 0}
229
229
+
onLoad={onLoad}
227
230
/>
228
231
) : (
229
232
<HighPriorityImage
···
234
237
uri: hackModifyThumbnailPath(avatar, size < 90),
235
238
}}
236
239
blurRadius={moderation?.blur ? BLUR_AMOUNT : 0}
240
240
+
onLoad={onLoad}
237
241
/>
238
242
)}
239
243
{alert}