diff --git a/src/Navigation.tsx b/src/Navigation.tsx index fa33a9d56..46e725c6f 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -136,6 +136,20 @@ import {router} from '#/routes' import {Referrer} from '../modules/expo-bluesky-swiss-army' import {useAccountSwitcher} from './lib/hooks/useAccountSwitcher' import {useNonReactiveCallback} from './lib/hooks/useNonReactiveCallback' +import {ProfileSearchScreen} from './screens/Profile/ProfileSearch' +import {AboutSettingsScreen} from './screens/Settings/AboutSettings' +import {AccessibilitySettingsScreen} from './screens/Settings/AccessibilitySettings' +import {AccountSettingsScreen} from './screens/Settings/AccountSettings' +import {AppPasswordsScreen} from './screens/Settings/AppPasswords' +import {ContentAndMediaSettingsScreen} from './screens/Settings/ContentAndMediaSettings' +import {DeerSettingsScreen} from './screens/Settings/DeerSettings' +import {ExternalMediaPreferencesScreen} from './screens/Settings/ExternalMediaPreferences' +import {FollowingFeedPreferencesScreen} from './screens/Settings/FollowingFeedPreferences' +import {LanguageSettingsScreen} from './screens/Settings/LanguageSettings' +import {PrivacyAndSecuritySettingsScreen} from './screens/Settings/PrivacyAndSecuritySettings' +import {SettingsScreen} from './screens/Settings/Settings' +import {ThreadPreferencesScreen} from './screens/Settings/ThreadPreferences' +import TopicScreen from './screens/Topic' import {useLoggedOutViewControls} from './state/shell/logged-out' import {useCloseAllActiveElements} from './state/util' @@ -384,6 +398,14 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { requireAuth: true, }} /> + DeerSettingsScreen} + options={{ + title: title(msg`Deer Settings`), + requireAuth: true, + }} + /> AppearanceSettingsScreen} diff --git a/src/components/Link.tsx b/src/components/Link.tsx index 2b533b599..739af80f2 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -20,6 +20,7 @@ import { } from '#/lib/strings/url-helpers' import {isNative, isWeb} from '#/platform/detection' import {useModalControls} from '#/state/modals' +import {useGoLinksEnabled} from '#/state/preferences' import {atoms as a, flatten, type TextStyleProp, useTheme, web} from '#/alf' import {Button, type ButtonProps} from '#/components/Button' import {useInteractionState} from '#/components/hooks/useInteractionState' @@ -117,6 +118,8 @@ export function useLink({ const {linkWarningDialogControl} = useGlobalDialogsControlContext() const openLink = useOpenLink() + const goLinksEnabled = useGoLinksEnabled() + const onPress = React.useCallback( (e: GestureResponderEvent) => { const exitEarlyIfFalse = outerOnPress?.(e) @@ -141,7 +144,8 @@ export function useLink({ }) } else { if (isExternal) { - openLink(href, overridePresentation, shouldProxy) + // openLink(href, overridePresentation, shouldProxy) + openLink(href, overridePresentation, goLinksEnabled && shouldProxy) } else { const shouldOpenInNewTab = shouldClickOpenNewTab(e) @@ -214,6 +218,7 @@ export function useLink({ overridePresentation, shouldProxy, linkWarningDialogControl, + goLinksEnabled, ], ) diff --git a/src/lib/statsig/statsig.tsx b/src/lib/statsig/statsig.tsx index 2f18c071f..52ef902e2 100644 --- a/src/lib/statsig/statsig.tsx +++ b/src/lib/statsig/statsig.tsx @@ -129,6 +129,22 @@ type GateOptions = { dangerouslyDisableExposureLogging?: boolean } +export function useGatesCache(): Map { + const cache = React.useContext(GateCache) + if (!cache) { + throw Error('useGate() cannot be called outside StatsigProvider.') + } + return cache +} + +function writeDeerGateCache(cache: Map) { + device.set(['deerGateCache'], JSON.stringify(Object.fromEntries(cache))) +} + +export function resetDeerGateCache() { + writeDeerGateCache(new Map()) +} + export function useGate(): (gateName: Gate, options?: GateOptions) => boolean { const cache = React.useContext(GateCache) if (!cache) { @@ -149,6 +165,7 @@ export function useGate(): (gateName: Gate, options?: GateOptions) => boolean { } } cache.set(gateName, value) + writeDeerGateCache(cache) return value }, [cache], @@ -172,6 +189,7 @@ export function useDangerousSetGate(): ( const dangerousSetGate = React.useCallback( (gateName: Gate, value: boolean) => { cache.set(gateName, value) + writeDeerGateCache(cache) }, [cache], ) diff --git a/src/routes.ts b/src/routes.ts index 1ed913bb2..6c93b2d28 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -47,6 +47,7 @@ export const router = new Router({ PreferencesThreads: '/settings/threads', PreferencesExternalEmbeds: '/settings/external-embeds', AccessibilitySettings: '/settings/accessibility', + DeerSettings: '/settings/deer', AppearanceSettings: '/settings/appearance', SavedFeeds: '/settings/saved-feeds', AccountSettings: '/settings/account', diff --git a/src/screens/Settings/DeerSettings.tsx b/src/screens/Settings/DeerSettings.tsx new file mode 100644 index 000000000..27acd3b75 --- /dev/null +++ b/src/screens/Settings/DeerSettings.tsx @@ -0,0 +1,181 @@ +import {useState} from 'react' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {type NativeStackScreenProps} from '@react-navigation/native-stack' + +import {type CommonNavigatorParams} from '#/lib/routes/types' +import {type Gate} from '#/lib/statsig/gates' +import { + resetDeerGateCache, + useDangerousSetGate, + useGatesCache, +} from '#/lib/statsig/statsig' +import {useGoLinksEnabled, useSetGoLinksEnabled} from '#/state/preferences' +import { + useConstellationEnabled, + useSetConstellationEnabled, +} from '#/state/preferences/constellation-enabled' +import { + useDirectFetchRecords, + useSetDirectFetchRecords, +} from '#/state/preferences/direct-fetch-records' +import * as SettingsList from '#/screens/Settings/components/SettingsList' +import {atoms as a} from '#/alf' +import {Admonition} from '#/components/Admonition' +import * as Toggle from '#/components/forms/Toggle' +import {Atom_Stroke2_Corner0_Rounded as DeerIcon} from '#/components/icons/Atom' +import {Eye_Stroke2_Corner0_Rounded as VisibilityIcon} from '#/components/icons/Eye' +import {PaintRoller_Stroke2_Corner2_Rounded as PaintRollerIcon} from '#/components/icons/PaintRoller' +import * as Layout from '#/components/Layout' + +type Props = NativeStackScreenProps + +export function DeerSettingsScreen({}: Props) { + const {_} = useLingui() + + const goLinksEnabled = useGoLinksEnabled() + const setGoLinksEnabled = useSetGoLinksEnabled() + + const constellationEnabled = useConstellationEnabled() + const setConstellationEnabled = useSetConstellationEnabled() + + const directFetchRecords = useDirectFetchRecords() + const setDirectFetchRecords = useSetDirectFetchRecords() + + const [gates, setGatesView] = useState(Object.fromEntries(useGatesCache())) + const dangerousSetGate = useDangerousSetGate() + const setGate = (gate: Gate, value: boolean) => { + dangerousSetGate(gate, value) + setGatesView({ + ...gates, + [gate]: value, + }) + } + + return ( + + + + + + Deer + + + + + + + + + + Redirects + + setGoLinksEnabled(value)} + style={[a.w_full]}> + + Redirect through go.bsky.app + + + + + + + + + Visibility + + setDirectFetchRecords(value)} + style={[a.w_full]}> + + + Fetch records directly from PDS to see through quote blocks + + + + + setConstellationEnabled(value)} + style={[a.w_full]}> + + + TODO: Fall back to constellation api to find blocked replies + + + + + + + + + + Tweaks + + {}} + disabled={true} + style={[a.w_full]}> + + 🚧 under construction... + + + + + + + + + Gates + + {Object.entries(gates).map(([gate, status]) => ( + setGate(gate as Gate, value)} + style={[a.w_full]}> + {gate} + + + ))} + { + resetDeerGateCache() + setGatesView({}) + }} + /> + + + + + + These settings might summon nasel demons! Restart the app after + changing if anything breaks. + + + + + + + ) +} diff --git a/src/screens/Settings/Settings.tsx b/src/screens/Settings/Settings.tsx index 106502f7d..3cf698dd5 100644 --- a/src/screens/Settings/Settings.tsx +++ b/src/screens/Settings/Settings.tsx @@ -40,6 +40,7 @@ import {Button, ButtonText} from '#/components/Button' import {useDialogControl} from '#/components/Dialog' import {SwitchAccountDialog} from '#/components/dialogs/SwitchAccount' import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility' +import {Atom_Stroke2_Corner0_Rounded as DeerIcon} from '#/components/icons/Atom' import {Bell_Stroke2_Corner0_Rounded as NotificationIcon} from '#/components/icons/Bell' import {BubbleInfo_Stroke2_Corner2_Rounded as BubbleInfoIcon} from '#/components/icons/BubbleInfo' import {ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon} from '#/components/icons/Chevron' @@ -215,6 +216,12 @@ export function SettingsScreen({}: Props) { Appearance + + + + Deer + + diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts index 1381f66b5..77d2deeb6 100644 --- a/src/state/persisted/schema.ts +++ b/src/state/persisted/schema.ts @@ -123,6 +123,13 @@ const schema = z.object({ kawaii: z.boolean().optional(), hasCheckedForStarterPack: z.boolean().optional(), subtitlesEnabled: z.boolean().optional(), + + // deer + goLinksEnabled: z.boolean().optional(), + constellationEnabled: z.boolean().optional(), + directFetchRecords: z.boolean().optional(), + unfollowConfirm: z.boolean().optional(), + /** @deprecated */ mutedThreads: z.array(z.string()), trendingDisabled: z.boolean().optional(), @@ -174,6 +181,12 @@ export const defaults: Schema = { subtitlesEnabled: true, trendingDisabled: false, trendingVideoDisabled: false, + + // deer + goLinksEnabled: true, + constellationEnabled: false, + directFetchRecords: false, + unfollowConfirm: false, } export function tryParse(rawData: string): Schema | undefined { diff --git a/src/state/preferences/constellation-enabled.tsx b/src/state/preferences/constellation-enabled.tsx new file mode 100644 index 000000000..6e2fb992c --- /dev/null +++ b/src/state/preferences/constellation-enabled.tsx @@ -0,0 +1,52 @@ +import React from 'react' + +import * as persisted from '#/state/persisted' + +type StateContext = persisted.Schema['constellationEnabled'] +type SetContext = (v: persisted.Schema['constellationEnabled']) => void + +const stateContext = React.createContext( + persisted.defaults.constellationEnabled, +) +const setContext = React.createContext( + (_: persisted.Schema['constellationEnabled']) => {}, +) + +export function Provider({children}: React.PropsWithChildren<{}>) { + const [state, setState] = React.useState( + persisted.get('constellationEnabled'), + ) + + const setStateWrapped = React.useCallback( + (constellationEnabled: persisted.Schema['constellationEnabled']) => { + setState(constellationEnabled) + persisted.write('constellationEnabled', constellationEnabled) + }, + [setState], + ) + + React.useEffect(() => { + return persisted.onUpdate( + 'constellationEnabled', + nextConstellationEnabled => { + setState(nextConstellationEnabled) + }, + ) + }, [setStateWrapped]) + + return ( + + + {children} + + + ) +} + +export function useConstellationEnabled() { + return React.useContext(stateContext) +} + +export function useSetConstellationEnabled() { + return React.useContext(setContext) +} diff --git a/src/state/preferences/direct-fetch-records.tsx b/src/state/preferences/direct-fetch-records.tsx new file mode 100644 index 000000000..72e515ea2 --- /dev/null +++ b/src/state/preferences/direct-fetch-records.tsx @@ -0,0 +1,47 @@ +import React from 'react' + +import * as persisted from '#/state/persisted' + +type StateContext = persisted.Schema['directFetchRecords'] +type SetContext = (v: persisted.Schema['directFetchRecords']) => void + +const stateContext = React.createContext( + persisted.defaults.directFetchRecords, +) +const setContext = React.createContext( + (_: persisted.Schema['directFetchRecords']) => {}, +) + +export function Provider({children}: React.PropsWithChildren<{}>) { + const [state, setState] = React.useState(persisted.get('directFetchRecords')) + + const setStateWrapped = React.useCallback( + (directFetchRecords: persisted.Schema['directFetchRecords']) => { + setState(directFetchRecords) + persisted.write('directFetchRecords', directFetchRecords) + }, + [setState], + ) + + React.useEffect(() => { + return persisted.onUpdate('directFetchRecords', nextDirectFetchRecords => { + setState(nextDirectFetchRecords) + }) + }, [setStateWrapped]) + + return ( + + + {children} + + + ) +} + +export function useDirectFetchRecords() { + return React.useContext(stateContext) +} + +export function useSetDirectFetchRecords() { + return React.useContext(setContext) +} diff --git a/src/state/preferences/go-links-enabled.tsx b/src/state/preferences/go-links-enabled.tsx new file mode 100644 index 000000000..05d26035c --- /dev/null +++ b/src/state/preferences/go-links-enabled.tsx @@ -0,0 +1,47 @@ +import React from 'react' + +import * as persisted from '#/state/persisted' + +type StateContext = persisted.Schema['goLinksEnabled'] +type SetContext = (v: persisted.Schema['goLinksEnabled']) => void + +const stateContext = React.createContext( + persisted.defaults.goLinksEnabled, +) +const setContext = React.createContext( + (_: persisted.Schema['goLinksEnabled']) => {}, +) + +export function Provider({children}: React.PropsWithChildren<{}>) { + const [state, setState] = React.useState(persisted.get('goLinksEnabled')) + + const setStateWrapped = React.useCallback( + (goLinksEnabled: persisted.Schema['goLinksEnabled']) => { + setState(goLinksEnabled) + persisted.write('goLinksEnabled', goLinksEnabled) + }, + [setState], + ) + + React.useEffect(() => { + return persisted.onUpdate('goLinksEnabled', nextGoLinksEnabled => { + setState(nextGoLinksEnabled) + }) + }, [setStateWrapped]) + + return ( + + + {children} + + + ) +} + +export function useGoLinksEnabled() { + return React.useContext(stateContext) +} + +export function useSetGoLinksEnabled() { + return React.useContext(setContext) +} diff --git a/src/state/preferences/index.tsx b/src/state/preferences/index.tsx index 740ea4bf3..132561dd8 100644 --- a/src/state/preferences/index.tsx +++ b/src/state/preferences/index.tsx @@ -2,8 +2,11 @@ import type React from 'react' import {Provider as AltTextRequiredProvider} from './alt-text-required' import {Provider as AutoplayProvider} from './autoplay' +import {Provider as ConstellationProvider} from './constellation-enabled' +import {Provider as DirectFetchRecordsProvider} from './direct-fetch-records' import {Provider as DisableHapticsProvider} from './disable-haptics' import {Provider as ExternalEmbedsProvider} from './external-embeds-prefs' +import {Provider as GoLinksProvider} from './go-links-enabled' import {Provider as HiddenPostsProvider} from './hidden-posts' import {Provider as InAppBrowserProvider} from './in-app-browser' import {Provider as KawaiiProvider} from './kawaii' @@ -23,6 +26,7 @@ export { useExternalEmbedsPrefs, useSetExternalEmbedPref, } from './external-embeds-prefs' +export {useGoLinksEnabled, useSetGoLinksEnabled} from './go-links-enabled' export * from './hidden-posts' export {useLabelDefinitions} from './label-defs' export {useLanguagePrefs, useLanguagePrefsApi} from './languages' @@ -32,25 +36,31 @@ export function Provider({children}: React.PropsWithChildren<{}>) { return ( - - - - - - - - - - {children} - - - - - - - - - + + + + + + + + + + + + + {children} + + + + + + + + + + + + ) diff --git a/src/state/queries/direct-fetch-record.ts b/src/state/queries/direct-fetch-record.ts new file mode 100644 index 000000000..3a801e076 --- /dev/null +++ b/src/state/queries/direct-fetch-record.ts @@ -0,0 +1,75 @@ +import {type AppBskyEmbedRecord, AppBskyFeedPost, AtUri} from '@atproto/api' +import {type ProfileViewBasic} from '@atproto/api/dist/client/types/app/bsky/actor/defs' +import {useQuery} from '@tanstack/react-query' + +import {retry} from '#/lib/async/retry' +import {STALE} from '#/state/queries' +import {useAgent} from '#/state/session' +import * as bsky from '#/types/bsky' + +const RQKEY_ROOT = 'direct-fetch-record' +export const RQKEY = (uri: string) => [RQKEY_ROOT, uri] + +export function useDirectFetchRecord({ + uri, + enabled, +}: { + uri: string + enabled?: boolean +}) { + const agent = useAgent() + return useQuery({ + staleTime: STALE.HOURS.ONE, + queryKey: RQKEY(uri || ''), + async queryFn() { + const urip = new AtUri(uri) + + if (!urip.host.startsWith('did:')) { + const res = await agent.resolveHandle({ + handle: urip.host, + }) + urip.host = res.data.did + } + + try { + // TODO: parallel, series fetch sucks there isn't a dependency + const profile = (await agent.getProfile({actor: urip.host})).data + const {data} = await retry( + 2, + e => { + if (e.message.includes(`Could not locate record:`)) { + return false + } + return true + }, + () => + agent.api.com.atproto.repo.getRecord({ + repo: urip.host, + collection: 'app.bsky.feed.post', + rkey: urip.rkey, + }), + ) + if ( + data.value && + bsky.validate(data.value, AppBskyFeedPost.validateRecord) + ) { + const record = data.value + return { + $type: 'app.bsky.embed.record#viewRecord', + uri, + author: profile as ProfileViewBasic, + cid: '', + value: record, + indexedAt: record.createdAt, + } satisfies AppBskyEmbedRecord.ViewRecord + } else { + return undefined + } + } catch (e) { + console.error(e) + return undefined + } + }, + enabled: enabled && !!uri, + }) +} diff --git a/src/storage/schema.ts b/src/storage/schema.ts index d562d9fae..90c8d57f4 100644 --- a/src/storage/schema.ts +++ b/src/storage/schema.ts @@ -43,6 +43,9 @@ export type Device = { */ policyUpdateDebugOverride?: boolean [PolicyUpdate202508]?: boolean + + // deer + deerGateCache: string } export type Account = { diff --git a/src/view/com/util/post-embeds/QuoteEmbed.tsx b/src/view/com/util/post-embeds/QuoteEmbed.tsx new file mode 100644 index 000000000..00686d97c --- /dev/null +++ b/src/view/com/util/post-embeds/QuoteEmbed.tsx @@ -0,0 +1,415 @@ +import React from 'react' +import { + type StyleProp, + StyleSheet, + TouchableOpacity, + View, + type ViewStyle, +} from 'react-native' +import { + AppBskyEmbedExternal, + AppBskyEmbedImages, + AppBskyEmbedRecord, + AppBskyEmbedRecordWithMedia, + AppBskyEmbedVideo, + type AppBskyFeedDefs, + AppBskyFeedPost, + moderatePost, + type ModerationDecision, + RichText as RichTextAPI, +} from '@atproto/api' +import {AtUri} from '@atproto/api' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useQueryClient} from '@tanstack/react-query' + +import {HITSLOP_20} from '#/lib/constants' +import {usePalette} from '#/lib/hooks/usePalette' +import {InfoCircleIcon} from '#/lib/icons' +import {makeProfileLink} from '#/lib/routes/links' +import {s} from '#/lib/styles' +import {useDirectFetchRecords} from '#/state/preferences/direct-fetch-records' +import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {useDirectFetchRecord} from '#/state/queries/direct-fetch-record' +import {precacheProfile} from '#/state/queries/profile' +import {useResolveLinkQuery} from '#/state/queries/resolve-link' +import {useSession} from '#/state/session' +import {atoms as a, useTheme} from '#/alf' +import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlashIcon} from '#/components/icons/EyeSlash' +import {RichText} from '#/components/RichText' +import {SubtleWebHover} from '#/components/SubtleWebHover' +import * as bsky from '#/types/bsky' +import {ContentHider} from '../../../../components/moderation/ContentHider' +import {PostAlerts} from '../../../../components/moderation/PostAlerts' +import {Link} from '../Link' +import {PostMeta} from '../PostMeta' +import {Text} from '../text/Text' +import {PostEmbeds} from '.' +import {type QuoteEmbedViewContext} from './types' + +export function MaybeQuoteEmbed({ + embed, + onOpen, + style, + allowNestedQuotes, + viewContext, +}: { + embed: AppBskyEmbedRecord.View + onOpen?: () => void + style?: StyleProp + allowNestedQuotes?: boolean + viewContext?: QuoteEmbedViewContext +}) { + const {_} = useLingui() + const t = useTheme() + const pal = usePalette('default') + const {currentAccount} = useSession() + + const directFetchEnabled = useDirectFetchRecords() + const shouldDirectFetch = + (AppBskyEmbedRecord.isViewBlocked(embed.record) || + AppBskyEmbedRecord.isViewDetached(embed.record)) && + directFetchEnabled + + const directRecord = useDirectFetchRecord({ + uri: + AppBskyEmbedRecord.isViewBlocked(embed.record) || + AppBskyEmbedRecord.isViewDetached(embed.record) + ? embed.record.uri + : '', + enabled: shouldDirectFetch, + }) + if ( + AppBskyEmbedRecord.isViewRecord(embed.record) && + AppBskyFeedPost.isRecord(embed.record.value) && + AppBskyFeedPost.validateRecord(embed.record.value).success + ) { + return ( + + ) + } else if (AppBskyEmbedRecord.isViewBlocked(embed.record)) { + const record = directRecord.data + if (record !== undefined) { + return ( + + + + ) + } + + return ( + + + + {directFetchEnabled ? ( + Blocked... + ) : ( + Blocked + )} + + + ) + } else if (AppBskyEmbedRecord.isViewNotFound(embed.record)) { + return ( + + + + Deleted + + + ) + } else if (AppBskyEmbedRecord.isViewDetached(embed.record)) { + const isViewerOwner = currentAccount?.did + ? embed.record.uri.includes(currentAccount.did) + : false + + const record = directRecord.data + if (record !== undefined) { + return ( + + + + ) + } + + return ( + + + + {isViewerOwner ? ( + Removed by you + ) : ( + Removed by author + )} + {directFetchEnabled ? ... : undefined} + + + ) + } + return null +} + +function QuoteEmbedModerated({ + viewRecord, + onOpen, + style, + allowNestedQuotes, + viewContext, + visibilityLabel, +}: { + viewRecord: AppBskyEmbedRecord.ViewRecord + onOpen?: () => void + style?: StyleProp + allowNestedQuotes?: boolean + viewContext?: QuoteEmbedViewContext + visibilityLabel?: string +}) { + const moderationOpts = useModerationOpts() + const postView = React.useMemo( + () => viewRecordToPostView(viewRecord), + [viewRecord], + ) + const moderation = React.useMemo(() => { + return moderationOpts ? moderatePost(postView, moderationOpts) : undefined + }, [postView, moderationOpts]) + + return ( + + ) +} + +export function QuoteEmbed({ + quote, + moderation, + onOpen, + style, + allowNestedQuotes, + visibilityLabel, +}: { + quote: AppBskyFeedDefs.PostView + moderation?: ModerationDecision + onOpen?: () => void + style?: StyleProp + allowNestedQuotes?: boolean + viewContext?: QuoteEmbedViewContext + visibilityLabel?: string +}) { + const t = useTheme() + const queryClient = useQueryClient() + const pal = usePalette('default') + const itemUrip = new AtUri(quote.uri) + const itemHref = makeProfileLink(quote.author, 'post', itemUrip.rkey) + const itemTitle = `Post by ${quote.author.handle}` + + const richText = React.useMemo(() => { + if ( + !bsky.dangerousIsType( + quote.record, + AppBskyFeedPost.isRecord, + ) + ) + return undefined + const {text, facets} = quote.record + return text.trim() + ? new RichTextAPI({text: text, facets: facets}) + : undefined + }, [quote.record]) + + const embed = React.useMemo(() => { + const e = quote.embed + + if (allowNestedQuotes) { + return e + } else { + if ( + AppBskyEmbedImages.isView(e) || + AppBskyEmbedExternal.isView(e) || + AppBskyEmbedVideo.isView(e) + ) { + return e + } else if ( + AppBskyEmbedRecordWithMedia.isView(e) && + (AppBskyEmbedImages.isView(e.media) || + AppBskyEmbedExternal.isView(e.media) || + AppBskyEmbedVideo.isView(e.media)) + ) { + return e.media + } + } + }, [quote.embed, allowNestedQuotes]) + + const onBeforePress = React.useCallback(() => { + precacheProfile(queryClient, quote.author) + onOpen?.() + }, [queryClient, quote.author, onOpen]) + + const [hover, setHover] = React.useState(false) + return ( + { + setHover(true) + }} + onPointerLeave={() => { + setHover(false) + }}> + + + + + {visibilityLabel !== undefined ? ( + + + + {visibilityLabel} + + + ) : undefined} + + + {moderation ? ( + + ) : null} + {richText ? ( + + ) : null} + {embed && } + + + + ) +} + +export function QuoteX({onRemove}: {onRemove: () => void}) { + const {_} = useLingui() + return ( + + + + ) +} + +export function LazyQuoteEmbed({uri}: {uri: string}) { + const {data} = useResolveLinkQuery(uri) + const moderationOpts = useModerationOpts() + if (!data || data.type !== 'record' || data.kind !== 'post') { + return null + } + const moderation = moderationOpts + ? moderatePost(data.view, moderationOpts) + : undefined + return +} + +function viewRecordToPostView( + viewRecord: AppBskyEmbedRecord.ViewRecord, +): AppBskyFeedDefs.PostView { + const {value, embeds, ...rest} = viewRecord + return { + ...rest, + $type: 'app.bsky.feed.defs#postView', + record: value, + embed: embeds?.[0], + } +} + +const styles = StyleSheet.create({ + errorContainer: { + flexDirection: 'row', + alignItems: 'center', + gap: 4, + borderRadius: 8, + marginTop: 8, + paddingVertical: 14, + paddingHorizontal: 14, + borderWidth: StyleSheet.hairlineWidth, + }, + alert: { + marginBottom: 6, + }, + blockHeader: { + flexDirection: 'row', + alignItems: 'center', + gap: 4, + marginBottom: 8, + }, +}) -- 2.51.2 From b25fbacc0860cec3014480fb4c524944d4eed553 Mon Sep 17 00:00:00 2001 From: serenity <98162476+NekoDrone@users.noreply.github.com> Date: Fri, 3 Oct 2025 16:03:38 +0800 Subject: [PATCH 2/5] fix: deconflict cherry-pick --- src/Navigation.tsx | 15 +-------------- src/lib/routes/types.ts | 1 + src/lib/statsig/statsig.tsx | 1 + 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 46e725c6f..0b07d514d 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -89,22 +89,13 @@ import {ProfileFeedScreen} from '#/screens/Profile/ProfileFeed' import {ProfileFollowersScreen} from '#/screens/Profile/ProfileFollowers' import {ProfileFollowsScreen} from '#/screens/Profile/ProfileFollows' import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy' -import {ProfileSearchScreen} from '#/screens/Profile/ProfileSearch' import {ProfileListScreen} from '#/screens/ProfileList' import {SavedFeeds} from '#/screens/SavedFeeds' import {SearchScreen} from '#/screens/Search' -import {AboutSettingsScreen} from '#/screens/Settings/AboutSettings' -import {AccessibilitySettingsScreen} from '#/screens/Settings/AccessibilitySettings' -import {AccountSettingsScreen} from '#/screens/Settings/AccountSettings' import {ActivityPrivacySettingsScreen} from '#/screens/Settings/ActivityPrivacySettings' import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings' import {AppIconSettingsScreen} from '#/screens/Settings/AppIconSettings' -import {AppPasswordsScreen} from '#/screens/Settings/AppPasswords' -import {ContentAndMediaSettingsScreen} from '#/screens/Settings/ContentAndMediaSettings' -import {ExternalMediaPreferencesScreen} from '#/screens/Settings/ExternalMediaPreferences' -import {FollowingFeedPreferencesScreen} from '#/screens/Settings/FollowingFeedPreferences' import {InterestsSettingsScreen} from '#/screens/Settings/InterestsSettings' -import {LanguageSettingsScreen} from '#/screens/Settings/LanguageSettings' import {LegacyNotificationSettingsScreen} from '#/screens/Settings/LegacyNotificationSettings' import {NotificationSettingsScreen} from '#/screens/Settings/NotificationSettings' import {ActivityNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/ActivityNotificationSettings' @@ -117,15 +108,11 @@ import {QuoteNotificationSettingsScreen} from '#/screens/Settings/NotificationSe import {ReplyNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/ReplyNotificationSettings' import {RepostNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/RepostNotificationSettings' import {RepostsOnRepostsNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings' -import {PrivacyAndSecuritySettingsScreen} from '#/screens/Settings/PrivacyAndSecuritySettings' -import {SettingsScreen} from '#/screens/Settings/Settings' -import {ThreadPreferencesScreen} from '#/screens/Settings/ThreadPreferences' import { StarterPackScreen, StarterPackScreenShort, } from '#/screens/StarterPack/StarterPackScreen' import {Wizard} from '#/screens/StarterPack/Wizard' -import TopicScreen from '#/screens/Topic' import {VideoFeed} from '#/screens/VideoFeed' import {type Theme, useTheme} from '#/alf' import { @@ -399,7 +386,7 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { }} /> DeerSettingsScreen} options={{ title: title(msg`Deer Settings`), diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index 4f7054cb3..08b81de1f 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -123,6 +123,7 @@ export type FlatNavigatorParams = CommonNavigatorParams & { Feeds: undefined Notifications: undefined Messages: {pushToConversation?: string; animation?: 'push' | 'pop'} + CatskySettings: undefined } export type AllNavigatorParams = CommonNavigatorParams & { diff --git a/src/lib/statsig/statsig.tsx b/src/lib/statsig/statsig.tsx index 52ef902e2..043c8df8b 100644 --- a/src/lib/statsig/statsig.tsx +++ b/src/lib/statsig/statsig.tsx @@ -7,6 +7,7 @@ import {type MetricEvents} from '#/logger/metrics' import {isWeb} from '#/platform/detection' import * as persisted from '#/state/persisted' import * as env from '#/env' +import {device} from '#/storage' import {timeout} from '../async/timeout' import {type Gate} from './gates' -- 2.51.2 From c6532ccc12ce70b3ea9244b88eb2f57ea5eac23f Mon Sep 17 00:00:00 2001 From: serenity <98162476+NekoDrone@users.noreply.github.com> Date: Fri, 3 Oct 2025 17:58:06 +0800 Subject: [PATCH 3/5] refactor: we aren't deer thank you aviva for the work you've done :) --- src/Navigation.tsx | 8 ++++---- src/lib/routes/types.ts | 2 +- src/lib/statsig/statsig.tsx | 12 ++++++------ src/routes.ts | 2 +- .../{DeerSettings.tsx => ExperimentalSettings.tsx} | 12 ++++++------ src/screens/Settings/Settings.tsx | 10 ++++++---- src/state/persisted/schema.ts | 2 +- src/storage/schema.ts | 4 ++-- 8 files changed, 27 insertions(+), 25 deletions(-) rename src/screens/Settings/{DeerSettings.tsx => ExperimentalSettings.tsx} (95%) diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 0b07d514d..522a591d8 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -95,6 +95,7 @@ import {SearchScreen} from '#/screens/Search' import {ActivityPrivacySettingsScreen} from '#/screens/Settings/ActivityPrivacySettings' import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings' import {AppIconSettingsScreen} from '#/screens/Settings/AppIconSettings' +import {ExperimentalSettingsScreen} from '#/screens/Settings/ExperimentalSettings' import {InterestsSettingsScreen} from '#/screens/Settings/InterestsSettings' import {LegacyNotificationSettingsScreen} from '#/screens/Settings/LegacyNotificationSettings' import {NotificationSettingsScreen} from '#/screens/Settings/NotificationSettings' @@ -129,7 +130,6 @@ import {AccessibilitySettingsScreen} from './screens/Settings/AccessibilitySetti import {AccountSettingsScreen} from './screens/Settings/AccountSettings' import {AppPasswordsScreen} from './screens/Settings/AppPasswords' import {ContentAndMediaSettingsScreen} from './screens/Settings/ContentAndMediaSettings' -import {DeerSettingsScreen} from './screens/Settings/DeerSettings' import {ExternalMediaPreferencesScreen} from './screens/Settings/ExternalMediaPreferences' import {FollowingFeedPreferencesScreen} from './screens/Settings/FollowingFeedPreferences' import {LanguageSettingsScreen} from './screens/Settings/LanguageSettings' @@ -386,10 +386,10 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { }} /> DeerSettingsScreen} + name="ExperimentalSettings" + getComponent={() => ExperimentalSettingsScreen} options={{ - title: title(msg`Deer Settings`), + title: title(msg`Experimental Settings`), requireAuth: true, }} /> diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index 08b81de1f..86f4d0878 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -123,7 +123,7 @@ export type FlatNavigatorParams = CommonNavigatorParams & { Feeds: undefined Notifications: undefined Messages: {pushToConversation?: string; animation?: 'push' | 'pop'} - CatskySettings: undefined + ExperimentalSettings: undefined } export type AllNavigatorParams = CommonNavigatorParams & { diff --git a/src/lib/statsig/statsig.tsx b/src/lib/statsig/statsig.tsx index 043c8df8b..2756b85c6 100644 --- a/src/lib/statsig/statsig.tsx +++ b/src/lib/statsig/statsig.tsx @@ -138,12 +138,12 @@ export function useGatesCache(): Map { return cache } -function writeDeerGateCache(cache: Map) { - device.set(['deerGateCache'], JSON.stringify(Object.fromEntries(cache))) +function writeCatskyGateCache(cache: Map) { + device.set(['catskyGateCache'], JSON.stringify(Object.fromEntries(cache))) } -export function resetDeerGateCache() { - writeDeerGateCache(new Map()) +export function resetCatskyGateCache() { + writeCatskyGateCache(new Map()) } export function useGate(): (gateName: Gate, options?: GateOptions) => boolean { @@ -166,7 +166,7 @@ export function useGate(): (gateName: Gate, options?: GateOptions) => boolean { } } cache.set(gateName, value) - writeDeerGateCache(cache) + writeCatskyGateCache(cache) return value }, [cache], @@ -190,7 +190,7 @@ export function useDangerousSetGate(): ( const dangerousSetGate = React.useCallback( (gateName: Gate, value: boolean) => { cache.set(gateName, value) - writeDeerGateCache(cache) + writeCatskyGateCache(cache) }, [cache], ) diff --git a/src/routes.ts b/src/routes.ts index 6c93b2d28..104182ea5 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -47,7 +47,7 @@ export const router = new Router({ PreferencesThreads: '/settings/threads', PreferencesExternalEmbeds: '/settings/external-embeds', AccessibilitySettings: '/settings/accessibility', - DeerSettings: '/settings/deer', + ExperimentalSettings: '/settings/experimental', AppearanceSettings: '/settings/appearance', SavedFeeds: '/settings/saved-feeds', AccountSettings: '/settings/account', diff --git a/src/screens/Settings/DeerSettings.tsx b/src/screens/Settings/ExperimentalSettings.tsx similarity index 95% rename from src/screens/Settings/DeerSettings.tsx rename to src/screens/Settings/ExperimentalSettings.tsx index 27acd3b75..69cbe8046 100644 --- a/src/screens/Settings/DeerSettings.tsx +++ b/src/screens/Settings/ExperimentalSettings.tsx @@ -6,7 +6,7 @@ import {type NativeStackScreenProps} from '@react-navigation/native-stack' import {type CommonNavigatorParams} from '#/lib/routes/types' import {type Gate} from '#/lib/statsig/gates' import { - resetDeerGateCache, + resetCatskyGateCache, useDangerousSetGate, useGatesCache, } from '#/lib/statsig/statsig' @@ -23,14 +23,14 @@ import * as SettingsList from '#/screens/Settings/components/SettingsList' import {atoms as a} from '#/alf' import {Admonition} from '#/components/Admonition' import * as Toggle from '#/components/forms/Toggle' -import {Atom_Stroke2_Corner0_Rounded as DeerIcon} from '#/components/icons/Atom' +import {Atom_Stroke2_Corner0_Rounded as ExperimentalIcon} from '#/components/icons/Atom' import {Eye_Stroke2_Corner0_Rounded as VisibilityIcon} from '#/components/icons/Eye' import {PaintRoller_Stroke2_Corner2_Rounded as PaintRollerIcon} from '#/components/icons/PaintRoller' import * as Layout from '#/components/Layout' type Props = NativeStackScreenProps -export function DeerSettingsScreen({}: Props) { +export function ExperimentalSettingsScreen({}: Props) { const {_} = useLingui() const goLinksEnabled = useGoLinksEnabled() @@ -58,7 +58,7 @@ export function DeerSettingsScreen({}: Props) { - Deer + Experimental @@ -66,7 +66,7 @@ export function DeerSettingsScreen({}: Props) { - + Redirects @@ -160,7 +160,7 @@ export function DeerSettingsScreen({}: Props) { { - resetDeerGateCache() + resetCatskyGateCache() setGatesView({}) }} /> diff --git a/src/screens/Settings/Settings.tsx b/src/screens/Settings/Settings.tsx index 3cf698dd5..d05134efb 100644 --- a/src/screens/Settings/Settings.tsx +++ b/src/screens/Settings/Settings.tsx @@ -40,7 +40,7 @@ import {Button, ButtonText} from '#/components/Button' import {useDialogControl} from '#/components/Dialog' import {SwitchAccountDialog} from '#/components/dialogs/SwitchAccount' import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility' -import {Atom_Stroke2_Corner0_Rounded as DeerIcon} from '#/components/icons/Atom' +import {Atom_Stroke2_Corner0_Rounded as ExperimentalIcon} from '#/components/icons/Atom' import {Bell_Stroke2_Corner0_Rounded as NotificationIcon} from '#/components/icons/Bell' import {BubbleInfo_Stroke2_Corner2_Rounded as BubbleInfoIcon} from '#/components/icons/BubbleInfo' import {ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon} from '#/components/icons/Chevron' @@ -216,10 +216,12 @@ export function SettingsScreen({}: Props) { Appearance - - + + - Deer + Experimental Date: Sun, 5 Oct 2025 04:29:03 +0800 Subject: [PATCH 4/5] refactor: label fixme --- src/components/Post/Embed/index.tsx | 8 ++++---- src/screens/Settings/ExperimentalSettings.tsx | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/Post/Embed/index.tsx b/src/components/Post/Embed/index.tsx index 4cb16cce9..ab0205cf4 100644 --- a/src/components/Post/Embed/index.tsx +++ b/src/components/Post/Embed/index.tsx @@ -175,14 +175,14 @@ function RecordEmbed({ case 'post_not_found': { return ( - Deleted + Deleted ) } case 'post_blocked': { return ( - Blocked + Blocked ) } @@ -208,9 +208,9 @@ export function PostDetachedEmbed({ return ( {isViewerOwner ? ( - Removed by you + Removed by you ) : ( - Removed by author + Removed by author )} ) diff --git a/src/screens/Settings/ExperimentalSettings.tsx b/src/screens/Settings/ExperimentalSettings.tsx index 69cbe8046..1c36eb448 100644 --- a/src/screens/Settings/ExperimentalSettings.tsx +++ b/src/screens/Settings/ExperimentalSettings.tsx @@ -91,14 +91,15 @@ export function ExperimentalSettingsScreen({}: Props) { setDirectFetchRecords(value)} style={[a.w_full]}> - Fetch records directly from PDS to see through quote blocks + FIXME: Fetch records directly from PDS to see through quote + blocks -- 2.51.2 From 98de81a46e89a8fa1bd695ad24ab32cbf77ee9ff Mon Sep 17 00:00:00 2001 From: serenity <98162476+NekoDrone@users.noreply.github.com> Date: Sun, 5 Oct 2025 04:41:58 +0800 Subject: [PATCH 5/5] refactor: comment out file for typecheck purposes --- src/view/com/util/post-embeds/QuoteEmbed.tsx | 830 +++++++++---------- 1 file changed, 415 insertions(+), 415 deletions(-) diff --git a/src/view/com/util/post-embeds/QuoteEmbed.tsx b/src/view/com/util/post-embeds/QuoteEmbed.tsx index 00686d97c..95346ec20 100644 --- a/src/view/com/util/post-embeds/QuoteEmbed.tsx +++ b/src/view/com/util/post-embeds/QuoteEmbed.tsx @@ -1,415 +1,415 @@ -import React from 'react' -import { - type StyleProp, - StyleSheet, - TouchableOpacity, - View, - type ViewStyle, -} from 'react-native' -import { - AppBskyEmbedExternal, - AppBskyEmbedImages, - AppBskyEmbedRecord, - AppBskyEmbedRecordWithMedia, - AppBskyEmbedVideo, - type AppBskyFeedDefs, - AppBskyFeedPost, - moderatePost, - type ModerationDecision, - RichText as RichTextAPI, -} from '@atproto/api' -import {AtUri} from '@atproto/api' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useQueryClient} from '@tanstack/react-query' - -import {HITSLOP_20} from '#/lib/constants' -import {usePalette} from '#/lib/hooks/usePalette' -import {InfoCircleIcon} from '#/lib/icons' -import {makeProfileLink} from '#/lib/routes/links' -import {s} from '#/lib/styles' -import {useDirectFetchRecords} from '#/state/preferences/direct-fetch-records' -import {useModerationOpts} from '#/state/preferences/moderation-opts' -import {useDirectFetchRecord} from '#/state/queries/direct-fetch-record' -import {precacheProfile} from '#/state/queries/profile' -import {useResolveLinkQuery} from '#/state/queries/resolve-link' -import {useSession} from '#/state/session' -import {atoms as a, useTheme} from '#/alf' -import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlashIcon} from '#/components/icons/EyeSlash' -import {RichText} from '#/components/RichText' -import {SubtleWebHover} from '#/components/SubtleWebHover' -import * as bsky from '#/types/bsky' -import {ContentHider} from '../../../../components/moderation/ContentHider' -import {PostAlerts} from '../../../../components/moderation/PostAlerts' -import {Link} from '../Link' -import {PostMeta} from '../PostMeta' -import {Text} from '../text/Text' -import {PostEmbeds} from '.' -import {type QuoteEmbedViewContext} from './types' - -export function MaybeQuoteEmbed({ - embed, - onOpen, - style, - allowNestedQuotes, - viewContext, -}: { - embed: AppBskyEmbedRecord.View - onOpen?: () => void - style?: StyleProp - allowNestedQuotes?: boolean - viewContext?: QuoteEmbedViewContext -}) { - const {_} = useLingui() - const t = useTheme() - const pal = usePalette('default') - const {currentAccount} = useSession() - - const directFetchEnabled = useDirectFetchRecords() - const shouldDirectFetch = - (AppBskyEmbedRecord.isViewBlocked(embed.record) || - AppBskyEmbedRecord.isViewDetached(embed.record)) && - directFetchEnabled - - const directRecord = useDirectFetchRecord({ - uri: - AppBskyEmbedRecord.isViewBlocked(embed.record) || - AppBskyEmbedRecord.isViewDetached(embed.record) - ? embed.record.uri - : '', - enabled: shouldDirectFetch, - }) - if ( - AppBskyEmbedRecord.isViewRecord(embed.record) && - AppBskyFeedPost.isRecord(embed.record.value) && - AppBskyFeedPost.validateRecord(embed.record.value).success - ) { - return ( - - ) - } else if (AppBskyEmbedRecord.isViewBlocked(embed.record)) { - const record = directRecord.data - if (record !== undefined) { - return ( - - - - ) - } - - return ( - - - - {directFetchEnabled ? ( - Blocked... - ) : ( - Blocked - )} - - - ) - } else if (AppBskyEmbedRecord.isViewNotFound(embed.record)) { - return ( - - - - Deleted - - - ) - } else if (AppBskyEmbedRecord.isViewDetached(embed.record)) { - const isViewerOwner = currentAccount?.did - ? embed.record.uri.includes(currentAccount.did) - : false - - const record = directRecord.data - if (record !== undefined) { - return ( - - - - ) - } - - return ( - - - - {isViewerOwner ? ( - Removed by you - ) : ( - Removed by author - )} - {directFetchEnabled ? ... : undefined} - - - ) - } - return null -} - -function QuoteEmbedModerated({ - viewRecord, - onOpen, - style, - allowNestedQuotes, - viewContext, - visibilityLabel, -}: { - viewRecord: AppBskyEmbedRecord.ViewRecord - onOpen?: () => void - style?: StyleProp - allowNestedQuotes?: boolean - viewContext?: QuoteEmbedViewContext - visibilityLabel?: string -}) { - const moderationOpts = useModerationOpts() - const postView = React.useMemo( - () => viewRecordToPostView(viewRecord), - [viewRecord], - ) - const moderation = React.useMemo(() => { - return moderationOpts ? moderatePost(postView, moderationOpts) : undefined - }, [postView, moderationOpts]) - - return ( - - ) -} - -export function QuoteEmbed({ - quote, - moderation, - onOpen, - style, - allowNestedQuotes, - visibilityLabel, -}: { - quote: AppBskyFeedDefs.PostView - moderation?: ModerationDecision - onOpen?: () => void - style?: StyleProp - allowNestedQuotes?: boolean - viewContext?: QuoteEmbedViewContext - visibilityLabel?: string -}) { - const t = useTheme() - const queryClient = useQueryClient() - const pal = usePalette('default') - const itemUrip = new AtUri(quote.uri) - const itemHref = makeProfileLink(quote.author, 'post', itemUrip.rkey) - const itemTitle = `Post by ${quote.author.handle}` - - const richText = React.useMemo(() => { - if ( - !bsky.dangerousIsType( - quote.record, - AppBskyFeedPost.isRecord, - ) - ) - return undefined - const {text, facets} = quote.record - return text.trim() - ? new RichTextAPI({text: text, facets: facets}) - : undefined - }, [quote.record]) - - const embed = React.useMemo(() => { - const e = quote.embed - - if (allowNestedQuotes) { - return e - } else { - if ( - AppBskyEmbedImages.isView(e) || - AppBskyEmbedExternal.isView(e) || - AppBskyEmbedVideo.isView(e) - ) { - return e - } else if ( - AppBskyEmbedRecordWithMedia.isView(e) && - (AppBskyEmbedImages.isView(e.media) || - AppBskyEmbedExternal.isView(e.media) || - AppBskyEmbedVideo.isView(e.media)) - ) { - return e.media - } - } - }, [quote.embed, allowNestedQuotes]) - - const onBeforePress = React.useCallback(() => { - precacheProfile(queryClient, quote.author) - onOpen?.() - }, [queryClient, quote.author, onOpen]) - - const [hover, setHover] = React.useState(false) - return ( - { - setHover(true) - }} - onPointerLeave={() => { - setHover(false) - }}> - - - - - {visibilityLabel !== undefined ? ( - - - - {visibilityLabel} - - - ) : undefined} - - - {moderation ? ( - - ) : null} - {richText ? ( - - ) : null} - {embed && } - - - - ) -} - -export function QuoteX({onRemove}: {onRemove: () => void}) { - const {_} = useLingui() - return ( - - - - ) -} - -export function LazyQuoteEmbed({uri}: {uri: string}) { - const {data} = useResolveLinkQuery(uri) - const moderationOpts = useModerationOpts() - if (!data || data.type !== 'record' || data.kind !== 'post') { - return null - } - const moderation = moderationOpts - ? moderatePost(data.view, moderationOpts) - : undefined - return -} - -function viewRecordToPostView( - viewRecord: AppBskyEmbedRecord.ViewRecord, -): AppBskyFeedDefs.PostView { - const {value, embeds, ...rest} = viewRecord - return { - ...rest, - $type: 'app.bsky.feed.defs#postView', - record: value, - embed: embeds?.[0], - } -} - -const styles = StyleSheet.create({ - errorContainer: { - flexDirection: 'row', - alignItems: 'center', - gap: 4, - borderRadius: 8, - marginTop: 8, - paddingVertical: 14, - paddingHorizontal: 14, - borderWidth: StyleSheet.hairlineWidth, - }, - alert: { - marginBottom: 6, - }, - blockHeader: { - flexDirection: 'row', - alignItems: 'center', - gap: 4, - marginBottom: 8, - }, -}) +// import React from 'react' +// import { +// type StyleProp, +// StyleSheet, +// TouchableOpacity, +// View, +// type ViewStyle, +// } from 'react-native' +// import { +// AppBskyEmbedExternal, +// AppBskyEmbedImages, +// AppBskyEmbedRecord, +// AppBskyEmbedRecordWithMedia, +// AppBskyEmbedVideo, +// type AppBskyFeedDefs, +// AppBskyFeedPost, +// moderatePost, +// type ModerationDecision, +// RichText as RichTextAPI, +// } from '@atproto/api' +// import {AtUri} from '@atproto/api' +// import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +// import {msg, Trans} from '@lingui/macro' +// import {useLingui} from '@lingui/react' +// import {useQueryClient} from '@tanstack/react-query' +// +// import {HITSLOP_20} from '#/lib/constants' +// import {usePalette} from '#/lib/hooks/usePalette' +// import {InfoCircleIcon} from '#/lib/icons' +// import {makeProfileLink} from '#/lib/routes/links' +// import {s} from '#/lib/styles' +// import {useDirectFetchRecords} from '#/state/preferences/direct-fetch-records' +// import {useModerationOpts} from '#/state/preferences/moderation-opts' +// import {useDirectFetchRecord} from '#/state/queries/direct-fetch-record' +// import {precacheProfile} from '#/state/queries/profile' +// import {useResolveLinkQuery} from '#/state/queries/resolve-link' +// import {useSession} from '#/state/session' +// import {atoms as a, useTheme} from '#/alf' +// import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlashIcon} from '#/components/icons/EyeSlash' +// import {RichText} from '#/components/RichText' +// import {SubtleWebHover} from '#/components/SubtleWebHover' +// import * as bsky from '#/types/bsky' +// import {ContentHider} from '../../../../components/moderation/ContentHider' +// import {PostAlerts} from '../../../../components/moderation/PostAlerts' +// import {Link} from '../Link' +// import {PostMeta} from '../PostMeta' +// import {Text} from '../text/Text' +// import {PostEmbeds} from '.' +// import {type QuoteEmbedViewContext} from './types' +// +// export function MaybeQuoteEmbed({ +// embed, +// onOpen, +// style, +// allowNestedQuotes, +// viewContext, +// }: { +// embed: AppBskyEmbedRecord.View +// onOpen?: () => void +// style?: StyleProp +// allowNestedQuotes?: boolean +// viewContext?: QuoteEmbedViewContext +// }) { +// const {_} = useLingui() +// const t = useTheme() +// const pal = usePalette('default') +// const {currentAccount} = useSession() +// +// const directFetchEnabled = useDirectFetchRecords() +// const shouldDirectFetch = +// (AppBskyEmbedRecord.isViewBlocked(embed.record) || +// AppBskyEmbedRecord.isViewDetached(embed.record)) && +// directFetchEnabled +// +// const directRecord = useDirectFetchRecord({ +// uri: +// AppBskyEmbedRecord.isViewBlocked(embed.record) || +// AppBskyEmbedRecord.isViewDetached(embed.record) +// ? embed.record.uri +// : '', +// enabled: shouldDirectFetch, +// }) +// if ( +// AppBskyEmbedRecord.isViewRecord(embed.record) && +// AppBskyFeedPost.isRecord(embed.record.value) && +// AppBskyFeedPost.validateRecord(embed.record.value).success +// ) { +// return ( +// +// ) +// } else if (AppBskyEmbedRecord.isViewBlocked(embed.record)) { +// const record = directRecord.data +// if (record !== undefined) { +// return ( +// +// +// +// ) +// } +// +// return ( +// +// +// +// {directFetchEnabled ? ( +// Blocked... +// ) : ( +// Blocked +// )} +// +// +// ) +// } else if (AppBskyEmbedRecord.isViewNotFound(embed.record)) { +// return ( +// +// +// +// Deleted +// +// +// ) +// } else if (AppBskyEmbedRecord.isViewDetached(embed.record)) { +// const isViewerOwner = currentAccount?.did +// ? embed.record.uri.includes(currentAccount.did) +// : false +// +// const record = directRecord.data +// if (record !== undefined) { +// return ( +// +// +// +// ) +// } +// +// return ( +// +// +// +// {isViewerOwner ? ( +// Removed by you +// ) : ( +// Removed by author +// )} +// {directFetchEnabled ? ... : undefined} +// +// +// ) +// } +// return null +// } +// +// function QuoteEmbedModerated({ +// viewRecord, +// onOpen, +// style, +// allowNestedQuotes, +// viewContext, +// visibilityLabel, +// }: { +// viewRecord: AppBskyEmbedRecord.ViewRecord +// onOpen?: () => void +// style?: StyleProp +// allowNestedQuotes?: boolean +// viewContext?: QuoteEmbedViewContext +// visibilityLabel?: string +// }) { +// const moderationOpts = useModerationOpts() +// const postView = React.useMemo( +// () => viewRecordToPostView(viewRecord), +// [viewRecord], +// ) +// const moderation = React.useMemo(() => { +// return moderationOpts ? moderatePost(postView, moderationOpts) : undefined +// }, [postView, moderationOpts]) +// +// return ( +// +// ) +// } +// +// export function QuoteEmbed({ +// quote, +// moderation, +// onOpen, +// style, +// allowNestedQuotes, +// visibilityLabel, +// }: { +// quote: AppBskyFeedDefs.PostView +// moderation?: ModerationDecision +// onOpen?: () => void +// style?: StyleProp +// allowNestedQuotes?: boolean +// viewContext?: QuoteEmbedViewContext +// visibilityLabel?: string +// }) { +// const t = useTheme() +// const queryClient = useQueryClient() +// const pal = usePalette('default') +// const itemUrip = new AtUri(quote.uri) +// const itemHref = makeProfileLink(quote.author, 'post', itemUrip.rkey) +// const itemTitle = `Post by ${quote.author.handle}` +// +// const richText = React.useMemo(() => { +// if ( +// !bsky.dangerousIsType( +// quote.record, +// AppBskyFeedPost.isRecord, +// ) +// ) +// return undefined +// const {text, facets} = quote.record +// return text.trim() +// ? new RichTextAPI({text: text, facets: facets}) +// : undefined +// }, [quote.record]) +// +// const embed = React.useMemo(() => { +// const e = quote.embed +// +// if (allowNestedQuotes) { +// return e +// } else { +// if ( +// AppBskyEmbedImages.isView(e) || +// AppBskyEmbedExternal.isView(e) || +// AppBskyEmbedVideo.isView(e) +// ) { +// return e +// } else if ( +// AppBskyEmbedRecordWithMedia.isView(e) && +// (AppBskyEmbedImages.isView(e.media) || +// AppBskyEmbedExternal.isView(e.media) || +// AppBskyEmbedVideo.isView(e.media)) +// ) { +// return e.media +// } +// } +// }, [quote.embed, allowNestedQuotes]) +// +// const onBeforePress = React.useCallback(() => { +// precacheProfile(queryClient, quote.author) +// onOpen?.() +// }, [queryClient, quote.author, onOpen]) +// +// const [hover, setHover] = React.useState(false) +// return ( +// { +// setHover(true) +// }} +// onPointerLeave={() => { +// setHover(false) +// }}> +// +// +// +// +// {visibilityLabel !== undefined ? ( +// +// +// +// {visibilityLabel} +// +// +// ) : undefined} +// +// +// {moderation ? ( +// +// ) : null} +// {richText ? ( +// +// ) : null} +// {embed && } +// +// +// +// ) +// } +// +// export function QuoteX({onRemove}: {onRemove: () => void}) { +// const {_} = useLingui() +// return ( +// +// +// +// ) +// } +// +// export function LazyQuoteEmbed({uri}: {uri: string}) { +// const {data} = useResolveLinkQuery(uri) +// const moderationOpts = useModerationOpts() +// if (!data || data.type !== 'record' || data.kind !== 'post') { +// return null +// } +// const moderation = moderationOpts +// ? moderatePost(data.view, moderationOpts) +// : undefined +// return +// } +// +// function viewRecordToPostView( +// viewRecord: AppBskyEmbedRecord.ViewRecord, +// ): AppBskyFeedDefs.PostView { +// const {value, embeds, ...rest} = viewRecord +// return { +// ...rest, +// $type: 'app.bsky.feed.defs#postView', +// record: value, +// embed: embeds?.[0], +// } +// } +// +// const styles = StyleSheet.create({ +// errorContainer: { +// flexDirection: 'row', +// alignItems: 'center', +// gap: 4, +// borderRadius: 8, +// marginTop: 8, +// paddingVertical: 14, +// paddingHorizontal: 14, +// borderWidth: StyleSheet.hairlineWidth, +// }, +// alert: { +// marginBottom: 6, +// }, +// blockHeader: { +// flexDirection: 'row', +// alignItems: 'center', +// gap: 4, +// marginBottom: 8, +// }, +// })