An ATproto social media client -- with an independent Appview.

Port post embeds to new arch (#7408)

* Direct port of embeds to new arch

(cherry picked from commit cc3fa1f6cea396dd9222486c633a508bfee1ecd6)

* Re-org

* Split out ListEmbed and FeedEmbed

* Split out ImageEmbed

* DRY up a bit

* Port over ExternalLinkEmbed

* Port over Player and Gif embeds

* Migrate ComposerReplyTo

* Replace other usages of old post-embeds

* Migrate view contexts

* Copy pasta VideoEmbed

* Copy pasta GifEmbed

* Swap in new file location

* Clean up

* Fix up native

* Add back in correct moderation on List and Feed embeds

* Format

* Prettier

* delete old video utils

* move bandwidth-estimate.ts

* Remove log

* Add LazyQuoteEmbed for composer use

* Clean up unused things

* Remove remaining items

* Prettier

* Fix imports

* Handle nested quotes same as prod

* Add back silenced error handling

* Fix lint

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>

authored by

Eric Bailey
Samuel Newman
and committed by
GitHub
45f0f7ee ba0f5a9b

+812 -799
+2 -2
bskylink/src/routes/index.ts
··· 1 - import {Express} from 'express' 1 + import {type Express} from 'express' 2 2 3 - import {AppContext} from '../context.js' 3 + import {type AppContext} from '../context.js' 4 4 import {default as createShortLink} from './createShortLink.js' 5 5 import {default as health} from './health.js' 6 6 import {default as redirect} from './redirect.js'
+2 -2
bskylink/src/routes/redirect.ts
··· 2 2 3 3 import {DAY, SECOND} from '@atproto/common' 4 4 import escapeHTML from 'escape-html' 5 - import {Express} from 'express' 5 + import {type Express} from 'express' 6 6 7 - import {AppContext} from '../context.js' 7 + import {type AppContext} from '../context.js' 8 8 import {handler} from './util.js' 9 9 10 10 const INTERNAL_IP_REGEX = new RegExp(
+2 -2
bskylink/src/routes/root.ts
··· 1 - import {Express} from 'express' 1 + import {type Express} from 'express' 2 2 3 - import {AppContext} from '../context.js' 3 + import {type AppContext} from '../context.js' 4 4 import {handler} from './util.js' 5 5 6 6 export default function (ctx: AppContext, app: Express) {
+1 -1
src/App.native.tsx
··· 59 59 import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' 60 60 import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies' 61 61 import {TestCtrls} from '#/view/com/testing/TestCtrls' 62 - import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext' 63 62 import * as Toast from '#/view/com/util/Toast' 64 63 import {Shell} from '#/view/shell' 65 64 import {ThemeProvider as Alf} from '#/alf' ··· 69 68 import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' 70 69 import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs' 71 70 import {Provider as PortalProvider} from '#/components/Portal' 71 + import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext' 72 72 import {Splash} from '#/Splash' 73 73 import {BottomSheetProvider} from '../modules/bottom-sheet' 74 74 import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
+2 -2
src/App.web.tsx
··· 48 48 import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' 49 49 import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' 50 50 import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies' 51 - import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoWebContext' 52 - import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext' 53 51 import * as Toast from '#/view/com/util/Toast' 54 52 import {ToastContainer} from '#/view/com/util/Toast.web' 55 53 import {Shell} from '#/view/shell/index' ··· 60 58 import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' 61 59 import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs' 62 60 import {Provider as PortalProvider} from '#/components/Portal' 61 + import {Provider as ActiveVideoProvider} from '#/components/Post/Embed/VideoEmbed/ActiveVideoWebContext' 62 + import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext' 63 63 import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' 64 64 import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottomBarBorder' 65 65
+1 -1
src/alf/util/systemUI.ts
··· 1 1 import * as SystemUI from 'expo-system-ui' 2 2 3 3 import {isAndroid} from '#/platform/detection' 4 - import {Theme} from '../types' 4 + import {type Theme} from '../types' 5 5 6 6 export function setSystemUITheme(themeType: 'theme' | 'lightbox', t: Theme) { 7 7 if (isAndroid) {
+1 -1
src/components/ContextMenu/Backdrop.tsx
··· 2 2 import Animated, { 3 3 Extrapolation, 4 4 interpolate, 5 - SharedValue, 5 + type SharedValue, 6 6 useAnimatedStyle, 7 7 } from 'react-native-reanimated' 8 8 import {msg} from '@lingui/macro'
+12 -6
src/components/FeedInterstitials.tsx
··· 1 1 import React from 'react' 2 2 import {View} from 'react-native' 3 3 import {ScrollView} from 'react-native-gesture-handler' 4 - import {AppBskyFeedDefs, AtUri} from '@atproto/api' 4 + import {type AppBskyFeedDefs, AtUri} from '@atproto/api' 5 5 import {msg, Trans} from '@lingui/macro' 6 6 import {useLingui} from '@lingui/react' 7 7 import {useNavigation} from '@react-navigation/native' 8 8 9 - import {NavigationProp} from '#/lib/routes/types' 9 + import {type NavigationProp} from '#/lib/routes/types' 10 10 import {logEvent} from '#/lib/statsig/statsig' 11 11 import {logger} from '#/logger' 12 12 import {useModerationOpts} from '#/state/preferences/moderation-opts' 13 13 import {useGetPopularFeedsQuery} from '#/state/queries/feed' 14 - import {FeedDescriptor} from '#/state/queries/post-feed' 14 + import {type FeedDescriptor} from '#/state/queries/post-feed' 15 15 import {useProfilesQuery} from '#/state/queries/profile' 16 16 import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows' 17 17 import {useSession} from '#/state/session' 18 18 import * as userActionHistory from '#/state/userActionHistory' 19 - import {SeenPost} from '#/state/userActionHistory' 19 + import {type SeenPost} from '#/state/userActionHistory' 20 20 import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture' 21 - import {atoms as a, useBreakpoints, useTheme, ViewStyleProp, web} from '#/alf' 21 + import { 22 + atoms as a, 23 + useBreakpoints, 24 + useTheme, 25 + type ViewStyleProp, 26 + web, 27 + } from '#/alf' 22 28 import {Button} from '#/components/Button' 23 29 import * as FeedCard from '#/components/FeedCard' 24 30 import {ArrowRight_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow' ··· 27 33 import {InlineLinkText} from '#/components/Link' 28 34 import * as ProfileCard from '#/components/ProfileCard' 29 35 import {Text} from '#/components/Typography' 30 - import * as bsky from '#/types/bsky' 36 + import type * as bsky from '#/types/bsky' 31 37 import {ProgressGuideList} from './ProgressGuide/List' 32 38 33 39 const MOBILE_CARD_WIDTH = 300
+4 -4
src/components/Layout/Header/index.tsx
··· 1 1 import {createContext, useCallback, useContext} from 'react' 2 - import {GestureResponderEvent, Keyboard, View} from 'react-native' 2 + import {type GestureResponderEvent, Keyboard, View} from 'react-native' 3 3 import {msg} from '@lingui/macro' 4 4 import {useLingui} from '@lingui/react' 5 5 import {useNavigation} from '@react-navigation/native' 6 6 7 7 import {HITSLOP_30} from '#/lib/constants' 8 - import {NavigationProp} from '#/lib/routes/types' 8 + import {type NavigationProp} from '#/lib/routes/types' 9 9 import {isIOS} from '#/platform/detection' 10 10 import {useSetDrawerOpen} from '#/state/shell' 11 11 import { 12 12 atoms as a, 13 13 platform, 14 - TextStyleProp, 14 + type TextStyleProp, 15 15 useBreakpoints, 16 16 useGutters, 17 17 useLayoutBreakpoints, 18 18 useTheme, 19 19 web, 20 20 } from '#/alf' 21 - import {Button, ButtonIcon, ButtonProps} from '#/components/Button' 21 + import {Button, ButtonIcon, type ButtonProps} from '#/components/Button' 22 22 import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeft} from '#/components/icons/Arrow' 23 23 import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu' 24 24 import {
+1 -1
src/components/Menu/context.tsx
··· 1 1 import React from 'react' 2 2 3 - import type {ContextType, ItemContextType} from '#/components/Menu/types' 3 + import {type ContextType, type ItemContextType} from '#/components/Menu/types' 4 4 5 5 export const Context = React.createContext<ContextType | null>(null) 6 6
+52
src/components/Post/Embed/FeedEmbed.tsx
··· 1 + import React from 'react' 2 + import {StyleSheet} from 'react-native' 3 + import {moderateFeedGenerator} from '@atproto/api' 4 + 5 + import {usePalette} from '#/lib/hooks/usePalette' 6 + import {useModerationOpts} from '#/state/preferences/moderation-opts' 7 + import {FeedSourceCard} from '#/view/com/feeds/FeedSourceCard' 8 + import {ContentHider} from '#/components/moderation/ContentHider' 9 + import {type EmbedType} from '#/types/bsky/post' 10 + import {type CommonProps} from './types' 11 + 12 + export function FeedEmbed({ 13 + embed, 14 + }: CommonProps & { 15 + embed: EmbedType<'feed'> 16 + }) { 17 + const pal = usePalette('default') 18 + return ( 19 + <FeedSourceCard 20 + feedUri={embed.view.uri} 21 + style={[pal.view, pal.border, styles.customFeedOuter]} 22 + showLikes 23 + /> 24 + ) 25 + } 26 + 27 + export function ModeratedFeedEmbed({ 28 + embed, 29 + }: CommonProps & { 30 + embed: EmbedType<'feed'> 31 + }) { 32 + const moderationOpts = useModerationOpts() 33 + const moderation = React.useMemo(() => { 34 + return moderationOpts 35 + ? moderateFeedGenerator(embed.view, moderationOpts) 36 + : undefined 37 + }, [embed.view, moderationOpts]) 38 + return ( 39 + <ContentHider modui={moderation?.ui('contentList')}> 40 + <FeedEmbed embed={embed} /> 41 + </ContentHider> 42 + ) 43 + } 44 + 45 + const styles = StyleSheet.create({ 46 + customFeedOuter: { 47 + borderWidth: StyleSheet.hairlineWidth, 48 + borderRadius: 8, 49 + paddingHorizontal: 12, 50 + paddingVertical: 12, 51 + }, 52 + })
+106
src/components/Post/Embed/ImageEmbed.tsx
··· 1 + import {InteractionManager, View} from 'react-native' 2 + import { 3 + type AnimatedRef, 4 + measure, 5 + type MeasuredDimensions, 6 + runOnJS, 7 + runOnUI, 8 + } from 'react-native-reanimated' 9 + import {Image} from 'expo-image' 10 + 11 + import {useLightboxControls} from '#/state/lightbox' 12 + import {type Dimensions} from '#/view/com/lightbox/ImageViewing/@types' 13 + import {AutoSizedImage} from '#/view/com/util/images/AutoSizedImage' 14 + import {ImageLayoutGrid} from '#/view/com/util/images/ImageLayoutGrid' 15 + import {atoms as a} from '#/alf' 16 + import {PostEmbedViewContext} from '#/components/Post/Embed/types' 17 + import {type EmbedType} from '#/types/bsky/post' 18 + import {type CommonProps} from './types' 19 + 20 + export function ImageEmbed({ 21 + embed, 22 + ...rest 23 + }: CommonProps & { 24 + embed: EmbedType<'images'> 25 + }) { 26 + const {openLightbox} = useLightboxControls() 27 + const {images} = embed.view 28 + 29 + if (images.length > 0) { 30 + const items = images.map(img => ({ 31 + uri: img.fullsize, 32 + thumbUri: img.thumb, 33 + alt: img.alt, 34 + dimensions: img.aspectRatio ?? null, 35 + })) 36 + const _openLightbox = ( 37 + index: number, 38 + thumbRects: (MeasuredDimensions | null)[], 39 + fetchedDims: (Dimensions | null)[], 40 + ) => { 41 + openLightbox({ 42 + images: items.map((item, i) => ({ 43 + ...item, 44 + thumbRect: thumbRects[i] ?? null, 45 + thumbDimensions: fetchedDims[i] ?? null, 46 + type: 'image', 47 + })), 48 + index, 49 + }) 50 + } 51 + const onPress = ( 52 + index: number, 53 + refs: AnimatedRef<any>[], 54 + fetchedDims: (Dimensions | null)[], 55 + ) => { 56 + runOnUI(() => { 57 + 'worklet' 58 + const rects: (MeasuredDimensions | null)[] = [] 59 + for (const r of refs) { 60 + rects.push(measure(r)) 61 + } 62 + runOnJS(_openLightbox)(index, rects, fetchedDims) 63 + })() 64 + } 65 + const onPressIn = (_: number) => { 66 + InteractionManager.runAfterInteractions(() => { 67 + Image.prefetch(items.map(i => i.uri)) 68 + }) 69 + } 70 + 71 + if (images.length === 1) { 72 + const image = images[0] 73 + return ( 74 + <View style={[a.mt_sm, rest.style]}> 75 + <AutoSizedImage 76 + crop={ 77 + rest.viewContext === PostEmbedViewContext.ThreadHighlighted 78 + ? 'none' 79 + : rest.viewContext === 80 + PostEmbedViewContext.FeedEmbedRecordWithMedia 81 + ? 'square' 82 + : 'constrained' 83 + } 84 + image={image} 85 + onPress={(containerRef, dims) => onPress(0, [containerRef], [dims])} 86 + onPressIn={() => onPressIn(0)} 87 + hideBadge={ 88 + rest.viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia 89 + } 90 + /> 91 + </View> 92 + ) 93 + } 94 + 95 + return ( 96 + <View style={[a.mt_sm, rest.style]}> 97 + <ImageLayoutGrid 98 + images={images} 99 + onPress={onPress} 100 + onPressIn={onPressIn} 101 + viewContext={rest.viewContext} 102 + /> 103 + </View> 104 + ) 105 + } 106 + }
+37
src/components/Post/Embed/LazyQuoteEmbed.tsx
··· 1 + import {useMemo} from 'react' 2 + import {View} from 'react-native' 3 + 4 + import {createEmbedViewRecordFromPost} from '#/state/queries/postgate/util' 5 + import {useResolveLinkQuery} from '#/state/queries/resolve-link' 6 + import {atoms as a, useTheme} from '#/alf' 7 + import {QuoteEmbed} from '#/components/Post/Embed' 8 + 9 + export function LazyQuoteEmbed({uri}: {uri: string}) { 10 + const t = useTheme() 11 + const {data} = useResolveLinkQuery(uri) 12 + 13 + const view = useMemo(() => { 14 + if (!data || data.type !== 'record' || data.kind !== 'post') return 15 + return createEmbedViewRecordFromPost(data.view) 16 + }, [data]) 17 + 18 + return view ? ( 19 + <QuoteEmbed 20 + embed={{ 21 + type: 'post', 22 + view, 23 + }} 24 + /> 25 + ) : ( 26 + <View 27 + style={[ 28 + a.w_full, 29 + a.rounded_md, 30 + t.atoms.bg_contrast_25, 31 + { 32 + height: 68, 33 + }, 34 + ]} 35 + /> 36 + ) 37 + }
+42
src/components/Post/Embed/ListEmbed.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + import {moderateUserList} from '@atproto/api' 4 + 5 + import {useModerationOpts} from '#/state/preferences/moderation-opts' 6 + import {atoms as a, useTheme} from '#/alf' 7 + import * as ListCard from '#/components/ListCard' 8 + import {ContentHider} from '#/components/moderation/ContentHider' 9 + import {EmbedType} from '#/types/bsky/post' 10 + import {CommonProps} from './types' 11 + 12 + export function ListEmbed({ 13 + embed, 14 + }: CommonProps & { 15 + embed: EmbedType<'list'> 16 + }) { 17 + const t = useTheme() 18 + return ( 19 + <View 20 + style={[a.border, t.atoms.border_contrast_medium, a.p_md, a.rounded_sm]}> 21 + <ListCard.Default view={embed.view} /> 22 + </View> 23 + ) 24 + } 25 + 26 + export function ModeratedListEmbed({ 27 + embed, 28 + }: CommonProps & { 29 + embed: EmbedType<'list'> 30 + }) { 31 + const moderationOpts = useModerationOpts() 32 + const moderation = React.useMemo(() => { 33 + return moderationOpts 34 + ? moderateUserList(embed.view, moderationOpts) 35 + : undefined 36 + }, [embed.view, moderationOpts]) 37 + return ( 38 + <ContentHider modui={moderation?.ui('contentList')}> 39 + <ListEmbed embed={embed} /> 40 + </ContentHider> 41 + ) 42 + }
+33
src/components/Post/Embed/PostPlaceholder.tsx
··· 1 + import {StyleSheet, View} from 'react-native' 2 + 3 + import {usePalette} from '#/lib/hooks/usePalette' 4 + import {InfoCircleIcon} from '#/lib/icons' 5 + import {Text} from '#/view/com/util/text/Text' 6 + import {atoms as a, useTheme} from '#/alf' 7 + 8 + export function PostPlaceholder({children}: {children: React.ReactNode}) { 9 + const t = useTheme() 10 + const pal = usePalette('default') 11 + return ( 12 + <View 13 + style={[styles.errorContainer, a.border, t.atoms.border_contrast_low]}> 14 + <InfoCircleIcon size={18} style={pal.text} /> 15 + <Text type="lg" style={pal.text}> 16 + {children} 17 + </Text> 18 + </View> 19 + ) 20 + } 21 + 22 + const styles = StyleSheet.create({ 23 + errorContainer: { 24 + flexDirection: 'row', 25 + alignItems: 'center', 26 + gap: 4, 27 + borderRadius: 8, 28 + marginTop: 8, 29 + paddingVertical: 14, 30 + paddingHorizontal: 14, 31 + borderWidth: StyleSheet.hairlineWidth, 32 + }, 33 + })
+332
src/components/Post/Embed/index.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + import { 4 + type $Typed, 5 + type AppBskyFeedDefs, 6 + AppBskyFeedPost, 7 + AtUri, 8 + moderatePost, 9 + RichText as RichTextAPI, 10 + } from '@atproto/api' 11 + import {Trans} from '@lingui/macro' 12 + import {useQueryClient} from '@tanstack/react-query' 13 + 14 + import {usePalette} from '#/lib/hooks/usePalette' 15 + import {makeProfileLink} from '#/lib/routes/links' 16 + import {useModerationOpts} from '#/state/preferences/moderation-opts' 17 + import {unstableCacheProfileView} from '#/state/queries/profile' 18 + import {useSession} from '#/state/session' 19 + import {Link} from '#/view/com/util/Link' 20 + import {PostMeta} from '#/view/com/util/PostMeta' 21 + import {atoms as a, useTheme} from '#/alf' 22 + import {ContentHider} from '#/components/moderation/ContentHider' 23 + import {PostAlerts} from '#/components/moderation/PostAlerts' 24 + import {RichText} from '#/components/RichText' 25 + import {Embed as StarterPackCard} from '#/components/StarterPack/StarterPackCard' 26 + import {SubtleWebHover} from '#/components/SubtleWebHover' 27 + import * as bsky from '#/types/bsky' 28 + import { 29 + type Embed as TEmbed, 30 + type EmbedType, 31 + parseEmbed, 32 + } from '#/types/bsky/post' 33 + import {ExternalEmbed} from './ExternalEmbed' 34 + import {ModeratedFeedEmbed} from './FeedEmbed' 35 + import {ImageEmbed} from './ImageEmbed' 36 + import {ModeratedListEmbed} from './ListEmbed' 37 + import {PostPlaceholder as PostPlaceholderText} from './PostPlaceholder' 38 + import { 39 + type CommonProps, 40 + type EmbedProps, 41 + PostEmbedViewContext, 42 + QuoteEmbedViewContext, 43 + } from './types' 44 + import {VideoEmbed} from './VideoEmbed' 45 + 46 + export {PostEmbedViewContext, QuoteEmbedViewContext} from './types' 47 + 48 + export function Embed({embed: rawEmbed, ...rest}: EmbedProps) { 49 + const embed = parseEmbed(rawEmbed) 50 + 51 + switch (embed.type) { 52 + case 'images': 53 + case 'link': 54 + case 'video': { 55 + return <MediaEmbed embed={embed} {...rest} /> 56 + } 57 + case 'feed': 58 + case 'list': 59 + case 'starter_pack': 60 + case 'labeler': 61 + case 'post': 62 + case 'post_not_found': 63 + case 'post_blocked': 64 + case 'post_detached': { 65 + return <RecordEmbed embed={embed} {...rest} /> 66 + } 67 + case 'post_with_media': { 68 + return ( 69 + <View style={rest.style}> 70 + <MediaEmbed embed={embed.media} {...rest} /> 71 + <RecordEmbed embed={embed.view} {...rest} /> 72 + </View> 73 + ) 74 + } 75 + default: { 76 + return null 77 + } 78 + } 79 + } 80 + 81 + function MediaEmbed({ 82 + embed, 83 + ...rest 84 + }: CommonProps & { 85 + embed: TEmbed 86 + }) { 87 + switch (embed.type) { 88 + case 'images': { 89 + return ( 90 + <ContentHider modui={rest.moderation?.ui('contentMedia')}> 91 + <ImageEmbed embed={embed} {...rest} /> 92 + </ContentHider> 93 + ) 94 + } 95 + case 'link': { 96 + return ( 97 + <ContentHider modui={rest.moderation?.ui('contentMedia')}> 98 + <ExternalEmbed 99 + link={embed.view.external} 100 + onOpen={rest.onOpen} 101 + style={[a.mt_sm, rest.style]} 102 + /> 103 + </ContentHider> 104 + ) 105 + } 106 + case 'video': { 107 + return ( 108 + <ContentHider modui={rest.moderation?.ui('contentMedia')}> 109 + <VideoEmbed embed={embed.view} /> 110 + </ContentHider> 111 + ) 112 + } 113 + default: { 114 + return null 115 + } 116 + } 117 + } 118 + 119 + function RecordEmbed({ 120 + embed, 121 + ...rest 122 + }: CommonProps & { 123 + embed: TEmbed 124 + }) { 125 + switch (embed.type) { 126 + case 'feed': { 127 + return ( 128 + <View style={a.mt_sm}> 129 + <ModeratedFeedEmbed embed={embed} {...rest} /> 130 + </View> 131 + ) 132 + } 133 + case 'list': { 134 + return ( 135 + <View style={a.mt_sm}> 136 + <ModeratedListEmbed embed={embed} /> 137 + </View> 138 + ) 139 + } 140 + case 'starter_pack': { 141 + return ( 142 + <View style={a.mt_sm}> 143 + <StarterPackCard starterPack={embed.view} /> 144 + </View> 145 + ) 146 + } 147 + case 'labeler': { 148 + // not implemented 149 + return null 150 + } 151 + case 'post': { 152 + if (rest.isWithinQuote && !rest.allowNestedQuotes) { 153 + return null 154 + } 155 + 156 + return ( 157 + <QuoteEmbed 158 + {...rest} 159 + embed={embed} 160 + viewContext={ 161 + rest.viewContext === PostEmbedViewContext.Feed 162 + ? QuoteEmbedViewContext.FeedEmbedRecordWithMedia 163 + : undefined 164 + } 165 + isWithinQuote={rest.isWithinQuote} 166 + allowNestedQuotes={rest.allowNestedQuotes} 167 + /> 168 + ) 169 + } 170 + case 'post_not_found': { 171 + return ( 172 + <PostPlaceholderText> 173 + <Trans>Deleted</Trans> 174 + </PostPlaceholderText> 175 + ) 176 + } 177 + case 'post_blocked': { 178 + return ( 179 + <PostPlaceholderText> 180 + <Trans>Blocked</Trans> 181 + </PostPlaceholderText> 182 + ) 183 + } 184 + case 'post_detached': { 185 + return <PostDetachedEmbed embed={embed} /> 186 + } 187 + default: { 188 + return null 189 + } 190 + } 191 + } 192 + 193 + export function PostDetachedEmbed({ 194 + embed, 195 + }: { 196 + embed: EmbedType<'post_detached'> 197 + }) { 198 + const {currentAccount} = useSession() 199 + const isViewerOwner = currentAccount?.did 200 + ? embed.view.uri.includes(currentAccount.did) 201 + : false 202 + 203 + return ( 204 + <PostPlaceholderText> 205 + {isViewerOwner ? ( 206 + <Trans>Removed by you</Trans> 207 + ) : ( 208 + <Trans>Removed by author</Trans> 209 + )} 210 + </PostPlaceholderText> 211 + ) 212 + } 213 + 214 + /* 215 + * Nests parent `Embed` component and therefore must live in this file to avoid 216 + * circular imports. 217 + */ 218 + export function QuoteEmbed({ 219 + embed, 220 + onOpen, 221 + style, 222 + isWithinQuote: parentIsWithinQuote, 223 + allowNestedQuotes: parentAllowNestedQuotes, 224 + }: Omit<CommonProps, 'viewContext'> & { 225 + embed: EmbedType<'post'> 226 + viewContext?: QuoteEmbedViewContext 227 + }) { 228 + const moderationOpts = useModerationOpts() 229 + const quote = React.useMemo<$Typed<AppBskyFeedDefs.PostView>>( 230 + () => ({ 231 + ...embed.view, 232 + $type: 'app.bsky.feed.defs#postView', 233 + record: embed.view.value, 234 + embed: embed.view.embeds?.[0], 235 + }), 236 + [embed], 237 + ) 238 + const moderation = React.useMemo(() => { 239 + return moderationOpts ? moderatePost(quote, moderationOpts) : undefined 240 + }, [quote, moderationOpts]) 241 + 242 + const t = useTheme() 243 + const queryClient = useQueryClient() 244 + const pal = usePalette('default') 245 + const itemUrip = new AtUri(quote.uri) 246 + const itemHref = makeProfileLink(quote.author, 'post', itemUrip.rkey) 247 + const itemTitle = `Post by ${quote.author.handle}` 248 + 249 + const richText = React.useMemo(() => { 250 + if ( 251 + !bsky.dangerousIsType<AppBskyFeedPost.Record>( 252 + quote.record, 253 + AppBskyFeedPost.isRecord, 254 + ) 255 + ) 256 + return undefined 257 + const {text, facets} = quote.record 258 + return text.trim() 259 + ? new RichTextAPI({text: text, facets: facets}) 260 + : undefined 261 + }, [quote.record]) 262 + 263 + const onBeforePress = React.useCallback(() => { 264 + unstableCacheProfileView(queryClient, quote.author) 265 + onOpen?.() 266 + }, [queryClient, quote.author, onOpen]) 267 + 268 + const [hover, setHover] = React.useState(false) 269 + return ( 270 + <View 271 + onPointerEnter={() => { 272 + setHover(true) 273 + }} 274 + onPointerLeave={() => { 275 + setHover(false) 276 + }}> 277 + <ContentHider 278 + modui={moderation?.ui('contentList')} 279 + style={[ 280 + a.rounded_md, 281 + a.p_md, 282 + a.mt_sm, 283 + a.border, 284 + t.atoms.border_contrast_low, 285 + style, 286 + ]} 287 + childContainerStyle={[a.pt_sm]}> 288 + <SubtleWebHover hover={hover} /> 289 + <Link 290 + hoverStyle={{borderColor: pal.colors.borderLinkHover}} 291 + href={itemHref} 292 + title={itemTitle} 293 + onBeforePress={onBeforePress}> 294 + <View pointerEvents="none"> 295 + <PostMeta 296 + author={quote.author} 297 + moderation={moderation} 298 + showAvatar 299 + postHref={itemHref} 300 + timestamp={quote.indexedAt} 301 + /> 302 + </View> 303 + {moderation ? ( 304 + <PostAlerts 305 + modui={moderation.ui('contentView')} 306 + style={[a.py_xs]} 307 + /> 308 + ) : null} 309 + {richText ? ( 310 + <RichText 311 + value={richText} 312 + style={a.text_md} 313 + numberOfLines={20} 314 + disableLinks 315 + /> 316 + ) : null} 317 + {quote.embed && ( 318 + <Embed 319 + embed={quote.embed} 320 + moderation={moderation} 321 + isWithinQuote={parentIsWithinQuote ?? true} 322 + // already within quote? override nested 323 + allowNestedQuotes={ 324 + parentIsWithinQuote ? false : parentAllowNestedQuotes 325 + } 326 + /> 327 + )} 328 + </Link> 329 + </ContentHider> 330 + </View> 331 + ) 332 + }
+25
src/components/Post/Embed/types.ts
··· 1 + import {type StyleProp, type ViewStyle} from 'react-native' 2 + import {type AppBskyFeedDefs, type ModerationDecision} from '@atproto/api' 3 + 4 + export enum PostEmbedViewContext { 5 + ThreadHighlighted = 'ThreadHighlighted', 6 + Feed = 'Feed', 7 + FeedEmbedRecordWithMedia = 'FeedEmbedRecordWithMedia', 8 + } 9 + 10 + export enum QuoteEmbedViewContext { 11 + FeedEmbedRecordWithMedia = PostEmbedViewContext.FeedEmbedRecordWithMedia, 12 + } 13 + 14 + export type CommonProps = { 15 + moderation?: ModerationDecision 16 + onOpen?: () => void 17 + style?: StyleProp<ViewStyle> 18 + viewContext?: PostEmbedViewContext 19 + isWithinQuote?: boolean 20 + allowNestedQuotes?: boolean 21 + } 22 + 23 + export type EmbedProps = CommonProps & { 24 + embed?: AppBskyFeedDefs.PostView['embed'] 25 + }
+1 -1
src/components/dms/ActionsWrapper.tsx
··· 1 1 import {View} from 'react-native' 2 - import {ChatBskyConvoDefs} from '@atproto/api' 2 + import {type ChatBskyConvoDefs} from '@atproto/api' 3 3 import {msg} from '@lingui/macro' 4 4 import {useLingui} from '@lingui/react' 5 5
+5 -4
src/components/dms/MessageItemEmbed.tsx
··· 1 1 import React from 'react' 2 2 import {useWindowDimensions, View} from 'react-native' 3 - import {AppBskyEmbedRecord} from '@atproto/api' 3 + import {type $Typed, type AppBskyEmbedRecord} from '@atproto/api' 4 4 5 - import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds' 6 5 import {atoms as a, native, tokens, useTheme, web} from '#/alf' 6 + import {PostEmbedViewContext} from '#/components/Post/Embed' 7 + import {Embed} from '#/components/Post/Embed' 7 8 import {MessageContextProvider} from './MessageContext' 8 9 9 10 let MessageItemEmbed = ({ 10 11 embed, 11 12 }: { 12 - embed: AppBskyEmbedRecord.View 13 + embed: $Typed<AppBskyEmbedRecord.View> 13 14 }): React.ReactNode => { 14 15 const t = useTheme() 15 16 const screen = useWindowDimensions() ··· 32 33 }), 33 34 ]}> 34 35 <View style={{marginTop: tokens.space.sm * -1}}> 35 - <PostEmbeds 36 + <Embed 36 37 embed={embed} 37 38 allowNestedQuotes 38 39 viewContext={PostEmbedViewContext.Feed}
+2 -2
src/components/hooks/dates.ts
··· 8 8 */ 9 9 10 10 import React from 'react' 11 - import {formatDistance, Locale} from 'date-fns' 11 + import {formatDistance, type Locale} from 'date-fns' 12 12 import { 13 13 ca, 14 14 cy, ··· 47 47 zhTW, 48 48 } from 'date-fns/locale' 49 49 50 - import {AppLanguage} from '#/locale/languages' 50 + import {type AppLanguage} from '#/locale/languages' 51 51 import {useLanguagePrefs} from '#/state/preferences' 52 52 53 53 /**
+13 -3
src/components/moderation/PostHider.tsx
··· 1 - import React, {ComponentProps} from 'react' 2 - import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native' 3 - import {AppBskyActorDefs, ModerationCause, ModerationUI} from '@atproto/api' 1 + import React, {type ComponentProps} from 'react' 2 + import { 3 + Pressable, 4 + type StyleProp, 5 + StyleSheet, 6 + View, 7 + type ViewStyle, 8 + } from 'react-native' 9 + import { 10 + type AppBskyActorDefs, 11 + type ModerationCause, 12 + type ModerationUI, 13 + } from '@atproto/api' 4 14 import {msg, Trans} from '@lingui/macro' 5 15 import {useLingui} from '@lingui/react' 6 16 import {useQueryClient} from '@tanstack/react-query'
+1 -1
src/locale/helpers.ts
··· 1 - import {AppBskyFeedDefs, AppBskyFeedPost} from '@atproto/api' 1 + import {type AppBskyFeedDefs, AppBskyFeedPost} from '@atproto/api' 2 2 import * as bcp47Match from 'bcp-47-match' 3 3 import lande from 'lande' 4 4
+2 -2
src/screens/Login/LoginForm.tsx
··· 3 3 ActivityIndicator, 4 4 Keyboard, 5 5 LayoutAnimation, 6 - TextInput, 6 + type TextInput, 7 7 View, 8 8 } from 'react-native' 9 9 import { 10 10 ComAtprotoServerCreateSession, 11 - ComAtprotoServerDescribeServer, 11 + type ComAtprotoServerDescribeServer, 12 12 } from '@atproto/api' 13 13 import {msg, Trans} from '@lingui/macro' 14 14 import {useLingui} from '@lingui/react'
+3 -3
src/screens/Onboarding/StepProfile/index.tsx
··· 2 2 import {View} from 'react-native' 3 3 import {Image as ExpoImage} from 'expo-image' 4 4 import { 5 - ImagePickerOptions, 5 + type ImagePickerOptions, 6 6 launchImageLibraryAsync, 7 7 MediaTypeOptions, 8 8 } from 'expo-image-picker' ··· 27 27 import {AvatarCreatorItems} from '#/screens/Onboarding/StepProfile/AvatarCreatorItems' 28 28 import { 29 29 PlaceholderCanvas, 30 - PlaceholderCanvasRef, 30 + type PlaceholderCanvasRef, 31 31 } from '#/screens/Onboarding/StepProfile/PlaceholderCanvas' 32 32 import {atoms as a, useBreakpoints, useTheme} from '#/alf' 33 33 import {Button, ButtonIcon, ButtonText} from '#/components/Button' ··· 38 38 import {CircleInfo_Stroke2_Corner0_Rounded} from '#/components/icons/CircleInfo' 39 39 import {StreamingLive_Stroke2_Corner0_Rounded as StreamingLive} from '#/components/icons/StreamingLive' 40 40 import {Text} from '#/components/Typography' 41 - import {AvatarColor, avatarColors, Emoji, emojiItems} from './types' 41 + import {type AvatarColor, avatarColors, type Emoji, emojiItems} from './types' 42 42 43 43 export interface Avatar { 44 44 image?: {
+2 -2
src/screens/PostThread/components/ThreadItemAnchor.tsx
··· 36 36 import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn' 37 37 import {Link} from '#/view/com/util/Link' 38 38 import {formatCount} from '#/view/com/util/numeric/format' 39 - import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds' 40 39 import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' 41 40 import { 42 41 LINEAR_AVI_WIDTH, ··· 53 52 import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' 54 53 import {PostAlerts} from '#/components/moderation/PostAlerts' 55 54 import {type AppModerationCause} from '#/components/Pills' 55 + import {Embed, PostEmbedViewContext} from '#/components/Post/Embed' 56 56 import {PostControls} from '#/components/PostControls' 57 57 import * as Prompt from '#/components/Prompt' 58 58 import {RichText} from '#/components/RichText' ··· 388 388 ) : undefined} 389 389 {post.embed && ( 390 390 <View style={[a.py_xs]}> 391 - <PostEmbeds 391 + <Embed 392 392 embed={post.embed} 393 393 moderation={moderation} 394 394 viewContext={PostEmbedViewContext.ThreadHighlighted}
+2 -2
src/screens/PostThread/components/ThreadItemPost.tsx
··· 25 25 import {type OnPostSuccessData} from '#/state/shell/composer' 26 26 import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' 27 27 import {TextLink} from '#/view/com/util/Link' 28 - import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds' 29 28 import {PostMeta} from '#/view/com/util/PostMeta' 30 29 import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' 31 30 import { ··· 40 39 import {PostAlerts} from '#/components/moderation/PostAlerts' 41 40 import {PostHider} from '#/components/moderation/PostHider' 42 41 import {type AppModerationCause} from '#/components/Pills' 42 + import {Embed, PostEmbedViewContext} from '#/components/Post/Embed' 43 43 import {PostControls} from '#/components/PostControls' 44 44 import {RichText} from '#/components/RichText' 45 45 import * as Skele from '#/components/Skeleton' ··· 323 323 ) : undefined} 324 324 {post.embed && ( 325 325 <View style={[a.pb_xs]}> 326 - <PostEmbeds 326 + <Embed 327 327 embed={post.embed} 328 328 moderation={moderation} 329 329 viewContext={PostEmbedViewContext.Feed}
+2 -2
src/screens/PostThread/components/ThreadItemTreePost.tsx
··· 24 24 import {type OnPostSuccessData} from '#/state/shell/composer' 25 25 import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' 26 26 import {TextLink} from '#/view/com/util/Link' 27 - import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds' 28 27 import {PostMeta} from '#/view/com/util/PostMeta' 29 28 import { 30 29 OUTER_SPACE, ··· 39 38 import {PostAlerts} from '#/components/moderation/PostAlerts' 40 39 import {PostHider} from '#/components/moderation/PostHider' 41 40 import {type AppModerationCause} from '#/components/Pills' 41 + import {Embed, PostEmbedViewContext} from '#/components/Post/Embed' 42 42 import {PostControls} from '#/components/PostControls' 43 43 import {RichText} from '#/components/RichText' 44 44 import * as Skele from '#/components/Skeleton' ··· 369 369 ) : undefined} 370 370 {post.embed && ( 371 371 <View style={[a.pb_xs]}> 372 - <PostEmbeds 372 + <Embed 373 373 embed={post.embed} 374 374 moderation={moderation} 375 375 viewContext={PostEmbedViewContext.Feed}
+2 -2
src/screens/Profile/Header/Handle.tsx
··· 1 1 import {View} from 'react-native' 2 - import {AppBskyActorDefs} from '@atproto/api' 2 + import {type AppBskyActorDefs} from '@atproto/api' 3 3 import {msg, Trans} from '@lingui/macro' 4 4 import {useLingui} from '@lingui/react' 5 5 6 6 import {isInvalidHandle, sanitizeHandle} from '#/lib/strings/handles' 7 7 import {isIOS} from '#/platform/detection' 8 - import {Shadow} from '#/state/cache/types' 8 + import {type Shadow} from '#/state/cache/types' 9 9 import {atoms as a, useTheme, web} from '#/alf' 10 10 import {NewskieDialog} from '#/components/NewskieDialog' 11 11 import {Text} from '#/components/Typography'
+2 -2
src/screens/Signup/StepInfo/Policies.tsx
··· 1 - import {ReactElement} from 'react' 1 + import {type ReactElement} from 'react' 2 2 import {View} from 'react-native' 3 - import {ComAtprotoServerDescribeServer} from '@atproto/api' 3 + import {type ComAtprotoServerDescribeServer} from '@atproto/api' 4 4 import {msg, Trans} from '@lingui/macro' 5 5 import {useLingui} from '@lingui/react' 6 6
+1 -1
src/screens/StarterPack/StarterPackLandingScreen.tsx
··· 5 5 AppBskyGraphDefs, 6 6 AppBskyGraphStarterpack, 7 7 AtUri, 8 - ModerationOpts, 8 + type ModerationOpts, 9 9 } from '@atproto/api' 10 10 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 11 11 import {msg, Trans} from '@lingui/macro'
+2 -2
src/screens/StarterPack/Wizard/State.tsx
··· 1 1 import React from 'react' 2 - import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api' 3 - import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs' 2 + import {type AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api' 3 + import {type GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs' 4 4 import {msg, plural} from '@lingui/macro' 5 5 6 6 import {STARTER_PACK_MAX_SIZE} from '#/lib/constants'
+1 -1
src/screens/Takendown.tsx
··· 3 3 import {SystemBars} from 'react-native-edge-to-edge' 4 4 import {KeyboardAwareScrollView} from 'react-native-keyboard-controller' 5 5 import {useSafeAreaInsets} from 'react-native-safe-area-context' 6 - import {ComAtprotoAdminDefs, ComAtprotoModerationDefs} from '@atproto/api' 6 + import {type ComAtprotoAdminDefs, ComAtprotoModerationDefs} from '@atproto/api' 7 7 import {msg, Trans} from '@lingui/macro' 8 8 import {useLingui} from '@lingui/react' 9 9 import {useMutation} from '@tanstack/react-query'
+1 -1
src/screens/VideoFeed/components/Scrubber.tsx
··· 22 22 import {useEventListener} from 'expo' 23 23 import {VideoPlayer} from 'expo-video' 24 24 25 - import {formatTime} from '#/view/com/util/post-embeds/VideoEmbedInner/web-controls/utils' 26 25 import {tokens} from '#/alf' 27 26 import {atoms as a} from '#/alf' 27 + import {formatTime} from '#/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/utils' 28 28 import {Text} from '#/components/Typography' 29 29 30 30 // magic number that is roughly the min height of the write reply button
+4 -4
src/state/messages/events/agent.ts
··· 1 - import {BskyAgent, ChatBskyConvoGetLog} from '@atproto/api' 1 + import {type BskyAgent, type ChatBskyConvoGetLog} from '@atproto/api' 2 2 import EventEmitter from 'eventemitter3' 3 3 import {nanoid} from 'nanoid/non-secure' 4 4 ··· 9 9 DEFAULT_POLL_INTERVAL, 10 10 } from '#/state/messages/events/const' 11 11 import { 12 - MessagesEventBusDispatch, 12 + type MessagesEventBusDispatch, 13 13 MessagesEventBusDispatchEvent, 14 14 MessagesEventBusErrorCode, 15 - MessagesEventBusEvent, 16 - MessagesEventBusParams, 15 + type MessagesEventBusEvent, 16 + type MessagesEventBusParams, 17 17 MessagesEventBusStatus, 18 18 } from '#/state/messages/events/types' 19 19 import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
+4 -3
src/state/queries/postgate/util.ts
··· 1 1 import { 2 - $Typed, 2 + type $Typed, 3 3 AppBskyEmbedRecord, 4 4 AppBskyEmbedRecordWithMedia, 5 - AppBskyFeedDefs, 6 - AppBskyFeedPostgate, 5 + type AppBskyFeedDefs, 6 + type AppBskyFeedPostgate, 7 7 AtUri, 8 8 } from '@atproto/api' 9 9 ··· 113 113 likeCount: post.likeCount, 114 114 quoteCount: post.quoteCount, 115 115 indexedAt: post.indexedAt, 116 + embeds: post.embed ? [post.embed] : [], 116 117 } 117 118 } 118 119
+2 -2
src/state/session/types.ts
··· 1 - import {LogEvents} from '#/lib/statsig/statsig' 2 - import {PersistedAccount} from '#/state/persisted' 1 + import {type LogEvents} from '#/lib/statsig/statsig' 2 + import {type PersistedAccount} from '#/state/persisted' 3 3 4 4 export type SessionAccount = PersistedAccount 5 5
+1 -1
src/state/threadgate-hidden-replies.tsx
··· 1 1 import React from 'react' 2 - import {AppBskyFeedThreadgate} from '@atproto/api' 2 + import {type AppBskyFeedThreadgate} from '@atproto/api' 3 3 4 4 type StateContext = { 5 5 uris: Set<string>
+10 -5
src/view/com/composer/Composer.tsx
··· 72 72 import {mimeToExt} from '#/lib/media/video/util' 73 73 import {logEvent} from '#/lib/statsig/statsig' 74 74 import {cleanError} from '#/lib/strings/errors' 75 - import {colors, s} from '#/lib/styles' 75 + import {colors} from '#/lib/styles' 76 76 import {logger} from '#/logger' 77 77 import {isAndroid, isIOS, isNative, isWeb} from '#/platform/detection' 78 78 import {useDialogStateControlContext} from '#/state/dialogs' ··· 97 97 ExternalEmbedGif, 98 98 ExternalEmbedLink, 99 99 } from '#/view/com/composer/ExternalEmbed' 100 + import {ExternalEmbedRemoveBtn} from '#/view/com/composer/ExternalEmbedRemoveBtn' 100 101 import {GifAltTextDialog} from '#/view/com/composer/GifAltText' 101 102 import {LabelsBtn} from '#/view/com/composer/labels/LabelsBtn' 102 103 import {Gallery} from '#/view/com/composer/photos/Gallery' ··· 116 117 import {SubtitleDialogBtn} from '#/view/com/composer/videos/SubtitleDialog' 117 118 import {VideoPreview} from '#/view/com/composer/videos/VideoPreview' 118 119 import {VideoTranscodeProgress} from '#/view/com/composer/videos/VideoTranscodeProgress' 119 - import {LazyQuoteEmbed, QuoteX} from '#/view/com/util/post-embeds/QuoteEmbed' 120 120 import {Text} from '#/view/com/util/text/Text' 121 121 import * as Toast from '#/view/com/util/Toast' 122 122 import {UserAvatar} from '#/view/com/util/UserAvatar' ··· 125 125 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' 126 126 import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji' 127 127 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' 128 + import {LazyQuoteEmbed} from '#/components/Post/Embed/LazyQuoteEmbed' 128 129 import * as Prompt from '#/components/Prompt' 129 130 import {Text as NewText} from '#/components/Typography' 130 131 import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet' ··· 1149 1150 )} 1150 1151 </LayoutAnimationConfig> 1151 1152 {embed.quote?.uri ? ( 1152 - <View style={!video ? [a.mt_md] : []}> 1153 - <View style={[s.mt5, s.mb2, isWeb && s.mb10]}> 1153 + <View 1154 + style={[a.pb_sm, video ? [a.pt_md] : [a.pt_xl], isWeb && [a.pb_md]]}> 1155 + <View style={[a.relative]}> 1154 1156 <View style={{pointerEvents: 'none'}}> 1155 1157 <LazyQuoteEmbed uri={embed.quote.uri} /> 1156 1158 </View> 1157 1159 {canRemoveQuote && ( 1158 - <QuoteX onRemove={() => dispatch({type: 'embed_remove_quote'})} /> 1160 + <ExternalEmbedRemoveBtn 1161 + onRemove={() => dispatch({type: 'embed_remove_quote'})} 1162 + style={{top: 16}} 1163 + /> 1159 1164 )} 1160 1165 </View> 1161 1166 </View>
+11 -2
src/view/com/composer/ComposerReplyTo.tsx
··· 13 13 import {sanitizeDisplayName} from '#/lib/strings/display-names' 14 14 import {sanitizeHandle} from '#/lib/strings/handles' 15 15 import {type ComposerOptsPostRef} from '#/state/shell/composer' 16 - import {MaybeQuoteEmbed} from '#/view/com/util/post-embeds/QuoteEmbed' 17 16 import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' 18 17 import {atoms as a, useTheme, web} from '#/alf' 18 + import {QuoteEmbed} from '#/components/Post/Embed' 19 19 import {Text} from '#/components/Typography' 20 20 import {useSimpleVerificationState} from '#/components/verification' 21 21 import {VerificationCheck} from '#/components/verification/VerificationCheck' 22 + import {parseEmbed} from '#/types/bsky/post' 22 23 23 24 export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) { 24 25 const t = useTheme() ··· 51 52 } 52 53 return null 53 54 }, [embed]) 55 + const parsedQuoteEmbed = quoteEmbed 56 + ? parseEmbed({ 57 + $type: 'app.bsky.embed.record#view', 58 + ...quoteEmbed, 59 + }) 60 + : null 54 61 55 62 const images = useMemo(() => { 56 63 if (AppBskyEmbedImages.isView(embed)) { ··· 124 131 <ComposerReplyToImages images={images} showFull={showFull} /> 125 132 )} 126 133 </View> 127 - {showFull && quoteEmbed && <MaybeQuoteEmbed embed={quoteEmbed} />} 134 + {showFull && parsedQuoteEmbed && parsedQuoteEmbed.type === 'post' && ( 135 + <QuoteEmbed embed={parsedQuoteEmbed} /> 136 + )} 128 137 </View> 129 138 </Pressable> 130 139 )
+29 -8
src/view/com/composer/ExternalEmbed.tsx
··· 1 1 import React from 'react' 2 - import {StyleProp, View, ViewStyle} from 'react-native' 2 + import {type StyleProp, View, type ViewStyle} from 'react-native' 3 3 4 4 import {cleanError} from '#/lib/strings/errors' 5 5 import { 6 6 useResolveGifQuery, 7 7 useResolveLinkQuery, 8 8 } from '#/state/queries/resolve-link' 9 - import {Gif} from '#/state/queries/tenor' 9 + import {type Gif} from '#/state/queries/tenor' 10 10 import {ExternalEmbedRemoveBtn} from '#/view/com/composer/ExternalEmbedRemoveBtn' 11 - import {ExternalLinkEmbed} from '#/view/com/util/post-embeds/ExternalLinkEmbed' 12 11 import {atoms as a, useTheme} from '#/alf' 13 12 import {Loader} from '#/components/Loader' 13 + import {ExternalEmbed} from '#/components/Post/Embed/ExternalEmbed' 14 + import {ModeratedFeedEmbed} from '#/components/Post/Embed/FeedEmbed' 15 + import {ModeratedListEmbed} from '#/components/Post/Embed/ListEmbed' 14 16 import {Embed as StarterPackEmbed} from '#/components/StarterPack/StarterPackCard' 15 17 import {Text} from '#/components/Typography' 16 - import {MaybeFeedCard, MaybeListCard} from '../util/post-embeds' 17 18 18 19 export const ExternalEmbedGif = ({ 19 20 onRemove, ··· 44 45 <View style={[a.overflow_hidden, t.atoms.border_contrast_medium]}> 45 46 {linkInfo ? ( 46 47 <View style={{pointerEvents: 'auto'}}> 47 - <ExternalLinkEmbed link={linkInfo} hideAlt /> 48 + <ExternalEmbed link={linkInfo} hideAlt /> 48 49 </View> 49 50 ) : error ? ( 50 51 <Container style={[a.align_start, a.p_md, a.gap_xs]}> ··· 80 81 if (data) { 81 82 if (data.type === 'external') { 82 83 return ( 83 - <ExternalLinkEmbed 84 + <ExternalEmbed 84 85 link={{ 85 86 title: data.title || uri, 86 87 uri, ··· 91 92 /> 92 93 ) 93 94 } else if (data.kind === 'feed') { 94 - return <MaybeFeedCard view={data.view} /> 95 + return ( 96 + <ModeratedFeedEmbed 97 + embed={{ 98 + type: 'feed', 99 + view: { 100 + $type: 'app.bsky.feed.defs#generatorView', 101 + ...data.view, 102 + }, 103 + }} 104 + /> 105 + ) 95 106 } else if (data.kind === 'list') { 96 - return <MaybeListCard view={data.view} /> 107 + return ( 108 + <ModeratedListEmbed 109 + embed={{ 110 + type: 'list', 111 + view: { 112 + $type: 'app.bsky.graph.defs#listView', 113 + ...data.view, 114 + }, 115 + }} 116 + /> 117 + ) 97 118 } else if (data.kind === 'starter-pack') { 98 119 return <StarterPackEmbed starterPack={data.view} /> 99 120 }
+9 -4
src/view/com/composer/ExternalEmbedRemoveBtn.tsx
··· 2 2 import {msg} from '@lingui/macro' 3 3 import {useLingui} from '@lingui/react' 4 4 5 - import {atoms as a} from '#/alf' 5 + import {atoms as a, useTheme, type ViewStyleProp} from '#/alf' 6 6 import {Button, ButtonIcon} from '#/components/Button' 7 7 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' 8 8 9 - export function ExternalEmbedRemoveBtn({onRemove}: {onRemove: () => void}) { 9 + export function ExternalEmbedRemoveBtn({ 10 + onRemove, 11 + style, 12 + }: {onRemove: () => void} & ViewStyleProp) { 13 + const t = useTheme() 10 14 const {_} = useLingui() 11 15 12 16 return ( 13 - <View style={[a.absolute, {top: 8, right: 8}, a.z_50]}> 17 + <View style={[a.absolute, {top: 8, right: 8}, a.z_50, style]}> 14 18 <Button 15 19 label={_(msg`Remove attachment`)} 16 20 onPress={onRemove} 17 21 size="small" 18 22 variant="solid" 19 23 color="secondary" 20 - shape="round"> 24 + shape="round" 25 + style={[t.atoms.shadow_sm]}> 21 26 <ButtonIcon icon={X} size="sm" /> 22 27 </Button> 23 28 </View>
+4 -4
src/view/com/composer/GifAltText.tsx
··· 6 6 import {HITSLOP_10, MAX_ALT_TEXT} from '#/lib/constants' 7 7 import {parseAltFromGIFDescription} from '#/lib/gif-alt-text' 8 8 import { 9 - EmbedPlayerParams, 9 + type EmbedPlayerParams, 10 10 parseEmbedPlayerFromUrl, 11 11 } from '#/lib/strings/embed-player' 12 12 import {isAndroid} from '#/platform/detection' 13 13 import {useResolveGifQuery} from '#/state/queries/resolve-link' 14 - import {Gif} from '#/state/queries/tenor' 14 + import {type Gif} from '#/state/queries/tenor' 15 15 import {AltTextCounterWrapper} from '#/view/com/composer/AltTextCounterWrapper' 16 16 import {atoms as a, useTheme} from '#/alf' 17 17 import {Button, ButtonText} from '#/components/Button' 18 18 import * as Dialog from '#/components/Dialog' 19 - import {DialogControlProps} from '#/components/Dialog' 19 + import {type DialogControlProps} from '#/components/Dialog' 20 20 import * as TextField from '#/components/forms/TextField' 21 21 import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' 22 22 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' 23 23 import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' 24 + import {GifEmbed} from '#/components/Post/Embed/ExternalEmbed/Gif' 24 25 import {Text} from '#/components/Typography' 25 - import {GifEmbed} from '../util/post-embeds/GifEmbed' 26 26 import {AltTextReminder} from './photos/Gallery' 27 27 28 28 export function GifAltTextDialog({
+3 -3
src/view/com/composer/labels/LabelsBtn.tsx
··· 4 4 5 5 import { 6 6 ADULT_CONTENT_LABELS, 7 - AdultSelfLabel, 7 + type AdultSelfLabel, 8 8 OTHER_SELF_LABELS, 9 - OtherSelfLabel, 10 - SelfLabel, 9 + type OtherSelfLabel, 10 + type SelfLabel, 11 11 } from '#/lib/moderation' 12 12 import {isWeb} from '#/platform/detection' 13 13 import {atoms as a, native, useTheme, web} from '#/alf'
+3 -3
src/view/com/composer/photos/ImageAltTextDialog.tsx
··· 1 1 import React from 'react' 2 - import {ImageStyle, useWindowDimensions, View} from 'react-native' 2 + import {type ImageStyle, useWindowDimensions, View} from 'react-native' 3 3 import {Image} from 'expo-image' 4 4 import {msg, Plural, Trans} from '@lingui/macro' 5 5 import {useLingui} from '@lingui/react' ··· 7 7 import {MAX_ALT_TEXT} from '#/lib/constants' 8 8 import {enforceLen} from '#/lib/strings/helpers' 9 9 import {isAndroid, isWeb} from '#/platform/detection' 10 - import {ComposerImage} from '#/state/gallery' 10 + import {type ComposerImage} from '#/state/gallery' 11 11 import {AltTextCounterWrapper} from '#/view/com/composer/AltTextCounterWrapper' 12 12 import {atoms as a, useTheme} from '#/alf' 13 13 import {Button, ButtonText} from '#/components/Button' 14 14 import * as Dialog from '#/components/Dialog' 15 - import {DialogControlProps} from '#/components/Dialog' 15 + import {type DialogControlProps} from '#/components/Dialog' 16 16 import * as TextField from '#/components/forms/TextField' 17 17 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' 18 18 import {Text} from '#/components/Typography'
+1 -1
src/view/com/composer/photos/OpenCameraBtn.tsx
··· 8 8 import {openCamera} from '#/lib/media/picker' 9 9 import {logger} from '#/logger' 10 10 import {isMobileWeb, isNative} from '#/platform/detection' 11 - import {ComposerImage, createComposerImage} from '#/state/gallery' 11 + import {type ComposerImage, createComposerImage} from '#/state/gallery' 12 12 import {atoms as a, useTheme} from '#/alf' 13 13 import {Button} from '#/components/Button' 14 14 import {Camera_Stroke2_Corner0_Rounded as Camera} from '#/components/icons/Camera'
+3 -3
src/view/com/post-thread/PostThreadItem.tsx
··· 46 46 import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 47 47 import {Link, TextLink} from '#/view/com/util/Link' 48 48 import {formatCount} from '#/view/com/util/numeric/format' 49 - import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds' 50 49 import {PostMeta} from '#/view/com/util/PostMeta' 51 50 import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' 52 51 import {atoms as a, useTheme} from '#/alf' ··· 62 61 import {PostAlerts} from '#/components/moderation/PostAlerts' 63 62 import {PostHider} from '#/components/moderation/PostHider' 64 63 import {type AppModerationCause} from '#/components/Pills' 64 + import {Embed, PostEmbedViewContext} from '#/components/Post/Embed' 65 65 import {PostControls} from '#/components/PostControls' 66 66 import * as Prompt from '#/components/Prompt' 67 67 import {RichText} from '#/components/RichText' ··· 465 465 ) : undefined} 466 466 {post.embed && ( 467 467 <View style={[a.py_xs]}> 468 - <PostEmbeds 468 + <Embed 469 469 embed={post.embed} 470 470 moderation={moderation} 471 471 viewContext={PostEmbedViewContext.ThreadHighlighted} ··· 697 697 ) : undefined} 698 698 {post.embed && ( 699 699 <View style={[a.pb_xs]}> 700 - <PostEmbeds 700 + <Embed 701 701 embed={post.embed} 702 702 moderation={moderation} 703 703 viewContext={PostEmbedViewContext.Feed}
+2 -2
src/view/com/post/Post.tsx
··· 28 28 import {precacheProfile} from '#/state/queries/profile' 29 29 import {useSession} from '#/state/session' 30 30 import {Link, TextLink} from '#/view/com/util/Link' 31 - import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds' 32 31 import {PostMeta} from '#/view/com/util/PostMeta' 33 32 import {Text} from '#/view/com/util/text/Text' 34 33 import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' ··· 37 36 import {ContentHider} from '#/components/moderation/ContentHider' 38 37 import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' 39 38 import {PostAlerts} from '#/components/moderation/PostAlerts' 39 + import {Embed, PostEmbedViewContext} from '#/components/Post/Embed' 40 40 import {PostControls} from '#/components/PostControls' 41 41 import {ProfileHoverCard} from '#/components/ProfileHoverCard' 42 42 import {RichText} from '#/components/RichText' ··· 248 248 /> 249 249 ) : undefined} 250 250 {post.embed ? ( 251 - <PostEmbeds 251 + <Embed 252 252 embed={post.embed} 253 253 moderation={moderation} 254 254 viewContext={PostEmbedViewContext.Feed}
+3 -2
src/view/com/posts/PostFeedItem.tsx
··· 42 42 } from '#/state/unstable-post-source' 43 43 import {FeedNameText} from '#/view/com/util/FeedInfoText' 44 44 import {Link, TextLink, TextLinkOnWebOnly} from '#/view/com/util/Link' 45 - import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds' 46 45 import {PostMeta} from '#/view/com/util/PostMeta' 47 46 import {Text} from '#/view/com/util/text/Text' 48 47 import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' ··· 53 52 import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' 54 53 import {PostAlerts} from '#/components/moderation/PostAlerts' 55 54 import {type AppModerationCause} from '#/components/Pills' 55 + import {Embed} from '#/components/Post/Embed' 56 + import {PostEmbedViewContext} from '#/components/Post/Embed/types' 56 57 import {PostControls} from '#/components/PostControls' 57 58 import {DiscoverDebug} from '#/components/PostControls/DiscoverDebug' 58 59 import {ProfileHoverCard} from '#/components/ProfileHoverCard' ··· 568 569 ) : undefined} 569 570 {postEmbed ? ( 570 571 <View style={[a.pb_xs]}> 571 - <PostEmbeds 572 + <Embed 572 573 embed={postEmbed} 573 574 moderation={moderation} 574 575 onOpen={onOpenEmbed}
+4 -4
src/view/com/util/Views.web.tsx
··· 14 14 15 15 import React from 'react' 16 16 import { 17 - FlatList, 18 - FlatListProps, 19 - ScrollViewProps, 17 + type FlatList, 18 + type FlatListProps, 19 + type ScrollViewProps, 20 20 StyleSheet, 21 21 View, 22 - ViewProps, 22 + type ViewProps, 23 23 } from 'react-native' 24 24 import Animated from 'react-native-reanimated' 25 25
+1 -1
src/view/com/util/images/Gallery.tsx
··· 8 8 9 9 import {type Dimensions} from '#/lib/media/types' 10 10 import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' 11 - import {PostEmbedViewContext} from '#/view/com/util/post-embeds/types' 12 11 import {atoms as a, useTheme} from '#/alf' 13 12 import {MediaInsetBorder} from '#/components/MediaInsetBorder' 13 + import {PostEmbedViewContext} from '#/components/Post/Embed/types' 14 14 import {Text} from '#/components/Typography' 15 15 16 16 type EventFunction = (index: number) => void
+1 -1
src/view/com/util/images/ImageLayoutGrid.tsx
··· 3 3 import {type AnimatedRef, useAnimatedRef} from 'react-native-reanimated' 4 4 import {type AppBskyEmbedImages} from '@atproto/api' 5 5 6 - import {PostEmbedViewContext} from '#/view/com/util/post-embeds/types' 7 6 import {atoms as a, useBreakpoints} from '#/alf' 7 + import {PostEmbedViewContext} from '#/components/Post/Embed/types' 8 8 import {type Dimensions} from '../../lightbox/ImageViewing/@types' 9 9 import {GalleryItem} from './Gallery' 10 10
src/view/com/util/post-embeds/ActiveVideoWebContext.tsx src/components/Post/Embed/VideoEmbed/ActiveVideoWebContext.tsx
+1 -1
src/view/com/util/post-embeds/ExternalGifEmbed.tsx src/components/Post/Embed/ExternalEmbed/ExternalGif.tsx
··· 14 14 import {Fill} from '#/components/Fill' 15 15 import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' 16 16 17 - export function ExternalGifEmbed({ 17 + export function ExternalGif({ 18 18 link, 19 19 params, 20 20 }: {
+5 -5
src/view/com/util/post-embeds/ExternalLinkEmbed.tsx src/components/Post/Embed/ExternalEmbed/index.tsx
··· 12 12 import {toNiceDomain} from '#/lib/strings/url-helpers' 13 13 import {isNative} from '#/platform/detection' 14 14 import {useExternalEmbedsPrefs} from '#/state/preferences' 15 - import {ExternalGifEmbed} from '#/view/com/util/post-embeds/ExternalGifEmbed' 16 - import {ExternalPlayer} from '#/view/com/util/post-embeds/ExternalPlayerEmbed' 17 - import {GifEmbed} from '#/view/com/util/post-embeds/GifEmbed' 18 15 import {atoms as a, useTheme} from '#/alf' 19 16 import {Divider} from '#/components/Divider' 20 17 import {Earth_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' 21 18 import {Link} from '#/components/Link' 22 19 import {Text} from '#/components/Typography' 20 + import {ExternalGif} from './ExternalGif' 21 + import {ExternalPlayer} from './ExternalPlayer' 22 + import {GifEmbed} from './Gif' 23 23 24 - export const ExternalLinkEmbed = ({ 24 + export const ExternalEmbed = ({ 25 25 link, 26 26 onOpen, 27 27 style, ··· 106 106 ) : undefined} 107 107 108 108 {embedPlayerParams?.isGif ? ( 109 - <ExternalGifEmbed link={link} params={embedPlayerParams} /> 109 + <ExternalGif link={link} params={embedPlayerParams} /> 110 110 ) : embedPlayerParams ? ( 111 111 <ExternalPlayer link={link} params={embedPlayerParams} /> 112 112 ) : undefined}
+1 -1
src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx src/components/Post/Embed/ExternalEmbed/ExternalPlayer.tsx
··· 25 25 import {EmbedPlayerParams, getPlayerAspect} from '#/lib/strings/embed-player' 26 26 import {isNative} from '#/platform/detection' 27 27 import {useExternalEmbedsPrefs} from '#/state/preferences' 28 + import {EventStopper} from '#/view/com/util/EventStopper' 28 29 import {atoms as a, useTheme} from '#/alf' 29 30 import {useDialogControl} from '#/components/Dialog' 30 31 import {EmbedConsentDialog} from '#/components/dialogs/EmbedConsent' 31 32 import {Fill} from '#/components/Fill' 32 33 import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' 33 - import {EventStopper} from '../EventStopper' 34 34 35 35 interface ShouldStartLoadRequest { 36 36 url: string
src/view/com/util/post-embeds/GifEmbed.tsx src/components/Post/Embed/ExternalEmbed/Gif.tsx
-337
src/view/com/util/post-embeds/QuoteEmbed.tsx
··· 1 - import React from 'react' 2 - import { 3 - StyleProp, 4 - StyleSheet, 5 - TouchableOpacity, 6 - View, 7 - ViewStyle, 8 - } from 'react-native' 9 - import { 10 - AppBskyEmbedExternal, 11 - AppBskyEmbedImages, 12 - AppBskyEmbedRecord, 13 - AppBskyEmbedRecordWithMedia, 14 - AppBskyEmbedVideo, 15 - AppBskyFeedDefs, 16 - AppBskyFeedPost, 17 - moderatePost, 18 - ModerationDecision, 19 - RichText as RichTextAPI, 20 - } from '@atproto/api' 21 - import {AtUri} from '@atproto/api' 22 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 23 - import {msg, Trans} from '@lingui/macro' 24 - import {useLingui} from '@lingui/react' 25 - import {useQueryClient} from '@tanstack/react-query' 26 - 27 - import {HITSLOP_20} from '#/lib/constants' 28 - import {usePalette} from '#/lib/hooks/usePalette' 29 - import {InfoCircleIcon} from '#/lib/icons' 30 - import {makeProfileLink} from '#/lib/routes/links' 31 - import {s} from '#/lib/styles' 32 - import {useModerationOpts} from '#/state/preferences/moderation-opts' 33 - import {precacheProfile} from '#/state/queries/profile' 34 - import {useResolveLinkQuery} from '#/state/queries/resolve-link' 35 - import {useSession} from '#/state/session' 36 - import {atoms as a, useTheme} from '#/alf' 37 - import {RichText} from '#/components/RichText' 38 - import {SubtleWebHover} from '#/components/SubtleWebHover' 39 - import * as bsky from '#/types/bsky' 40 - import {ContentHider} from '../../../../components/moderation/ContentHider' 41 - import {PostAlerts} from '../../../../components/moderation/PostAlerts' 42 - import {Link} from '../Link' 43 - import {PostMeta} from '../PostMeta' 44 - import {Text} from '../text/Text' 45 - import {PostEmbeds} from '.' 46 - import {QuoteEmbedViewContext} from './types' 47 - 48 - export function MaybeQuoteEmbed({ 49 - embed, 50 - onOpen, 51 - style, 52 - allowNestedQuotes, 53 - viewContext, 54 - }: { 55 - embed: AppBskyEmbedRecord.View 56 - onOpen?: () => void 57 - style?: StyleProp<ViewStyle> 58 - allowNestedQuotes?: boolean 59 - viewContext?: QuoteEmbedViewContext 60 - }) { 61 - const t = useTheme() 62 - const pal = usePalette('default') 63 - const {currentAccount} = useSession() 64 - if ( 65 - AppBskyEmbedRecord.isViewRecord(embed.record) && 66 - AppBskyFeedPost.isRecord(embed.record.value) && 67 - AppBskyFeedPost.validateRecord(embed.record.value).success 68 - ) { 69 - return ( 70 - <QuoteEmbedModerated 71 - viewRecord={embed.record} 72 - onOpen={onOpen} 73 - style={style} 74 - allowNestedQuotes={allowNestedQuotes} 75 - viewContext={viewContext} 76 - /> 77 - ) 78 - } else if (AppBskyEmbedRecord.isViewBlocked(embed.record)) { 79 - return ( 80 - <View 81 - style={[styles.errorContainer, a.border, t.atoms.border_contrast_low]}> 82 - <InfoCircleIcon size={18} style={pal.text} /> 83 - <Text type="lg" style={pal.text}> 84 - <Trans>Blocked</Trans> 85 - </Text> 86 - </View> 87 - ) 88 - } else if (AppBskyEmbedRecord.isViewNotFound(embed.record)) { 89 - return ( 90 - <View 91 - style={[styles.errorContainer, a.border, t.atoms.border_contrast_low]}> 92 - <InfoCircleIcon size={18} style={pal.text} /> 93 - <Text type="lg" style={pal.text}> 94 - <Trans>Deleted</Trans> 95 - </Text> 96 - </View> 97 - ) 98 - } else if (AppBskyEmbedRecord.isViewDetached(embed.record)) { 99 - const isViewerOwner = currentAccount?.did 100 - ? embed.record.uri.includes(currentAccount.did) 101 - : false 102 - return ( 103 - <View 104 - style={[styles.errorContainer, a.border, t.atoms.border_contrast_low]}> 105 - <InfoCircleIcon size={18} style={pal.text} /> 106 - <Text type="lg" style={pal.text}> 107 - {isViewerOwner ? ( 108 - <Trans>Removed by you</Trans> 109 - ) : ( 110 - <Trans>Removed by author</Trans> 111 - )} 112 - </Text> 113 - </View> 114 - ) 115 - } 116 - return null 117 - } 118 - 119 - function QuoteEmbedModerated({ 120 - viewRecord, 121 - onOpen, 122 - style, 123 - allowNestedQuotes, 124 - viewContext, 125 - }: { 126 - viewRecord: AppBskyEmbedRecord.ViewRecord 127 - onOpen?: () => void 128 - style?: StyleProp<ViewStyle> 129 - allowNestedQuotes?: boolean 130 - viewContext?: QuoteEmbedViewContext 131 - }) { 132 - const moderationOpts = useModerationOpts() 133 - const postView = React.useMemo( 134 - () => viewRecordToPostView(viewRecord), 135 - [viewRecord], 136 - ) 137 - const moderation = React.useMemo(() => { 138 - return moderationOpts ? moderatePost(postView, moderationOpts) : undefined 139 - }, [postView, moderationOpts]) 140 - 141 - return ( 142 - <QuoteEmbed 143 - quote={postView} 144 - moderation={moderation} 145 - onOpen={onOpen} 146 - style={style} 147 - allowNestedQuotes={allowNestedQuotes} 148 - viewContext={viewContext} 149 - /> 150 - ) 151 - } 152 - 153 - export function QuoteEmbed({ 154 - quote, 155 - moderation, 156 - onOpen, 157 - style, 158 - allowNestedQuotes, 159 - }: { 160 - quote: AppBskyFeedDefs.PostView 161 - moderation?: ModerationDecision 162 - onOpen?: () => void 163 - style?: StyleProp<ViewStyle> 164 - allowNestedQuotes?: boolean 165 - viewContext?: QuoteEmbedViewContext 166 - }) { 167 - const t = useTheme() 168 - const queryClient = useQueryClient() 169 - const pal = usePalette('default') 170 - const itemUrip = new AtUri(quote.uri) 171 - const itemHref = makeProfileLink(quote.author, 'post', itemUrip.rkey) 172 - const itemTitle = `Post by ${quote.author.handle}` 173 - 174 - const richText = React.useMemo(() => { 175 - if ( 176 - !bsky.dangerousIsType<AppBskyFeedPost.Record>( 177 - quote.record, 178 - AppBskyFeedPost.isRecord, 179 - ) 180 - ) 181 - return undefined 182 - const {text, facets} = quote.record 183 - return text.trim() 184 - ? new RichTextAPI({text: text, facets: facets}) 185 - : undefined 186 - }, [quote.record]) 187 - 188 - const embed = React.useMemo(() => { 189 - const e = quote.embed 190 - 191 - if (allowNestedQuotes) { 192 - return e 193 - } else { 194 - if ( 195 - AppBskyEmbedImages.isView(e) || 196 - AppBskyEmbedExternal.isView(e) || 197 - AppBskyEmbedVideo.isView(e) 198 - ) { 199 - return e 200 - } else if ( 201 - AppBskyEmbedRecordWithMedia.isView(e) && 202 - (AppBskyEmbedImages.isView(e.media) || 203 - AppBskyEmbedExternal.isView(e.media) || 204 - AppBskyEmbedVideo.isView(e.media)) 205 - ) { 206 - return e.media 207 - } 208 - } 209 - }, [quote.embed, allowNestedQuotes]) 210 - 211 - const onBeforePress = React.useCallback(() => { 212 - precacheProfile(queryClient, quote.author) 213 - onOpen?.() 214 - }, [queryClient, quote.author, onOpen]) 215 - 216 - const [hover, setHover] = React.useState(false) 217 - return ( 218 - <View 219 - onPointerEnter={() => { 220 - setHover(true) 221 - }} 222 - onPointerLeave={() => { 223 - setHover(false) 224 - }}> 225 - <ContentHider 226 - modui={moderation?.ui('contentList')} 227 - style={[ 228 - a.rounded_md, 229 - a.p_md, 230 - a.mt_sm, 231 - a.border, 232 - t.atoms.border_contrast_low, 233 - style, 234 - ]} 235 - childContainerStyle={[a.pt_sm]}> 236 - <SubtleWebHover hover={hover} /> 237 - <Link 238 - hoverStyle={{borderColor: pal.colors.borderLinkHover}} 239 - href={itemHref} 240 - title={itemTitle} 241 - onBeforePress={onBeforePress}> 242 - <View pointerEvents="none"> 243 - <PostMeta 244 - author={quote.author} 245 - moderation={moderation} 246 - showAvatar 247 - postHref={itemHref} 248 - timestamp={quote.indexedAt} 249 - /> 250 - </View> 251 - {moderation ? ( 252 - <PostAlerts 253 - modui={moderation.ui('contentView')} 254 - style={[a.py_xs]} 255 - /> 256 - ) : null} 257 - {richText ? ( 258 - <RichText 259 - value={richText} 260 - style={a.text_md} 261 - numberOfLines={20} 262 - disableLinks 263 - /> 264 - ) : null} 265 - {embed && <PostEmbeds embed={embed} moderation={moderation} />} 266 - </Link> 267 - </ContentHider> 268 - </View> 269 - ) 270 - } 271 - 272 - export function QuoteX({onRemove}: {onRemove: () => void}) { 273 - const {_} = useLingui() 274 - return ( 275 - <TouchableOpacity 276 - style={[ 277 - a.absolute, 278 - a.p_xs, 279 - a.rounded_full, 280 - a.align_center, 281 - a.justify_center, 282 - { 283 - top: 16, 284 - right: 10, 285 - backgroundColor: 'rgba(0, 0, 0, 0.75)', 286 - }, 287 - ]} 288 - onPress={onRemove} 289 - accessibilityRole="button" 290 - accessibilityLabel={_(msg`Remove quote`)} 291 - accessibilityHint={_(msg`Removes quoted post`)} 292 - onAccessibilityEscape={onRemove} 293 - hitSlop={HITSLOP_20}> 294 - <FontAwesomeIcon size={12} icon="xmark" style={s.white} /> 295 - </TouchableOpacity> 296 - ) 297 - } 298 - 299 - export function LazyQuoteEmbed({uri}: {uri: string}) { 300 - const {data} = useResolveLinkQuery(uri) 301 - const moderationOpts = useModerationOpts() 302 - if (!data || data.type !== 'record' || data.kind !== 'post') { 303 - return null 304 - } 305 - const moderation = moderationOpts 306 - ? moderatePost(data.view, moderationOpts) 307 - : undefined 308 - return <QuoteEmbed quote={data.view} moderation={moderation} /> 309 - } 310 - 311 - function viewRecordToPostView( 312 - viewRecord: AppBskyEmbedRecord.ViewRecord, 313 - ): AppBskyFeedDefs.PostView { 314 - const {value, embeds, ...rest} = viewRecord 315 - return { 316 - ...rest, 317 - $type: 'app.bsky.feed.defs#postView', 318 - record: value, 319 - embed: embeds?.[0], 320 - } 321 - } 322 - 323 - const styles = StyleSheet.create({ 324 - errorContainer: { 325 - flexDirection: 'row', 326 - alignItems: 'center', 327 - gap: 4, 328 - borderRadius: 8, 329 - marginTop: 8, 330 - paddingVertical: 14, 331 - paddingHorizontal: 14, 332 - borderWidth: StyleSheet.hairlineWidth, 333 - }, 334 - alert: { 335 - marginBottom: 6, 336 - }, 337 - })
+2 -2
src/view/com/util/post-embeds/VideoEmbed.tsx src/components/Post/Embed/VideoEmbed/index.tsx
··· 5 5 import {msg, Trans} from '@lingui/macro' 6 6 import {useLingui} from '@lingui/react' 7 7 8 + import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' 8 9 import {ConstrainedImage} from '#/view/com/util/images/AutoSizedImage' 9 - import {VideoEmbedInnerNative} from '#/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative' 10 10 import {atoms as a, useTheme} from '#/alf' 11 11 import {Button} from '#/components/Button' 12 12 import {useThrottledValue} from '#/components/hooks/useThrottledValue' 13 13 import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' 14 - import {ErrorBoundary} from '../ErrorBoundary' 14 + import {VideoEmbedInnerNative} from './VideoEmbedInner/VideoEmbedInnerNative' 15 15 import * as VideoFallback from './VideoEmbedInner/VideoFallback' 16 16 17 17 interface Props {
+5 -5
src/view/com/util/post-embeds/VideoEmbed.web.tsx src/components/Post/Embed/VideoEmbed/index.web.tsx
··· 5 5 import {useLingui} from '@lingui/react' 6 6 7 7 import {isFirefox} from '#/lib/browser' 8 + import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' 8 9 import {ConstrainedImage} from '#/view/com/util/images/AutoSizedImage' 10 + import {atoms as a} from '#/alf' 11 + import {useIsWithinMessage} from '#/components/dms/MessageContext' 12 + import {useFullscreen} from '#/components/hooks/useFullscreen' 9 13 import { 10 14 HLSUnsupportedError, 11 15 VideoEmbedInnerWeb, 12 16 VideoNotFoundError, 13 - } from '#/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb' 14 - import {atoms as a} from '#/alf' 15 - import {useIsWithinMessage} from '#/components/dms/MessageContext' 16 - import {useFullscreen} from '#/components/hooks/useFullscreen' 17 - import {ErrorBoundary} from '../ErrorBoundary' 17 + } from '#/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb' 18 18 import {useActiveVideoWeb} from './ActiveVideoWebContext' 19 19 import * as VideoFallback from './VideoEmbedInner/VideoFallback' 20 20
src/view/com/util/post-embeds/VideoEmbedInner/TimeIndicator.tsx src/components/Post/Embed/VideoEmbed/VideoEmbedInner/TimeIndicator.tsx
+1 -1
src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx
··· 7 7 8 8 import {HITSLOP_30} from '#/lib/constants' 9 9 import {useAutoplayDisabled} from '#/state/preferences' 10 - import {useVideoMuteState} from '#/view/com/util/post-embeds/VideoVolumeContext' 11 10 import {atoms as a, useTheme} from '#/alf' 12 11 import {useIsWithinMessage} from '#/components/dms/MessageContext' 13 12 import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute' ··· 15 14 import {Play_Filled_Corner0_Rounded as PlayIcon} from '#/components/icons/Play' 16 15 import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker' 17 16 import {MediaInsetBorder} from '#/components/MediaInsetBorder' 17 + import {useVideoMuteState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext' 18 18 import {TimeIndicator} from './TimeIndicator' 19 19 20 20 export const VideoEmbedInnerNative = React.forwardRef(
src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.web.tsx src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.web.tsx
src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.native.tsx src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.native.tsx
src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx
src/view/com/util/post-embeds/VideoEmbedInner/VideoFallback.tsx src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx
src/view/com/util/post-embeds/VideoEmbedInner/bandwidth-estimate.ts src/components/Post/Embed/VideoEmbed/VideoEmbedInner/bandwidth-estimate.ts
+1 -1
src/view/com/util/post-embeds/VideoEmbedInner/web-controls/ControlButton.tsx src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/ControlButton.tsx
··· 1 1 import React from 'react' 2 2 import {SvgProps} from 'react-native-svg' 3 3 4 + import {PressableWithHover} from '#/view/com/util/PressableWithHover' 4 5 import {atoms as a, useTheme, web} from '#/alf' 5 - import {PressableWithHover} from '../../../PressableWithHover' 6 6 7 7 export function ControlButton({ 8 8 active,
src/view/com/util/post-embeds/VideoEmbedInner/web-controls/Scrubber.tsx src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/Scrubber.tsx
src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.native.tsx src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.native.tsx
src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx
+1 -1
src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VolumeControl.tsx src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx
··· 8 8 import {atoms as a} from '#/alf' 9 9 import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute' 10 10 import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker' 11 - import {useVideoVolumeState} from '../../VideoVolumeContext' 11 + import {useVideoVolumeState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext' 12 12 import {ControlButton} from './ControlButton' 13 13 14 14 export function VolumeControl({
+3 -3
src/view/com/util/post-embeds/VideoEmbedInner/web-controls/utils.tsx src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/utils.tsx
··· 1 - import React, {useCallback, useEffect, useRef, useState} from 'react' 1 + import {type RefObject, useCallback, useEffect, useRef, useState} from 'react' 2 2 3 3 import {isSafari} from '#/lib/browser' 4 - import {useVideoVolumeState} from '../../VideoVolumeContext' 4 + import {useVideoVolumeState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext' 5 5 6 - export function useVideoElement(ref: React.RefObject<HTMLVideoElement>) { 6 + export function useVideoElement(ref: RefObject<HTMLVideoElement>) { 7 7 const [playing, setPlaying] = useState(false) 8 8 const [muted, setMuted] = useState(true) 9 9 const [currentTime, setCurrentTime] = useState(0)
src/view/com/util/post-embeds/VideoVolumeContext.tsx src/components/Post/Embed/VideoEmbed/VideoVolumeContext.tsx
-327
src/view/com/util/post-embeds/index.tsx
··· 1 - import React from 'react' 2 - import { 3 - InteractionManager, 4 - type StyleProp, 5 - StyleSheet, 6 - View, 7 - type ViewStyle, 8 - } from 'react-native' 9 - import { 10 - type AnimatedRef, 11 - measure, 12 - type MeasuredDimensions, 13 - runOnJS, 14 - runOnUI, 15 - } from 'react-native-reanimated' 16 - import {Image} from 'expo-image' 17 - import { 18 - AppBskyEmbedExternal, 19 - AppBskyEmbedImages, 20 - AppBskyEmbedRecord, 21 - AppBskyEmbedRecordWithMedia, 22 - AppBskyEmbedVideo, 23 - AppBskyFeedDefs, 24 - AppBskyGraphDefs, 25 - moderateFeedGenerator, 26 - moderateUserList, 27 - type ModerationDecision, 28 - } from '@atproto/api' 29 - 30 - import {usePalette} from '#/lib/hooks/usePalette' 31 - import {useLightboxControls} from '#/state/lightbox' 32 - import {useModerationOpts} from '#/state/preferences/moderation-opts' 33 - import {FeedSourceCard} from '#/view/com/feeds/FeedSourceCard' 34 - import {atoms as a, useTheme} from '#/alf' 35 - import * as ListCard from '#/components/ListCard' 36 - import {Embed as StarterPackCard} from '#/components/StarterPack/StarterPackCard' 37 - import {ContentHider} from '../../../../components/moderation/ContentHider' 38 - import {type Dimensions} from '../../lightbox/ImageViewing/@types' 39 - import {AutoSizedImage} from '../images/AutoSizedImage' 40 - import {ImageLayoutGrid} from '../images/ImageLayoutGrid' 41 - import {ExternalLinkEmbed} from './ExternalLinkEmbed' 42 - import {MaybeQuoteEmbed} from './QuoteEmbed' 43 - import {PostEmbedViewContext, QuoteEmbedViewContext} from './types' 44 - import {VideoEmbed} from './VideoEmbed' 45 - 46 - export * from './types' 47 - 48 - type Embed = 49 - | AppBskyEmbedRecord.View 50 - | AppBskyEmbedImages.View 51 - | AppBskyEmbedVideo.View 52 - | AppBskyEmbedExternal.View 53 - | AppBskyEmbedRecordWithMedia.View 54 - | {$type: string; [k: string]: unknown} 55 - 56 - export function PostEmbeds({ 57 - embed, 58 - moderation, 59 - onOpen, 60 - style, 61 - allowNestedQuotes, 62 - viewContext, 63 - }: { 64 - embed?: Embed 65 - moderation?: ModerationDecision 66 - onOpen?: () => void 67 - style?: StyleProp<ViewStyle> 68 - allowNestedQuotes?: boolean 69 - viewContext?: PostEmbedViewContext 70 - }) { 71 - const {openLightbox} = useLightboxControls() 72 - 73 - // quote post with media 74 - // = 75 - if (AppBskyEmbedRecordWithMedia.isView(embed)) { 76 - return ( 77 - <View style={style}> 78 - <PostEmbeds 79 - embed={embed.media} 80 - moderation={moderation} 81 - onOpen={onOpen} 82 - viewContext={viewContext} 83 - /> 84 - <MaybeQuoteEmbed 85 - embed={embed.record} 86 - onOpen={onOpen} 87 - viewContext={ 88 - viewContext === PostEmbedViewContext.Feed 89 - ? QuoteEmbedViewContext.FeedEmbedRecordWithMedia 90 - : undefined 91 - } 92 - /> 93 - </View> 94 - ) 95 - } 96 - 97 - if (AppBskyEmbedRecord.isView(embed)) { 98 - // custom feed embed (i.e. generator view) 99 - if (AppBskyFeedDefs.isGeneratorView(embed.record)) { 100 - return ( 101 - <View style={a.mt_sm}> 102 - <MaybeFeedCard view={embed.record} /> 103 - </View> 104 - ) 105 - } 106 - 107 - // list embed 108 - if (AppBskyGraphDefs.isListView(embed.record)) { 109 - return ( 110 - <View style={a.mt_sm}> 111 - <MaybeListCard view={embed.record} /> 112 - </View> 113 - ) 114 - } 115 - 116 - // starter pack embed 117 - if (AppBskyGraphDefs.isStarterPackViewBasic(embed.record)) { 118 - return ( 119 - <View style={a.mt_sm}> 120 - <StarterPackCard starterPack={embed.record} /> 121 - </View> 122 - ) 123 - } 124 - 125 - // quote post 126 - // = 127 - return ( 128 - <MaybeQuoteEmbed 129 - embed={embed} 130 - style={style} 131 - onOpen={onOpen} 132 - allowNestedQuotes={allowNestedQuotes} 133 - /> 134 - ) 135 - } 136 - 137 - // image embed 138 - // = 139 - if (AppBskyEmbedImages.isView(embed)) { 140 - const {images} = embed 141 - 142 - if (images.length > 0) { 143 - const items = embed.images.map(img => ({ 144 - uri: img.fullsize, 145 - thumbUri: img.thumb, 146 - alt: img.alt, 147 - dimensions: img.aspectRatio ?? null, 148 - })) 149 - const _openLightbox = ( 150 - index: number, 151 - thumbRects: (MeasuredDimensions | null)[], 152 - fetchedDims: (Dimensions | null)[], 153 - ) => { 154 - openLightbox({ 155 - images: items.map((item, i) => ({ 156 - ...item, 157 - thumbRect: thumbRects[i] ?? null, 158 - thumbDimensions: fetchedDims[i] ?? null, 159 - type: 'image', 160 - })), 161 - index, 162 - }) 163 - } 164 - const onPress = ( 165 - index: number, 166 - refs: AnimatedRef<any>[], 167 - fetchedDims: (Dimensions | null)[], 168 - ) => { 169 - runOnUI(() => { 170 - 'worklet' 171 - const rects: (MeasuredDimensions | null)[] = [] 172 - for (const r of refs) { 173 - rects.push(measure(r)) 174 - } 175 - runOnJS(_openLightbox)(index, rects, fetchedDims) 176 - })() 177 - } 178 - const onPressIn = (_: number) => { 179 - InteractionManager.runAfterInteractions(() => { 180 - Image.prefetch(items.map(i => i.uri)) 181 - }) 182 - } 183 - 184 - if (images.length === 1) { 185 - const image = images[0] 186 - return ( 187 - <ContentHider modui={moderation?.ui('contentMedia')}> 188 - <View style={[a.mt_sm, style]}> 189 - <AutoSizedImage 190 - crop={ 191 - viewContext === PostEmbedViewContext.ThreadHighlighted 192 - ? 'none' 193 - : viewContext === 194 - PostEmbedViewContext.FeedEmbedRecordWithMedia 195 - ? 'square' 196 - : 'constrained' 197 - } 198 - image={image} 199 - onPress={(containerRef, dims) => 200 - onPress(0, [containerRef], [dims]) 201 - } 202 - onPressIn={() => onPressIn(0)} 203 - hideBadge={ 204 - viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia 205 - } 206 - /> 207 - </View> 208 - </ContentHider> 209 - ) 210 - } 211 - 212 - return ( 213 - <ContentHider modui={moderation?.ui('contentMedia')}> 214 - <View style={[a.mt_sm, style]}> 215 - <ImageLayoutGrid 216 - images={embed.images} 217 - onPress={onPress} 218 - onPressIn={onPressIn} 219 - viewContext={viewContext} 220 - /> 221 - </View> 222 - </ContentHider> 223 - ) 224 - } 225 - } 226 - 227 - // external link embed 228 - // = 229 - if (AppBskyEmbedExternal.isView(embed)) { 230 - const link = embed.external 231 - return ( 232 - <ContentHider modui={moderation?.ui('contentMedia')}> 233 - <ExternalLinkEmbed 234 - link={link} 235 - onOpen={onOpen} 236 - style={[a.mt_sm, style]} 237 - /> 238 - </ContentHider> 239 - ) 240 - } 241 - 242 - // video embed 243 - // = 244 - if (AppBskyEmbedVideo.isView(embed)) { 245 - return ( 246 - <ContentHider modui={moderation?.ui('contentMedia')}> 247 - <VideoEmbed 248 - embed={embed} 249 - crop={ 250 - viewContext === PostEmbedViewContext.ThreadHighlighted 251 - ? 'none' 252 - : viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia 253 - ? 'square' 254 - : 'constrained' 255 - } 256 - /> 257 - </ContentHider> 258 - ) 259 - } 260 - 261 - return <View /> 262 - } 263 - 264 - export function MaybeFeedCard({view}: {view: AppBskyFeedDefs.GeneratorView}) { 265 - const pal = usePalette('default') 266 - const moderationOpts = useModerationOpts() 267 - const moderation = React.useMemo(() => { 268 - return moderationOpts 269 - ? moderateFeedGenerator(view, moderationOpts) 270 - : undefined 271 - }, [view, moderationOpts]) 272 - 273 - return ( 274 - <ContentHider modui={moderation?.ui('contentList')}> 275 - <FeedSourceCard 276 - feedUri={view.uri} 277 - style={[pal.view, pal.border, styles.customFeedOuter]} 278 - showLikes 279 - /> 280 - </ContentHider> 281 - ) 282 - } 283 - 284 - export function MaybeListCard({view}: {view: AppBskyGraphDefs.ListView}) { 285 - const moderationOpts = useModerationOpts() 286 - const moderation = React.useMemo(() => { 287 - return moderationOpts ? moderateUserList(view, moderationOpts) : undefined 288 - }, [view, moderationOpts]) 289 - const t = useTheme() 290 - 291 - return ( 292 - <ContentHider modui={moderation?.ui('contentList')}> 293 - <View 294 - style={[ 295 - a.border, 296 - t.atoms.border_contrast_medium, 297 - a.p_md, 298 - a.rounded_sm, 299 - ]}> 300 - <ListCard.Default view={view} /> 301 - </View> 302 - </ContentHider> 303 - ) 304 - } 305 - 306 - const styles = StyleSheet.create({ 307 - altContainer: { 308 - backgroundColor: 'rgba(0, 0, 0, 0.75)', 309 - borderRadius: 6, 310 - paddingHorizontal: 6, 311 - paddingVertical: 3, 312 - position: 'absolute', 313 - right: 6, 314 - bottom: 6, 315 - }, 316 - alt: { 317 - color: 'white', 318 - fontSize: 7, 319 - fontWeight: '600', 320 - }, 321 - customFeedOuter: { 322 - borderWidth: StyleSheet.hairlineWidth, 323 - borderRadius: 8, 324 - paddingHorizontal: 12, 325 - paddingVertical: 12, 326 - }, 327 - })
-9
src/view/com/util/post-embeds/types.ts
··· 1 - export enum PostEmbedViewContext { 2 - ThreadHighlighted = 'ThreadHighlighted', 3 - Feed = 'Feed', 4 - FeedEmbedRecordWithMedia = 'FeedEmbedRecordWithMedia', 5 - } 6 - 7 - export enum QuoteEmbedViewContext { 8 - FeedEmbedRecordWithMedia = PostEmbedViewContext.FeedEmbedRecordWithMedia, 9 - }