···1-import React from 'react'
2import {View} from 'react-native'
3import {type ComAtprotoLabelDefs, ComAtprotoModerationDefs} from '@atproto/api'
04import {msg, Trans} from '@lingui/macro'
5import {useLingui} from '@lingui/react'
6import {useMutation} from '@tanstack/react-query'
···19import * as Dialog from '#/components/Dialog'
20import {InlineLinkText} from '#/components/Link'
21import {Text} from '#/components/Typography'
022import {Divider} from '../Divider'
23import {Loader} from '../Loader'
24···228 const sourceName = labeler
229 ? sanitizeHandle(labeler.creator.handle, '@')
230 : label.src
0231232 const {mutate, isPending} = useMutation({
233 mutationFn: async () => {
···252 )
253 },
254 onError: err => {
000000000255 logger.error('Failed to submit label appeal', {message: err})
256- Toast.show(_(msg`Failed to submit appeal, please try again.`), 'xmark')
257 },
258 onSuccess: () => {
259 control.close()
···285 </Trans>
286 </Text>
287 </View>
00000288 <View style={[a.my_md]}>
289 <Dialog.Input
290 label={_(msg`Text input field`)}
···1+import React, {useState} from 'react'
2import {View} from 'react-native'
3import {type ComAtprotoLabelDefs, ComAtprotoModerationDefs} from '@atproto/api'
4+import {XRPCError} from '@atproto/xrpc'
5import {msg, Trans} from '@lingui/macro'
6import {useLingui} from '@lingui/react'
7import {useMutation} from '@tanstack/react-query'
···20import * as Dialog from '#/components/Dialog'
21import {InlineLinkText} from '#/components/Link'
22import {Text} from '#/components/Typography'
23+import {Admonition} from '../Admonition'
24import {Divider} from '../Divider'
25import {Loader} from '../Loader'
26···230 const sourceName = labeler
231 ? sanitizeHandle(labeler.creator.handle, '@')
232 : label.src
233+ const [error, setError] = useState<string | null>(null)
234235 const {mutate, isPending} = useMutation({
236 mutationFn: async () => {
···255 )
256 },
257 onError: err => {
258+ if (err instanceof XRPCError && err.error === 'AlreadyAppealed') {
259+ setError(
260+ _(
261+ msg`You've already appealed this label and it's being reviewed by our moderation team.`,
262+ ),
263+ )
264+ } else {
265+ setError(_(msg`Failed to submit appeal, please try again.`))
266+ }
267 logger.error('Failed to submit label appeal', {message: err})
0268 },
269 onSuccess: () => {
270 control.close()
···296 </Trans>
297 </Text>
298 </View>
299+ {error && (
300+ <Admonition type="error" style={[a.mt_sm]}>
301+ {error}
302+ </Admonition>
303+ )}
304 <View style={[a.my_md]}>
305 <Dialog.Input
306 label={_(msg`Text input field`)}
+34-2
src/lib/media/manip.ts
···115 // as the starting image, or put it directly into the album
116 const album = await MediaLibrary.getAlbumAsync(ALBUM_NAME)
117 if (album) {
118- // if album exists, put the image straight in there
119- await MediaLibrary.createAssetAsync(imagePath, album)
0000000000000000000000000000000120 } else {
121 // otherwise, create album with asset (albums must always have at least one asset)
122 await MediaLibrary.createAlbumAsync(
···133 logger.error(err instanceof Error ? err : String(err), {
134 message: 'Failed to save image to media library',
135 })
0136 } finally {
137 safeDeleteAsync(imagePath)
138 }
···115 // as the starting image, or put it directly into the album
116 const album = await MediaLibrary.getAlbumAsync(ALBUM_NAME)
117 if (album) {
118+ // try and migrate if needed
119+ try {
120+ if (await MediaLibrary.albumNeedsMigrationAsync(album)) {
121+ await MediaLibrary.migrateAlbumIfNeededAsync(album)
122+ }
123+ } catch (err) {
124+ logger.info('Attempted and failed to migrate album', {
125+ safeMessage: err,
126+ })
127+ }
128+129+ try {
130+ // if album exists, put the image straight in there
131+ await MediaLibrary.createAssetAsync(imagePath, album)
132+ } catch (err) {
133+ logger.info('Failed to create asset', {safeMessage: err})
134+ // however, it's possible that we don't have write permission to the album
135+ // try making a new one!
136+ try {
137+ await MediaLibrary.createAlbumAsync(
138+ ALBUM_NAME,
139+ undefined,
140+ undefined,
141+ imagePath,
142+ )
143+ } catch (err2) {
144+ logger.info('Failed to create asset in a fresh album', {
145+ safeMessage: err2,
146+ })
147+ // ... and if all else fails, just put it in DCIM
148+ await MediaLibrary.createAssetAsync(imagePath)
149+ }
150+ }
151 } else {
152 // otherwise, create album with asset (albums must always have at least one asset)
153 await MediaLibrary.createAlbumAsync(
···164 logger.error(err instanceof Error ? err : String(err), {
165 message: 'Failed to save image to media library',
166 })
167+ throw err
168 } finally {
169 safeDeleteAsync(imagePath)
170 }
···46import {atoms as a, useTheme} from '#/alf'
47import {colors} from '#/components/Admonition'
48import {Button} from '#/components/Button'
049import {CalendarClock_Stroke2_Corner0_Rounded as CalendarClockIcon} from '#/components/icons/CalendarClock'
50import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
51import {InlineLinkText, Link} from '#/components/Link'
···529 />
530 </FeedFeedbackProvider>
531 </View>
0532 </View>
533 </View>
534 </>
···46import {atoms as a, useTheme} from '#/alf'
47import {colors} from '#/components/Admonition'
48import {Button} from '#/components/Button'
49+import {DebugFieldDisplay} from '#/components/DebugFieldDisplay'
50import {CalendarClock_Stroke2_Corner0_Rounded as CalendarClockIcon} from '#/components/icons/CalendarClock'
51import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
52import {InlineLinkText, Link} from '#/components/Link'
···530 />
531 </FeedFeedbackProvider>
532 </View>
533+ <DebugFieldDisplay subject={post} />
534 </View>
535 </View>
536 </>
···30 REPLY_LINE_WIDTH,
31} from '#/screens/PostThread/const'
32import {atoms as a, useTheme} from '#/alf'
033import {useInteractionState} from '#/components/hooks/useInteractionState'
34import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
35import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
···335 logContext="PostThreadItem"
336 threadgateRecord={threadgateRecord}
337 />
0338 </View>
339 </View>
340 </PostHider>
···30 REPLY_LINE_WIDTH,
31} from '#/screens/PostThread/const'
32import {atoms as a, useTheme} from '#/alf'
33+import {DebugFieldDisplay} from '#/components/DebugFieldDisplay'
34import {useInteractionState} from '#/components/hooks/useInteractionState'
35import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
36import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
···336 logContext="PostThreadItem"
337 threadgateRecord={threadgateRecord}
338 />
339+ <DebugFieldDisplay subject={post} />
340 </View>
341 </View>
342 </PostHider>
···29 TREE_INDENT,
30} from '#/screens/PostThread/const'
31import {atoms as a, useTheme} from '#/alf'
032import {useInteractionState} from '#/components/hooks/useInteractionState'
33import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
34import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
···376 logContext="PostThreadItem"
377 threadgateRecord={threadgateRecord}
378 />
0379 </View>
380 </View>
381 </View>
···29 TREE_INDENT,
30} from '#/screens/PostThread/const'
31import {atoms as a, useTheme} from '#/alf'
32+import {DebugFieldDisplay} from '#/components/DebugFieldDisplay'
33import {useInteractionState} from '#/components/hooks/useInteractionState'
34import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
35import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
···377 logContext="PostThreadItem"
378 threadgateRecord={threadgateRecord}
379 />
380+ <DebugFieldDisplay subject={post} />
381 </View>
382 </View>
383 </View>
···26import {atoms as a, platform, useBreakpoints, useTheme} from '#/alf'
27import {SubscribeProfileButton} from '#/components/activity-notifications/SubscribeProfileButton'
28import {Button, ButtonIcon, ButtonText} from '#/components/Button'
029import {useDialogControl} from '#/components/Dialog'
30import {MessageProfileButton} from '#/components/dms/MessageProfileButton'
31import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
···320 )}
321 </View>
322 )}
00323 </View>
324325 <Prompt.Basic
···26import {atoms as a, platform, useBreakpoints, useTheme} from '#/alf'
27import {SubscribeProfileButton} from '#/components/activity-notifications/SubscribeProfileButton'
28import {Button, ButtonIcon, ButtonText} from '#/components/Button'
29+import {DebugFieldDisplay} from '#/components/DebugFieldDisplay'
30import {useDialogControl} from '#/components/Dialog'
31import {MessageProfileButton} from '#/components/dms/MessageProfileButton'
32import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
···321 )}
322 </View>
323 )}
324+325+ <DebugFieldDisplay subject={profile} />
326 </View>
327328 <Prompt.Basic
+12-1
src/screens/Profile/Header/SuggestedFollows.tsx
···28 actorDid: string
29}) {
30 const gate = useGate()
000003132 /* NOTE (caidanw):
33 * Android does not work well with this feature yet.
···4041 return (
42 <AccordionAnimation isExpanded={isExpanded}>
43- <ProfileHeaderSuggestedFollows actorDid={actorDid} />
00000044 </AccordionAnimation>
45 )
46}
···28 actorDid: string
29}) {
30 const gate = useGate()
31+ const {isLoading, data, error} = useSuggestedFollowsByActorQuery({
32+ did: actorDid,
33+ })
34+35+ if (!data?.suggestions?.length) return null
3637 /* NOTE (caidanw):
38 * Android does not work well with this feature yet.
···4546 return (
47 <AccordionAnimation isExpanded={isExpanded}>
48+ <ProfileGrid
49+ isSuggestionsLoading={isLoading}
50+ profiles={data.suggestions}
51+ recId={data.recId}
52+ error={error}
53+ viewContext="profileHeader"
54+ />
55 </AccordionAnimation>
56 )
57}