diff options
75 files changed, 812 insertions, 799 deletions
diff --git a/bskylink/src/routes/index.ts b/bskylink/src/routes/index.ts index 9fd20d276..d0122ff8b 100644 --- a/bskylink/src/routes/index.ts +++ b/bskylink/src/routes/index.ts @@ -1,6 +1,6 @@ -import {Express} from 'express' +import {type Express} from 'express' -import {AppContext} from '../context.js' +import {type AppContext} from '../context.js' import {default as createShortLink} from './createShortLink.js' import {default as health} from './health.js' import {default as redirect} from './redirect.js' diff --git a/bskylink/src/routes/redirect.ts b/bskylink/src/routes/redirect.ts index 468d25019..7d68e4245 100644 --- a/bskylink/src/routes/redirect.ts +++ b/bskylink/src/routes/redirect.ts @@ -2,9 +2,9 @@ import assert from 'node:assert' import {DAY, SECOND} from '@atproto/common' import escapeHTML from 'escape-html' -import {Express} from 'express' +import {type Express} from 'express' -import {AppContext} from '../context.js' +import {type AppContext} from '../context.js' import {handler} from './util.js' const INTERNAL_IP_REGEX = new RegExp( diff --git a/bskylink/src/routes/root.ts b/bskylink/src/routes/root.ts index 12bdf1515..8c6c4afc3 100644 --- a/bskylink/src/routes/root.ts +++ b/bskylink/src/routes/root.ts @@ -1,6 +1,6 @@ -import {Express} from 'express' +import {type Express} from 'express' -import {AppContext} from '../context.js' +import {type AppContext} from '../context.js' import {handler} from './util.js' export default function (ctx: AppContext, app: Express) { diff --git a/src/App.native.tsx b/src/App.native.tsx index 25d186dcf..81d4a870e 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -59,7 +59,6 @@ import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies' import {TestCtrls} from '#/view/com/testing/TestCtrls' -import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext' import * as Toast from '#/view/com/util/Toast' import {Shell} from '#/view/shell' import {ThemeProvider as Alf} from '#/alf' @@ -69,6 +68,7 @@ import {NuxDialogs} from '#/components/dialogs/nuxs' import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs' import {Provider as PortalProvider} from '#/components/Portal' +import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext' import {Splash} from '#/Splash' import {BottomSheetProvider} from '../modules/bottom-sheet' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' diff --git a/src/App.web.tsx b/src/App.web.tsx index fa8e24e53..b706774fd 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -48,8 +48,6 @@ import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide' import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies' -import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoWebContext' -import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext' import * as Toast from '#/view/com/util/Toast' import {ToastContainer} from '#/view/com/util/Toast.web' import {Shell} from '#/view/shell/index' @@ -60,6 +58,8 @@ import {NuxDialogs} from '#/components/dialogs/nuxs' import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs' import {Provider as PortalProvider} from '#/components/Portal' +import {Provider as ActiveVideoProvider} from '#/components/Post/Embed/VideoEmbed/ActiveVideoWebContext' +import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottomBarBorder' diff --git a/src/alf/util/systemUI.ts b/src/alf/util/systemUI.ts index c973e10ea..9e5769c4c 100644 --- a/src/alf/util/systemUI.ts +++ b/src/alf/util/systemUI.ts @@ -1,7 +1,7 @@ import * as SystemUI from 'expo-system-ui' import {isAndroid} from '#/platform/detection' -import {Theme} from '../types' +import {type Theme} from '../types' export function setSystemUITheme(themeType: 'theme' | 'lightbox', t: Theme) { if (isAndroid) { diff --git a/src/components/ContextMenu/Backdrop.tsx b/src/components/ContextMenu/Backdrop.tsx index 027bf9849..37fcebf49 100644 --- a/src/components/ContextMenu/Backdrop.tsx +++ b/src/components/ContextMenu/Backdrop.tsx @@ -2,7 +2,7 @@ import {Pressable} from 'react-native' import Animated, { Extrapolation, interpolate, - SharedValue, + type SharedValue, useAnimatedStyle, } from 'react-native-reanimated' import {msg} from '@lingui/macro' diff --git a/src/components/FeedInterstitials.tsx b/src/components/FeedInterstitials.tsx index 6ecc3f5a8..a92e7be7f 100644 --- a/src/components/FeedInterstitials.tsx +++ b/src/components/FeedInterstitials.tsx @@ -1,24 +1,30 @@ import React from 'react' import {View} from 'react-native' import {ScrollView} from 'react-native-gesture-handler' -import {AppBskyFeedDefs, AtUri} from '@atproto/api' +import {type AppBskyFeedDefs, AtUri} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' -import {NavigationProp} from '#/lib/routes/types' +import {type NavigationProp} from '#/lib/routes/types' import {logEvent} from '#/lib/statsig/statsig' import {logger} from '#/logger' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useGetPopularFeedsQuery} from '#/state/queries/feed' -import {FeedDescriptor} from '#/state/queries/post-feed' +import {type FeedDescriptor} from '#/state/queries/post-feed' import {useProfilesQuery} from '#/state/queries/profile' import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows' import {useSession} from '#/state/session' import * as userActionHistory from '#/state/userActionHistory' -import {SeenPost} from '#/state/userActionHistory' +import {type SeenPost} from '#/state/userActionHistory' import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture' -import {atoms as a, useBreakpoints, useTheme, ViewStyleProp, web} from '#/alf' +import { + atoms as a, + useBreakpoints, + useTheme, + type ViewStyleProp, + web, +} from '#/alf' import {Button} from '#/components/Button' import * as FeedCard from '#/components/FeedCard' import {ArrowRight_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow' @@ -27,7 +33,7 @@ import {PersonPlus_Stroke2_Corner0_Rounded as Person} from '#/components/icons/P import {InlineLinkText} from '#/components/Link' import * as ProfileCard from '#/components/ProfileCard' import {Text} from '#/components/Typography' -import * as bsky from '#/types/bsky' +import type * as bsky from '#/types/bsky' import {ProgressGuideList} from './ProgressGuide/List' const MOBILE_CARD_WIDTH = 300 diff --git a/src/components/Layout/Header/index.tsx b/src/components/Layout/Header/index.tsx index 44faa9649..d68f4bd1d 100644 --- a/src/components/Layout/Header/index.tsx +++ b/src/components/Layout/Header/index.tsx @@ -1,24 +1,24 @@ import {createContext, useCallback, useContext} from 'react' -import {GestureResponderEvent, Keyboard, View} from 'react-native' +import {type GestureResponderEvent, Keyboard, View} from 'react-native' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' import {HITSLOP_30} from '#/lib/constants' -import {NavigationProp} from '#/lib/routes/types' +import {type NavigationProp} from '#/lib/routes/types' import {isIOS} from '#/platform/detection' import {useSetDrawerOpen} from '#/state/shell' import { atoms as a, platform, - TextStyleProp, + type TextStyleProp, useBreakpoints, useGutters, useLayoutBreakpoints, useTheme, web, } from '#/alf' -import {Button, ButtonIcon, ButtonProps} from '#/components/Button' +import {Button, ButtonIcon, type ButtonProps} from '#/components/Button' import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeft} from '#/components/icons/Arrow' import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu' import { diff --git a/src/components/Menu/context.tsx b/src/components/Menu/context.tsx index d810a03de..076bc8151 100644 --- a/src/components/Menu/context.tsx +++ b/src/components/Menu/context.tsx @@ -1,6 +1,6 @@ import React from 'react' -import type {ContextType, ItemContextType} from '#/components/Menu/types' +import {type ContextType, type ItemContextType} from '#/components/Menu/types' export const Context = React.createContext<ContextType | null>(null) diff --git a/src/view/com/util/post-embeds/ExternalGifEmbed.tsx b/src/components/Post/Embed/ExternalEmbed/ExternalGif.tsx index 39c1d109e..8a12f0374 100644 --- a/src/view/com/util/post-embeds/ExternalGifEmbed.tsx +++ b/src/components/Post/Embed/ExternalEmbed/ExternalGif.tsx @@ -14,7 +14,7 @@ import {EmbedConsentDialog} from '#/components/dialogs/EmbedConsent' import {Fill} from '#/components/Fill' import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' -export function ExternalGifEmbed({ +export function ExternalGif({ link, params, }: { diff --git a/src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx b/src/components/Post/Embed/ExternalEmbed/ExternalPlayer.tsx index e78abdf17..7f6d53340 100644 --- a/src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx +++ b/src/components/Post/Embed/ExternalEmbed/ExternalPlayer.tsx @@ -25,12 +25,12 @@ import {NavigationProp} from '#/lib/routes/types' import {EmbedPlayerParams, getPlayerAspect} from '#/lib/strings/embed-player' import {isNative} from '#/platform/detection' import {useExternalEmbedsPrefs} from '#/state/preferences' +import {EventStopper} from '#/view/com/util/EventStopper' import {atoms as a, useTheme} from '#/alf' import {useDialogControl} from '#/components/Dialog' import {EmbedConsentDialog} from '#/components/dialogs/EmbedConsent' import {Fill} from '#/components/Fill' import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' -import {EventStopper} from '../EventStopper' interface ShouldStartLoadRequest { url: string diff --git a/src/view/com/util/post-embeds/GifEmbed.tsx b/src/components/Post/Embed/ExternalEmbed/Gif.tsx index a839294f1..a839294f1 100644 --- a/src/view/com/util/post-embeds/GifEmbed.tsx +++ b/src/components/Post/Embed/ExternalEmbed/Gif.tsx diff --git a/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx b/src/components/Post/Embed/ExternalEmbed/index.tsx index 7ca11f60d..714eaecd6 100644 --- a/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx +++ b/src/components/Post/Embed/ExternalEmbed/index.tsx @@ -12,16 +12,16 @@ import {parseEmbedPlayerFromUrl} from '#/lib/strings/embed-player' import {toNiceDomain} from '#/lib/strings/url-helpers' import {isNative} from '#/platform/detection' import {useExternalEmbedsPrefs} from '#/state/preferences' -import {ExternalGifEmbed} from '#/view/com/util/post-embeds/ExternalGifEmbed' -import {ExternalPlayer} from '#/view/com/util/post-embeds/ExternalPlayerEmbed' -import {GifEmbed} from '#/view/com/util/post-embeds/GifEmbed' import {atoms as a, useTheme} from '#/alf' import {Divider} from '#/components/Divider' import {Earth_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' import {Link} from '#/components/Link' import {Text} from '#/components/Typography' +import {ExternalGif} from './ExternalGif' +import {ExternalPlayer} from './ExternalPlayer' +import {GifEmbed} from './Gif' -export const ExternalLinkEmbed = ({ +export const ExternalEmbed = ({ link, onOpen, style, @@ -106,7 +106,7 @@ export const ExternalLinkEmbed = ({ ) : undefined} {embedPlayerParams?.isGif ? ( - <ExternalGifEmbed link={link} params={embedPlayerParams} /> + <ExternalGif link={link} params={embedPlayerParams} /> ) : embedPlayerParams ? ( <ExternalPlayer link={link} params={embedPlayerParams} /> ) : undefined} diff --git a/src/components/Post/Embed/FeedEmbed.tsx b/src/components/Post/Embed/FeedEmbed.tsx new file mode 100644 index 000000000..fad4cd4d8 --- /dev/null +++ b/src/components/Post/Embed/FeedEmbed.tsx @@ -0,0 +1,52 @@ +import React from 'react' +import {StyleSheet} from 'react-native' +import {moderateFeedGenerator} from '@atproto/api' + +import {usePalette} from '#/lib/hooks/usePalette' +import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {FeedSourceCard} from '#/view/com/feeds/FeedSourceCard' +import {ContentHider} from '#/components/moderation/ContentHider' +import {type EmbedType} from '#/types/bsky/post' +import {type CommonProps} from './types' + +export function FeedEmbed({ + embed, +}: CommonProps & { + embed: EmbedType<'feed'> +}) { + const pal = usePalette('default') + return ( + <FeedSourceCard + feedUri={embed.view.uri} + style={[pal.view, pal.border, styles.customFeedOuter]} + showLikes + /> + ) +} + +export function ModeratedFeedEmbed({ + embed, +}: CommonProps & { + embed: EmbedType<'feed'> +}) { + const moderationOpts = useModerationOpts() + const moderation = React.useMemo(() => { + return moderationOpts + ? moderateFeedGenerator(embed.view, moderationOpts) + : undefined + }, [embed.view, moderationOpts]) + return ( + <ContentHider modui={moderation?.ui('contentList')}> + <FeedEmbed embed={embed} /> + </ContentHider> + ) +} + +const styles = StyleSheet.create({ + customFeedOuter: { + borderWidth: StyleSheet.hairlineWidth, + borderRadius: 8, + paddingHorizontal: 12, + paddingVertical: 12, + }, +}) diff --git a/src/components/Post/Embed/ImageEmbed.tsx b/src/components/Post/Embed/ImageEmbed.tsx new file mode 100644 index 000000000..030d237a0 --- /dev/null +++ b/src/components/Post/Embed/ImageEmbed.tsx @@ -0,0 +1,106 @@ +import {InteractionManager, View} from 'react-native' +import { + type AnimatedRef, + measure, + type MeasuredDimensions, + runOnJS, + runOnUI, +} from 'react-native-reanimated' +import {Image} from 'expo-image' + +import {useLightboxControls} from '#/state/lightbox' +import {type Dimensions} from '#/view/com/lightbox/ImageViewing/@types' +import {AutoSizedImage} from '#/view/com/util/images/AutoSizedImage' +import {ImageLayoutGrid} from '#/view/com/util/images/ImageLayoutGrid' +import {atoms as a} from '#/alf' +import {PostEmbedViewContext} from '#/components/Post/Embed/types' +import {type EmbedType} from '#/types/bsky/post' +import {type CommonProps} from './types' + +export function ImageEmbed({ + embed, + ...rest +}: CommonProps & { + embed: EmbedType<'images'> +}) { + const {openLightbox} = useLightboxControls() + const {images} = embed.view + + if (images.length > 0) { + const items = images.map(img => ({ + uri: img.fullsize, + thumbUri: img.thumb, + alt: img.alt, + dimensions: img.aspectRatio ?? null, + })) + const _openLightbox = ( + index: number, + thumbRects: (MeasuredDimensions | null)[], + fetchedDims: (Dimensions | null)[], + ) => { + openLightbox({ + images: items.map((item, i) => ({ + ...item, + thumbRect: thumbRects[i] ?? null, + thumbDimensions: fetchedDims[i] ?? null, + type: 'image', + })), + index, + }) + } + const onPress = ( + index: number, + refs: AnimatedRef<any>[], + fetchedDims: (Dimensions | null)[], + ) => { + runOnUI(() => { + 'worklet' + const rects: (MeasuredDimensions | null)[] = [] + for (const r of refs) { + rects.push(measure(r)) + } + runOnJS(_openLightbox)(index, rects, fetchedDims) + })() + } + const onPressIn = (_: number) => { + InteractionManager.runAfterInteractions(() => { + Image.prefetch(items.map(i => i.uri)) + }) + } + + if (images.length === 1) { + const image = images[0] + return ( + <View style={[a.mt_sm, rest.style]}> + <AutoSizedImage + crop={ + rest.viewContext === PostEmbedViewContext.ThreadHighlighted + ? 'none' + : rest.viewContext === + PostEmbedViewContext.FeedEmbedRecordWithMedia + ? 'square' + : 'constrained' + } + image={image} + onPress={(containerRef, dims) => onPress(0, [containerRef], [dims])} + onPressIn={() => onPressIn(0)} + hideBadge={ + rest.viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia + } + /> + </View> + ) + } + + return ( + <View style={[a.mt_sm, rest.style]}> + <ImageLayoutGrid + images={images} + onPress={onPress} + onPressIn={onPressIn} + viewContext={rest.viewContext} + /> + </View> + ) + } +} diff --git a/src/components/Post/Embed/LazyQuoteEmbed.tsx b/src/components/Post/Embed/LazyQuoteEmbed.tsx new file mode 100644 index 000000000..fdc1c6309 --- /dev/null +++ b/src/components/Post/Embed/LazyQuoteEmbed.tsx @@ -0,0 +1,37 @@ +import {useMemo} from 'react' +import {View} from 'react-native' + +import {createEmbedViewRecordFromPost} from '#/state/queries/postgate/util' +import {useResolveLinkQuery} from '#/state/queries/resolve-link' +import {atoms as a, useTheme} from '#/alf' +import {QuoteEmbed} from '#/components/Post/Embed' + +export function LazyQuoteEmbed({uri}: {uri: string}) { + const t = useTheme() + const {data} = useResolveLinkQuery(uri) + + const view = useMemo(() => { + if (!data || data.type !== 'record' || data.kind !== 'post') return + return createEmbedViewRecordFromPost(data.view) + }, [data]) + + return view ? ( + <QuoteEmbed + embed={{ + type: 'post', + view, + }} + /> + ) : ( + <View + style={[ + a.w_full, + a.rounded_md, + t.atoms.bg_contrast_25, + { + height: 68, + }, + ]} + /> + ) +} diff --git a/src/components/Post/Embed/ListEmbed.tsx b/src/components/Post/Embed/ListEmbed.tsx new file mode 100644 index 000000000..dc79a7579 --- /dev/null +++ b/src/components/Post/Embed/ListEmbed.tsx @@ -0,0 +1,42 @@ +import React from 'react' +import {View} from 'react-native' +import {moderateUserList} from '@atproto/api' + +import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {atoms as a, useTheme} from '#/alf' +import * as ListCard from '#/components/ListCard' +import {ContentHider} from '#/components/moderation/ContentHider' +import {EmbedType} from '#/types/bsky/post' +import {CommonProps} from './types' + +export function ListEmbed({ + embed, +}: CommonProps & { + embed: EmbedType<'list'> +}) { + const t = useTheme() + return ( + <View + style={[a.border, t.atoms.border_contrast_medium, a.p_md, a.rounded_sm]}> + <ListCard.Default view={embed.view} /> + </View> + ) +} + +export function ModeratedListEmbed({ + embed, +}: CommonProps & { + embed: EmbedType<'list'> +}) { + const moderationOpts = useModerationOpts() + const moderation = React.useMemo(() => { + return moderationOpts + ? moderateUserList(embed.view, moderationOpts) + : undefined + }, [embed.view, moderationOpts]) + return ( + <ContentHider modui={moderation?.ui('contentList')}> + <ListEmbed embed={embed} /> + </ContentHider> + ) +} diff --git a/src/components/Post/Embed/PostPlaceholder.tsx b/src/components/Post/Embed/PostPlaceholder.tsx new file mode 100644 index 000000000..840234026 --- /dev/null +++ b/src/components/Post/Embed/PostPlaceholder.tsx @@ -0,0 +1,33 @@ +import {StyleSheet, View} from 'react-native' + +import {usePalette} from '#/lib/hooks/usePalette' +import {InfoCircleIcon} from '#/lib/icons' +import {Text} from '#/view/com/util/text/Text' +import {atoms as a, useTheme} from '#/alf' + +export function PostPlaceholder({children}: {children: React.ReactNode}) { + const t = useTheme() + const pal = usePalette('default') + return ( + <View + style={[styles.errorContainer, a.border, t.atoms.border_contrast_low]}> + <InfoCircleIcon size={18} style={pal.text} /> + <Text type="lg" style={pal.text}> + {children} + </Text> + </View> + ) +} + +const styles = StyleSheet.create({ + errorContainer: { + flexDirection: 'row', + alignItems: 'center', + gap: 4, + borderRadius: 8, + marginTop: 8, + paddingVertical: 14, + paddingHorizontal: 14, + borderWidth: StyleSheet.hairlineWidth, + }, +}) diff --git a/src/view/com/util/post-embeds/ActiveVideoWebContext.tsx b/src/components/Post/Embed/VideoEmbed/ActiveVideoWebContext.tsx index a038403b2..a038403b2 100644 --- a/src/view/com/util/post-embeds/ActiveVideoWebContext.tsx +++ b/src/components/Post/Embed/VideoEmbed/ActiveVideoWebContext.tsx diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/TimeIndicator.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/TimeIndicator.tsx index 95401309f..95401309f 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/TimeIndicator.tsx +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/TimeIndicator.tsx diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx index 8b44f5448..88879d45a 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx @@ -7,7 +7,6 @@ import {useLingui} from '@lingui/react' import {HITSLOP_30} from '#/lib/constants' import {useAutoplayDisabled} from '#/state/preferences' -import {useVideoMuteState} from '#/view/com/util/post-embeds/VideoVolumeContext' import {atoms as a, useTheme} from '#/alf' import {useIsWithinMessage} from '#/components/dms/MessageContext' import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute' @@ -15,6 +14,7 @@ import {Pause_Filled_Corner0_Rounded as PauseIcon} from '#/components/icons/Paus import {Play_Filled_Corner0_Rounded as PlayIcon} from '#/components/icons/Play' import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker' import {MediaInsetBorder} from '#/components/MediaInsetBorder' +import {useVideoMuteState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext' import {TimeIndicator} from './TimeIndicator' export const VideoEmbedInnerNative = React.forwardRef( diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.web.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.web.tsx index 2760c7faf..2760c7faf 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.web.tsx +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.web.tsx diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.native.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.native.tsx index 8664aae14..8664aae14 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.native.tsx +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.native.tsx diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx index ce3a7b2c9..ce3a7b2c9 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/VideoFallback.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx index 1b46163cc..1b46163cc 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/VideoFallback.tsx +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/bandwidth-estimate.ts b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/bandwidth-estimate.ts index 122e10aef..122e10aef 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/bandwidth-estimate.ts +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/bandwidth-estimate.ts diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/ControlButton.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/ControlButton.tsx index 651046445..1b69a3e25 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/ControlButton.tsx +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/ControlButton.tsx @@ -1,8 +1,8 @@ import React from 'react' import {SvgProps} from 'react-native-svg' +import {PressableWithHover} from '#/view/com/util/PressableWithHover' import {atoms as a, useTheme, web} from '#/alf' -import {PressableWithHover} from '../../../PressableWithHover' export function ControlButton({ active, diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/Scrubber.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/Scrubber.tsx index 96960bad4..96960bad4 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/Scrubber.tsx +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/Scrubber.tsx diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.native.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.native.tsx index e2e24ed36..e2e24ed36 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.native.tsx +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.native.tsx diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx index 6d14deafc..6d14deafc 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VolumeControl.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx index 90ffb9e6b..e0b688075 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VolumeControl.tsx +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx @@ -8,7 +8,7 @@ import {isSafari, isTouchDevice} from '#/lib/browser' import {atoms as a} from '#/alf' import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute' import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker' -import {useVideoVolumeState} from '../../VideoVolumeContext' +import {useVideoVolumeState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext' import {ControlButton} from './ControlButton' export function VolumeControl({ diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/utils.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/utils.tsx index 108814ea2..320f61a5f 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/utils.tsx +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/utils.tsx @@ -1,9 +1,9 @@ -import React, {useCallback, useEffect, useRef, useState} from 'react' +import {type RefObject, useCallback, useEffect, useRef, useState} from 'react' import {isSafari} from '#/lib/browser' -import {useVideoVolumeState} from '../../VideoVolumeContext' +import {useVideoVolumeState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext' -export function useVideoElement(ref: React.RefObject<HTMLVideoElement>) { +export function useVideoElement(ref: RefObject<HTMLVideoElement>) { const [playing, setPlaying] = useState(false) const [muted, setMuted] = useState(true) const [currentTime, setCurrentTime] = useState(0) diff --git a/src/view/com/util/post-embeds/VideoVolumeContext.tsx b/src/components/Post/Embed/VideoEmbed/VideoVolumeContext.tsx index 6343081da..6343081da 100644 --- a/src/view/com/util/post-embeds/VideoVolumeContext.tsx +++ b/src/components/Post/Embed/VideoEmbed/VideoVolumeContext.tsx diff --git a/src/view/com/util/post-embeds/VideoEmbed.tsx b/src/components/Post/Embed/VideoEmbed/index.tsx index b45027089..fe29ecad6 100644 --- a/src/view/com/util/post-embeds/VideoEmbed.tsx +++ b/src/components/Post/Embed/VideoEmbed/index.tsx @@ -5,13 +5,13 @@ import {AppBskyEmbedVideo} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' import {ConstrainedImage} from '#/view/com/util/images/AutoSizedImage' -import {VideoEmbedInnerNative} from '#/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative' import {atoms as a, useTheme} from '#/alf' import {Button} from '#/components/Button' import {useThrottledValue} from '#/components/hooks/useThrottledValue' import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' -import {ErrorBoundary} from '../ErrorBoundary' +import {VideoEmbedInnerNative} from './VideoEmbedInner/VideoEmbedInnerNative' import * as VideoFallback from './VideoEmbedInner/VideoFallback' interface Props { diff --git a/src/view/com/util/post-embeds/VideoEmbed.web.tsx b/src/components/Post/Embed/VideoEmbed/index.web.tsx index b0ded6754..53adc3b6a 100644 --- a/src/view/com/util/post-embeds/VideoEmbed.web.tsx +++ b/src/components/Post/Embed/VideoEmbed/index.web.tsx @@ -5,16 +5,16 @@ import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {isFirefox} from '#/lib/browser' +import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' import {ConstrainedImage} from '#/view/com/util/images/AutoSizedImage' +import {atoms as a} from '#/alf' +import {useIsWithinMessage} from '#/components/dms/MessageContext' +import {useFullscreen} from '#/components/hooks/useFullscreen' import { HLSUnsupportedError, VideoEmbedInnerWeb, VideoNotFoundError, -} from '#/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb' -import {atoms as a} from '#/alf' -import {useIsWithinMessage} from '#/components/dms/MessageContext' -import {useFullscreen} from '#/components/hooks/useFullscreen' -import {ErrorBoundary} from '../ErrorBoundary' +} from '#/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb' import {useActiveVideoWeb} from './ActiveVideoWebContext' import * as VideoFallback from './VideoEmbedInner/VideoFallback' diff --git a/src/components/Post/Embed/index.tsx b/src/components/Post/Embed/index.tsx new file mode 100644 index 000000000..ace85dc98 --- /dev/null +++ b/src/components/Post/Embed/index.tsx @@ -0,0 +1,332 @@ +import React from 'react' +import {View} from 'react-native' +import { + type $Typed, + type AppBskyFeedDefs, + AppBskyFeedPost, + AtUri, + moderatePost, + RichText as RichTextAPI, +} from '@atproto/api' +import {Trans} from '@lingui/macro' +import {useQueryClient} from '@tanstack/react-query' + +import {usePalette} from '#/lib/hooks/usePalette' +import {makeProfileLink} from '#/lib/routes/links' +import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {unstableCacheProfileView} from '#/state/queries/profile' +import {useSession} from '#/state/session' +import {Link} from '#/view/com/util/Link' +import {PostMeta} from '#/view/com/util/PostMeta' +import {atoms as a, useTheme} from '#/alf' +import {ContentHider} from '#/components/moderation/ContentHider' +import {PostAlerts} from '#/components/moderation/PostAlerts' +import {RichText} from '#/components/RichText' +import {Embed as StarterPackCard} from '#/components/StarterPack/StarterPackCard' +import {SubtleWebHover} from '#/components/SubtleWebHover' +import * as bsky from '#/types/bsky' +import { + type Embed as TEmbed, + type EmbedType, + parseEmbed, +} from '#/types/bsky/post' +import {ExternalEmbed} from './ExternalEmbed' +import {ModeratedFeedEmbed} from './FeedEmbed' +import {ImageEmbed} from './ImageEmbed' +import {ModeratedListEmbed} from './ListEmbed' +import {PostPlaceholder as PostPlaceholderText} from './PostPlaceholder' +import { + type CommonProps, + type EmbedProps, + PostEmbedViewContext, + QuoteEmbedViewContext, +} from './types' +import {VideoEmbed} from './VideoEmbed' + +export {PostEmbedViewContext, QuoteEmbedViewContext} from './types' + +export function Embed({embed: rawEmbed, ...rest}: EmbedProps) { + const embed = parseEmbed(rawEmbed) + + switch (embed.type) { + case 'images': + case 'link': + case 'video': { + return <MediaEmbed embed={embed} {...rest} /> + } + case 'feed': + case 'list': + case 'starter_pack': + case 'labeler': + case 'post': + case 'post_not_found': + case 'post_blocked': + case 'post_detached': { + return <RecordEmbed embed={embed} {...rest} /> + } + case 'post_with_media': { + return ( + <View style={rest.style}> + <MediaEmbed embed={embed.media} {...rest} /> + <RecordEmbed embed={embed.view} {...rest} /> + </View> + ) + } + default: { + return null + } + } +} + +function MediaEmbed({ + embed, + ...rest +}: CommonProps & { + embed: TEmbed +}) { + switch (embed.type) { + case 'images': { + return ( + <ContentHider modui={rest.moderation?.ui('contentMedia')}> + <ImageEmbed embed={embed} {...rest} /> + </ContentHider> + ) + } + case 'link': { + return ( + <ContentHider modui={rest.moderation?.ui('contentMedia')}> + <ExternalEmbed + link={embed.view.external} + onOpen={rest.onOpen} + style={[a.mt_sm, rest.style]} + /> + </ContentHider> + ) + } + case 'video': { + return ( + <ContentHider modui={rest.moderation?.ui('contentMedia')}> + <VideoEmbed embed={embed.view} /> + </ContentHider> + ) + } + default: { + return null + } + } +} + +function RecordEmbed({ + embed, + ...rest +}: CommonProps & { + embed: TEmbed +}) { + switch (embed.type) { + case 'feed': { + return ( + <View style={a.mt_sm}> + <ModeratedFeedEmbed embed={embed} {...rest} /> + </View> + ) + } + case 'list': { + return ( + <View style={a.mt_sm}> + <ModeratedListEmbed embed={embed} /> + </View> + ) + } + case 'starter_pack': { + return ( + <View style={a.mt_sm}> + <StarterPackCard starterPack={embed.view} /> + </View> + ) + } + case 'labeler': { + // not implemented + return null + } + case 'post': { + if (rest.isWithinQuote && !rest.allowNestedQuotes) { + return null + } + + return ( + <QuoteEmbed + {...rest} + embed={embed} + viewContext={ + rest.viewContext === PostEmbedViewContext.Feed + ? QuoteEmbedViewContext.FeedEmbedRecordWithMedia + : undefined + } + isWithinQuote={rest.isWithinQuote} + allowNestedQuotes={rest.allowNestedQuotes} + /> + ) + } + case 'post_not_found': { + return ( + <PostPlaceholderText> + <Trans>Deleted</Trans> + </PostPlaceholderText> + ) + } + case 'post_blocked': { + return ( + <PostPlaceholderText> + <Trans>Blocked</Trans> + </PostPlaceholderText> + ) + } + case 'post_detached': { + return <PostDetachedEmbed embed={embed} /> + } + default: { + return null + } + } +} + +export function PostDetachedEmbed({ + embed, +}: { + embed: EmbedType<'post_detached'> +}) { + const {currentAccount} = useSession() + const isViewerOwner = currentAccount?.did + ? embed.view.uri.includes(currentAccount.did) + : false + + return ( + <PostPlaceholderText> + {isViewerOwner ? ( + <Trans>Removed by you</Trans> + ) : ( + <Trans>Removed by author</Trans> + )} + </PostPlaceholderText> + ) +} + +/* + * Nests parent `Embed` component and therefore must live in this file to avoid + * circular imports. + */ +export function QuoteEmbed({ + embed, + onOpen, + style, + isWithinQuote: parentIsWithinQuote, + allowNestedQuotes: parentAllowNestedQuotes, +}: Omit<CommonProps, 'viewContext'> & { + embed: EmbedType<'post'> + viewContext?: QuoteEmbedViewContext +}) { + const moderationOpts = useModerationOpts() + const quote = React.useMemo<$Typed<AppBskyFeedDefs.PostView>>( + () => ({ + ...embed.view, + $type: 'app.bsky.feed.defs#postView', + record: embed.view.value, + embed: embed.view.embeds?.[0], + }), + [embed], + ) + const moderation = React.useMemo(() => { + return moderationOpts ? moderatePost(quote, moderationOpts) : undefined + }, [quote, moderationOpts]) + + 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<AppBskyFeedPost.Record>( + quote.record, + AppBskyFeedPost.isRecord, + ) + ) + return undefined + const {text, facets} = quote.record + return text.trim() + ? new RichTextAPI({text: text, facets: facets}) + : undefined + }, [quote.record]) + + const onBeforePress = React.useCallback(() => { + unstableCacheProfileView(queryClient, quote.author) + onOpen?.() + }, [queryClient, quote.author, onOpen]) + + const [hover, setHover] = React.useState(false) + return ( + <View + onPointerEnter={() => { + setHover(true) + }} + onPointerLeave={() => { + setHover(false) + }}> + <ContentHider + modui={moderation?.ui('contentList')} + style={[ + a.rounded_md, + a.p_md, + a.mt_sm, + a.border, + t.atoms.border_contrast_low, + style, + ]} + childContainerStyle={[a.pt_sm]}> + <SubtleWebHover hover={hover} /> + <Link + hoverStyle={{borderColor: pal.colors.borderLinkHover}} + href={itemHref} + title={itemTitle} + onBeforePress={onBeforePress}> + <View pointerEvents="none"> + <PostMeta + author={quote.author} + moderation={moderation} + showAvatar + postHref={itemHref} + timestamp={quote.indexedAt} + /> + </View> + {moderation ? ( + <PostAlerts + modui={moderation.ui('contentView')} + style={[a.py_xs]} + /> + ) : null} + {richText ? ( + <RichText + value={richText} + style={a.text_md} + numberOfLines={20} + disableLinks + /> + ) : null} + {quote.embed && ( + <Embed + embed={quote.embed} + moderation={moderation} + isWithinQuote={parentIsWithinQuote ?? true} + // already within quote? override nested + allowNestedQuotes={ + parentIsWithinQuote ? false : parentAllowNestedQuotes + } + /> + )} + </Link> + </ContentHider> + </View> + ) +} diff --git a/src/components/Post/Embed/types.ts b/src/components/Post/Embed/types.ts new file mode 100644 index 000000000..b719d00b4 --- /dev/null +++ b/src/components/Post/Embed/types.ts @@ -0,0 +1,25 @@ +import {type StyleProp, type ViewStyle} from 'react-native' +import {type AppBskyFeedDefs, type ModerationDecision} from '@atproto/api' + +export enum PostEmbedViewContext { + ThreadHighlighted = 'ThreadHighlighted', + Feed = 'Feed', + FeedEmbedRecordWithMedia = 'FeedEmbedRecordWithMedia', +} + +export enum QuoteEmbedViewContext { + FeedEmbedRecordWithMedia = PostEmbedViewContext.FeedEmbedRecordWithMedia, +} + +export type CommonProps = { + moderation?: ModerationDecision + onOpen?: () => void + style?: StyleProp<ViewStyle> + viewContext?: PostEmbedViewContext + isWithinQuote?: boolean + allowNestedQuotes?: boolean +} + +export type EmbedProps = CommonProps & { + embed?: AppBskyFeedDefs.PostView['embed'] +} diff --git a/src/components/dms/ActionsWrapper.tsx b/src/components/dms/ActionsWrapper.tsx index 120a5f8ad..eb9f0a09a 100644 --- a/src/components/dms/ActionsWrapper.tsx +++ b/src/components/dms/ActionsWrapper.tsx @@ -1,5 +1,5 @@ import {View} from 'react-native' -import {ChatBskyConvoDefs} from '@atproto/api' +import {type ChatBskyConvoDefs} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' diff --git a/src/components/dms/MessageItemEmbed.tsx b/src/components/dms/MessageItemEmbed.tsx index f1c6189d0..6390300c1 100644 --- a/src/components/dms/MessageItemEmbed.tsx +++ b/src/components/dms/MessageItemEmbed.tsx @@ -1,15 +1,16 @@ import React from 'react' import {useWindowDimensions, View} from 'react-native' -import {AppBskyEmbedRecord} from '@atproto/api' +import {type $Typed, type AppBskyEmbedRecord} from '@atproto/api' -import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds' import {atoms as a, native, tokens, useTheme, web} from '#/alf' +import {PostEmbedViewContext} from '#/components/Post/Embed' +import {Embed} from '#/components/Post/Embed' import {MessageContextProvider} from './MessageContext' let MessageItemEmbed = ({ embed, }: { - embed: AppBskyEmbedRecord.View + embed: $Typed<AppBskyEmbedRecord.View> }): React.ReactNode => { const t = useTheme() const screen = useWindowDimensions() @@ -32,7 +33,7 @@ let MessageItemEmbed = ({ }), ]}> <View style={{marginTop: tokens.space.sm * -1}}> - <PostEmbeds + <Embed embed={embed} allowNestedQuotes viewContext={PostEmbedViewContext.Feed} diff --git a/src/components/hooks/dates.ts b/src/components/hooks/dates.ts index b33a866d1..898e245ec 100644 --- a/src/components/hooks/dates.ts +++ b/src/components/hooks/dates.ts @@ -8,7 +8,7 @@ */ import React from 'react' -import {formatDistance, Locale} from 'date-fns' +import {formatDistance, type Locale} from 'date-fns' import { ca, cy, @@ -47,7 +47,7 @@ import { zhTW, } from 'date-fns/locale' -import {AppLanguage} from '#/locale/languages' +import {type AppLanguage} from '#/locale/languages' import {useLanguagePrefs} from '#/state/preferences' /** diff --git a/src/components/moderation/PostHider.tsx b/src/components/moderation/PostHider.tsx index 03a58ab0b..057c77023 100644 --- a/src/components/moderation/PostHider.tsx +++ b/src/components/moderation/PostHider.tsx @@ -1,6 +1,16 @@ -import React, {ComponentProps} from 'react' -import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native' -import {AppBskyActorDefs, ModerationCause, ModerationUI} from '@atproto/api' +import React, {type ComponentProps} from 'react' +import { + Pressable, + type StyleProp, + StyleSheet, + View, + type ViewStyle, +} from 'react-native' +import { + type AppBskyActorDefs, + type ModerationCause, + type ModerationUI, +} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useQueryClient} from '@tanstack/react-query' diff --git a/src/locale/helpers.ts b/src/locale/helpers.ts index 8d650a234..380f996b9 100644 --- a/src/locale/helpers.ts +++ b/src/locale/helpers.ts @@ -1,4 +1,4 @@ -import {AppBskyFeedDefs, AppBskyFeedPost} from '@atproto/api' +import {type AppBskyFeedDefs, AppBskyFeedPost} from '@atproto/api' import * as bcp47Match from 'bcp-47-match' import lande from 'lande' diff --git a/src/screens/Login/LoginForm.tsx b/src/screens/Login/LoginForm.tsx index 6c3d7633a..b6a528e42 100644 --- a/src/screens/Login/LoginForm.tsx +++ b/src/screens/Login/LoginForm.tsx @@ -3,12 +3,12 @@ import { ActivityIndicator, Keyboard, LayoutAnimation, - TextInput, + type TextInput, View, } from 'react-native' import { ComAtprotoServerCreateSession, - ComAtprotoServerDescribeServer, + type ComAtprotoServerDescribeServer, } from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' diff --git a/src/screens/Onboarding/StepProfile/index.tsx b/src/screens/Onboarding/StepProfile/index.tsx index 0e738f145..3d2c551e9 100644 --- a/src/screens/Onboarding/StepProfile/index.tsx +++ b/src/screens/Onboarding/StepProfile/index.tsx @@ -2,7 +2,7 @@ import React from 'react' import {View} from 'react-native' import {Image as ExpoImage} from 'expo-image' import { - ImagePickerOptions, + type ImagePickerOptions, launchImageLibraryAsync, MediaTypeOptions, } from 'expo-image-picker' @@ -27,7 +27,7 @@ import {AvatarCreatorCircle} from '#/screens/Onboarding/StepProfile/AvatarCreato import {AvatarCreatorItems} from '#/screens/Onboarding/StepProfile/AvatarCreatorItems' import { PlaceholderCanvas, - PlaceholderCanvasRef, + type PlaceholderCanvasRef, } from '#/screens/Onboarding/StepProfile/PlaceholderCanvas' import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' @@ -38,7 +38,7 @@ import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components import {CircleInfo_Stroke2_Corner0_Rounded} from '#/components/icons/CircleInfo' import {StreamingLive_Stroke2_Corner0_Rounded as StreamingLive} from '#/components/icons/StreamingLive' import {Text} from '#/components/Typography' -import {AvatarColor, avatarColors, Emoji, emojiItems} from './types' +import {type AvatarColor, avatarColors, type Emoji, emojiItems} from './types' export interface Avatar { image?: { diff --git a/src/screens/PostThread/components/ThreadItemAnchor.tsx b/src/screens/PostThread/components/ThreadItemAnchor.tsx index 0aacd4e77..d1e3518cc 100644 --- a/src/screens/PostThread/components/ThreadItemAnchor.tsx +++ b/src/screens/PostThread/components/ThreadItemAnchor.tsx @@ -36,7 +36,6 @@ import {type PostSource} from '#/state/unstable-post-source' import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn' import {Link} from '#/view/com/util/Link' import {formatCount} from '#/view/com/util/numeric/format' -import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' import { LINEAR_AVI_WIDTH, @@ -53,6 +52,7 @@ import {ContentHider} from '#/components/moderation/ContentHider' import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' import {PostAlerts} from '#/components/moderation/PostAlerts' import {type AppModerationCause} from '#/components/Pills' +import {Embed, PostEmbedViewContext} from '#/components/Post/Embed' import {PostControls} from '#/components/PostControls' import * as Prompt from '#/components/Prompt' import {RichText} from '#/components/RichText' @@ -388,7 +388,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({ ) : undefined} {post.embed && ( <View style={[a.py_xs]}> - <PostEmbeds + <Embed embed={post.embed} moderation={moderation} viewContext={PostEmbedViewContext.ThreadHighlighted} diff --git a/src/screens/PostThread/components/ThreadItemPost.tsx b/src/screens/PostThread/components/ThreadItemPost.tsx index 1f63b10cd..9393a6d1b 100644 --- a/src/screens/PostThread/components/ThreadItemPost.tsx +++ b/src/screens/PostThread/components/ThreadItemPost.tsx @@ -25,7 +25,6 @@ import {useSession} from '#/state/session' import {type OnPostSuccessData} from '#/state/shell/composer' import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' import {TextLink} from '#/view/com/util/Link' -import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds' import {PostMeta} from '#/view/com/util/PostMeta' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' import { @@ -40,6 +39,7 @@ import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' import {PostAlerts} from '#/components/moderation/PostAlerts' import {PostHider} from '#/components/moderation/PostHider' import {type AppModerationCause} from '#/components/Pills' +import {Embed, PostEmbedViewContext} from '#/components/Post/Embed' import {PostControls} from '#/components/PostControls' import {RichText} from '#/components/RichText' import * as Skele from '#/components/Skeleton' @@ -323,7 +323,7 @@ const ThreadItemPostInner = memo(function ThreadItemPostInner({ ) : undefined} {post.embed && ( <View style={[a.pb_xs]}> - <PostEmbeds + <Embed embed={post.embed} moderation={moderation} viewContext={PostEmbedViewContext.Feed} diff --git a/src/screens/PostThread/components/ThreadItemTreePost.tsx b/src/screens/PostThread/components/ThreadItemTreePost.tsx index d86d2ef6f..ac659a6e0 100644 --- a/src/screens/PostThread/components/ThreadItemTreePost.tsx +++ b/src/screens/PostThread/components/ThreadItemTreePost.tsx @@ -24,7 +24,6 @@ import {useSession} from '#/state/session' import {type OnPostSuccessData} from '#/state/shell/composer' import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' import {TextLink} from '#/view/com/util/Link' -import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds' import {PostMeta} from '#/view/com/util/PostMeta' import { OUTER_SPACE, @@ -39,6 +38,7 @@ import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' import {PostAlerts} from '#/components/moderation/PostAlerts' import {PostHider} from '#/components/moderation/PostHider' import {type AppModerationCause} from '#/components/Pills' +import {Embed, PostEmbedViewContext} from '#/components/Post/Embed' import {PostControls} from '#/components/PostControls' import {RichText} from '#/components/RichText' import * as Skele from '#/components/Skeleton' @@ -369,7 +369,7 @@ const ThreadItemTreePostInner = memo(function ThreadItemTreePostInner({ ) : undefined} {post.embed && ( <View style={[a.pb_xs]}> - <PostEmbeds + <Embed embed={post.embed} moderation={moderation} viewContext={PostEmbedViewContext.Feed} diff --git a/src/screens/Profile/Header/Handle.tsx b/src/screens/Profile/Header/Handle.tsx index a8bf65692..cfbf430c4 100644 --- a/src/screens/Profile/Header/Handle.tsx +++ b/src/screens/Profile/Header/Handle.tsx @@ -1,11 +1,11 @@ import {View} from 'react-native' -import {AppBskyActorDefs} from '@atproto/api' +import {type AppBskyActorDefs} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {isInvalidHandle, sanitizeHandle} from '#/lib/strings/handles' import {isIOS} from '#/platform/detection' -import {Shadow} from '#/state/cache/types' +import {type Shadow} from '#/state/cache/types' import {atoms as a, useTheme, web} from '#/alf' import {NewskieDialog} from '#/components/NewskieDialog' import {Text} from '#/components/Typography' diff --git a/src/screens/Signup/StepInfo/Policies.tsx b/src/screens/Signup/StepInfo/Policies.tsx index 81533c58e..17980172d 100644 --- a/src/screens/Signup/StepInfo/Policies.tsx +++ b/src/screens/Signup/StepInfo/Policies.tsx @@ -1,6 +1,6 @@ -import {ReactElement} from 'react' +import {type ReactElement} from 'react' import {View} from 'react-native' -import {ComAtprotoServerDescribeServer} from '@atproto/api' +import {type ComAtprotoServerDescribeServer} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' diff --git a/src/screens/StarterPack/StarterPackLandingScreen.tsx b/src/screens/StarterPack/StarterPackLandingScreen.tsx index b522bc906..39ae57855 100644 --- a/src/screens/StarterPack/StarterPackLandingScreen.tsx +++ b/src/screens/StarterPack/StarterPackLandingScreen.tsx @@ -5,7 +5,7 @@ import { AppBskyGraphDefs, AppBskyGraphStarterpack, AtUri, - ModerationOpts, + type ModerationOpts, } from '@atproto/api' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {msg, Trans} from '@lingui/macro' diff --git a/src/screens/StarterPack/Wizard/State.tsx b/src/screens/StarterPack/Wizard/State.tsx index 1ecd038a4..07d744c06 100644 --- a/src/screens/StarterPack/Wizard/State.tsx +++ b/src/screens/StarterPack/Wizard/State.tsx @@ -1,6 +1,6 @@ import React from 'react' -import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api' -import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs' +import {type AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api' +import {type GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs' import {msg, plural} from '@lingui/macro' import {STARTER_PACK_MAX_SIZE} from '#/lib/constants' diff --git a/src/screens/Takendown.tsx b/src/screens/Takendown.tsx index ef3e93658..d01903eb5 100644 --- a/src/screens/Takendown.tsx +++ b/src/screens/Takendown.tsx @@ -3,7 +3,7 @@ import {Modal, View} from 'react-native' import {SystemBars} from 'react-native-edge-to-edge' import {KeyboardAwareScrollView} from 'react-native-keyboard-controller' import {useSafeAreaInsets} from 'react-native-safe-area-context' -import {ComAtprotoAdminDefs, ComAtprotoModerationDefs} from '@atproto/api' +import {type ComAtprotoAdminDefs, ComAtprotoModerationDefs} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useMutation} from '@tanstack/react-query' diff --git a/src/screens/VideoFeed/components/Scrubber.tsx b/src/screens/VideoFeed/components/Scrubber.tsx index ef3190526..29cc4b278 100644 --- a/src/screens/VideoFeed/components/Scrubber.tsx +++ b/src/screens/VideoFeed/components/Scrubber.tsx @@ -22,9 +22,9 @@ import { import {useEventListener} from 'expo' import {VideoPlayer} from 'expo-video' -import {formatTime} from '#/view/com/util/post-embeds/VideoEmbedInner/web-controls/utils' import {tokens} from '#/alf' import {atoms as a} from '#/alf' +import {formatTime} from '#/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/utils' import {Text} from '#/components/Typography' // magic number that is roughly the min height of the write reply button diff --git a/src/state/messages/events/agent.ts b/src/state/messages/events/agent.ts index 1a6cfb3f2..589c5b6d3 100644 --- a/src/state/messages/events/agent.ts +++ b/src/state/messages/events/agent.ts @@ -1,4 +1,4 @@ -import {BskyAgent, ChatBskyConvoGetLog} from '@atproto/api' +import {type BskyAgent, type ChatBskyConvoGetLog} from '@atproto/api' import EventEmitter from 'eventemitter3' import {nanoid} from 'nanoid/non-secure' @@ -9,11 +9,11 @@ import { DEFAULT_POLL_INTERVAL, } from '#/state/messages/events/const' import { - MessagesEventBusDispatch, + type MessagesEventBusDispatch, MessagesEventBusDispatchEvent, MessagesEventBusErrorCode, - MessagesEventBusEvent, - MessagesEventBusParams, + type MessagesEventBusEvent, + type MessagesEventBusParams, MessagesEventBusStatus, } from '#/state/messages/events/types' import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const' diff --git a/src/state/queries/postgate/util.ts b/src/state/queries/postgate/util.ts index c1955cc74..0952a1ad0 100644 --- a/src/state/queries/postgate/util.ts +++ b/src/state/queries/postgate/util.ts @@ -1,9 +1,9 @@ import { - $Typed, + type $Typed, AppBskyEmbedRecord, AppBskyEmbedRecordWithMedia, - AppBskyFeedDefs, - AppBskyFeedPostgate, + type AppBskyFeedDefs, + type AppBskyFeedPostgate, AtUri, } from '@atproto/api' @@ -113,6 +113,7 @@ export function createEmbedViewRecordFromPost( likeCount: post.likeCount, quoteCount: post.quoteCount, indexedAt: post.indexedAt, + embeds: post.embed ? [post.embed] : [], } } diff --git a/src/state/session/types.ts b/src/state/session/types.ts index 9aadf9d05..aa8b9a99e 100644 --- a/src/state/session/types.ts +++ b/src/state/session/types.ts @@ -1,5 +1,5 @@ -import {LogEvents} from '#/lib/statsig/statsig' -import {PersistedAccount} from '#/state/persisted' +import {type LogEvents} from '#/lib/statsig/statsig' +import {type PersistedAccount} from '#/state/persisted' export type SessionAccount = PersistedAccount diff --git a/src/state/threadgate-hidden-replies.tsx b/src/state/threadgate-hidden-replies.tsx index 9d116c7f9..8a3ee0f24 100644 --- a/src/state/threadgate-hidden-replies.tsx +++ b/src/state/threadgate-hidden-replies.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {AppBskyFeedThreadgate} from '@atproto/api' +import {type AppBskyFeedThreadgate} from '@atproto/api' type StateContext = { uris: Set<string> diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 17d0f94f7..de060c6c2 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -72,7 +72,7 @@ import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {mimeToExt} from '#/lib/media/video/util' import {logEvent} from '#/lib/statsig/statsig' import {cleanError} from '#/lib/strings/errors' -import {colors, s} from '#/lib/styles' +import {colors} from '#/lib/styles' import {logger} from '#/logger' import {isAndroid, isIOS, isNative, isWeb} from '#/platform/detection' import {useDialogStateControlContext} from '#/state/dialogs' @@ -97,6 +97,7 @@ import { ExternalEmbedGif, ExternalEmbedLink, } from '#/view/com/composer/ExternalEmbed' +import {ExternalEmbedRemoveBtn} from '#/view/com/composer/ExternalEmbedRemoveBtn' import {GifAltTextDialog} from '#/view/com/composer/GifAltText' import {LabelsBtn} from '#/view/com/composer/labels/LabelsBtn' import {Gallery} from '#/view/com/composer/photos/Gallery' @@ -116,7 +117,6 @@ import {SelectVideoBtn} from '#/view/com/composer/videos/SelectVideoBtn' import {SubtitleDialogBtn} from '#/view/com/composer/videos/SubtitleDialog' import {VideoPreview} from '#/view/com/composer/videos/VideoPreview' import {VideoTranscodeProgress} from '#/view/com/composer/videos/VideoTranscodeProgress' -import {LazyQuoteEmbed, QuoteX} from '#/view/com/util/post-embeds/QuoteEmbed' import {Text} from '#/view/com/util/text/Text' import * as Toast from '#/view/com/util/Toast' import {UserAvatar} from '#/view/com/util/UserAvatar' @@ -125,6 +125,7 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji' import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' +import {LazyQuoteEmbed} from '#/components/Post/Embed/LazyQuoteEmbed' import * as Prompt from '#/components/Prompt' import {Text as NewText} from '#/components/Typography' import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet' @@ -1149,13 +1150,17 @@ function ComposerEmbeds({ )} </LayoutAnimationConfig> {embed.quote?.uri ? ( - <View style={!video ? [a.mt_md] : []}> - <View style={[s.mt5, s.mb2, isWeb && s.mb10]}> + <View + style={[a.pb_sm, video ? [a.pt_md] : [a.pt_xl], isWeb && [a.pb_md]]}> + <View style={[a.relative]}> <View style={{pointerEvents: 'none'}}> <LazyQuoteEmbed uri={embed.quote.uri} /> </View> {canRemoveQuote && ( - <QuoteX onRemove={() => dispatch({type: 'embed_remove_quote'})} /> + <ExternalEmbedRemoveBtn + onRemove={() => dispatch({type: 'embed_remove_quote'})} + style={{top: 16}} + /> )} </View> </View> diff --git a/src/view/com/composer/ComposerReplyTo.tsx b/src/view/com/composer/ComposerReplyTo.tsx index 0ced14359..acab84f65 100644 --- a/src/view/com/composer/ComposerReplyTo.tsx +++ b/src/view/com/composer/ComposerReplyTo.tsx @@ -13,12 +13,13 @@ import {useLingui} from '@lingui/react' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' import {type ComposerOptsPostRef} from '#/state/shell/composer' -import {MaybeQuoteEmbed} from '#/view/com/util/post-embeds/QuoteEmbed' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, useTheme, web} from '#/alf' +import {QuoteEmbed} from '#/components/Post/Embed' import {Text} from '#/components/Typography' import {useSimpleVerificationState} from '#/components/verification' import {VerificationCheck} from '#/components/verification/VerificationCheck' +import {parseEmbed} from '#/types/bsky/post' export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) { const t = useTheme() @@ -51,6 +52,12 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) { } return null }, [embed]) + const parsedQuoteEmbed = quoteEmbed + ? parseEmbed({ + $type: 'app.bsky.embed.record#view', + ...quoteEmbed, + }) + : null const images = useMemo(() => { if (AppBskyEmbedImages.isView(embed)) { @@ -124,7 +131,9 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) { <ComposerReplyToImages images={images} showFull={showFull} /> )} </View> - {showFull && quoteEmbed && <MaybeQuoteEmbed embed={quoteEmbed} />} + {showFull && parsedQuoteEmbed && parsedQuoteEmbed.type === 'post' && ( + <QuoteEmbed embed={parsedQuoteEmbed} /> + )} </View> </Pressable> ) diff --git a/src/view/com/composer/ExternalEmbed.tsx b/src/view/com/composer/ExternalEmbed.tsx index d819b28b7..e4bdabac3 100644 --- a/src/view/com/composer/ExternalEmbed.tsx +++ b/src/view/com/composer/ExternalEmbed.tsx @@ -1,19 +1,20 @@ import React from 'react' -import {StyleProp, View, ViewStyle} from 'react-native' +import {type StyleProp, View, type ViewStyle} from 'react-native' import {cleanError} from '#/lib/strings/errors' import { useResolveGifQuery, useResolveLinkQuery, } from '#/state/queries/resolve-link' -import {Gif} from '#/state/queries/tenor' +import {type Gif} from '#/state/queries/tenor' import {ExternalEmbedRemoveBtn} from '#/view/com/composer/ExternalEmbedRemoveBtn' -import {ExternalLinkEmbed} from '#/view/com/util/post-embeds/ExternalLinkEmbed' import {atoms as a, useTheme} from '#/alf' import {Loader} from '#/components/Loader' +import {ExternalEmbed} from '#/components/Post/Embed/ExternalEmbed' +import {ModeratedFeedEmbed} from '#/components/Post/Embed/FeedEmbed' +import {ModeratedListEmbed} from '#/components/Post/Embed/ListEmbed' import {Embed as StarterPackEmbed} from '#/components/StarterPack/StarterPackCard' import {Text} from '#/components/Typography' -import {MaybeFeedCard, MaybeListCard} from '../util/post-embeds' export const ExternalEmbedGif = ({ onRemove, @@ -44,7 +45,7 @@ export const ExternalEmbedGif = ({ <View style={[a.overflow_hidden, t.atoms.border_contrast_medium]}> {linkInfo ? ( <View style={{pointerEvents: 'auto'}}> - <ExternalLinkEmbed link={linkInfo} hideAlt /> + <ExternalEmbed link={linkInfo} hideAlt /> </View> ) : error ? ( <Container style={[a.align_start, a.p_md, a.gap_xs]}> @@ -80,7 +81,7 @@ export const ExternalEmbedLink = ({ if (data) { if (data.type === 'external') { return ( - <ExternalLinkEmbed + <ExternalEmbed link={{ title: data.title || uri, uri, @@ -91,9 +92,29 @@ export const ExternalEmbedLink = ({ /> ) } else if (data.kind === 'feed') { - return <MaybeFeedCard view={data.view} /> + return ( + <ModeratedFeedEmbed + embed={{ + type: 'feed', + view: { + $type: 'app.bsky.feed.defs#generatorView', + ...data.view, + }, + }} + /> + ) } else if (data.kind === 'list') { - return <MaybeListCard view={data.view} /> + return ( + <ModeratedListEmbed + embed={{ + type: 'list', + view: { + $type: 'app.bsky.graph.defs#listView', + ...data.view, + }, + }} + /> + ) } else if (data.kind === 'starter-pack') { return <StarterPackEmbed starterPack={data.view} /> } diff --git a/src/view/com/composer/ExternalEmbedRemoveBtn.tsx b/src/view/com/composer/ExternalEmbedRemoveBtn.tsx index 92102f847..1e363d018 100644 --- a/src/view/com/composer/ExternalEmbedRemoveBtn.tsx +++ b/src/view/com/composer/ExternalEmbedRemoveBtn.tsx @@ -2,22 +2,27 @@ import {View} from 'react-native' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {atoms as a} from '#/alf' +import {atoms as a, useTheme, type ViewStyleProp} from '#/alf' import {Button, ButtonIcon} from '#/components/Button' import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' -export function ExternalEmbedRemoveBtn({onRemove}: {onRemove: () => void}) { +export function ExternalEmbedRemoveBtn({ + onRemove, + style, +}: {onRemove: () => void} & ViewStyleProp) { + const t = useTheme() const {_} = useLingui() return ( - <View style={[a.absolute, {top: 8, right: 8}, a.z_50]}> + <View style={[a.absolute, {top: 8, right: 8}, a.z_50, style]}> <Button label={_(msg`Remove attachment`)} onPress={onRemove} size="small" variant="solid" color="secondary" - shape="round"> + shape="round" + style={[t.atoms.shadow_sm]}> <ButtonIcon icon={X} size="sm" /> </Button> </View> diff --git a/src/view/com/composer/GifAltText.tsx b/src/view/com/composer/GifAltText.tsx index 4d2539c4e..ceee17eaa 100644 --- a/src/view/com/composer/GifAltText.tsx +++ b/src/view/com/composer/GifAltText.tsx @@ -6,23 +6,23 @@ import {useLingui} from '@lingui/react' import {HITSLOP_10, MAX_ALT_TEXT} from '#/lib/constants' import {parseAltFromGIFDescription} from '#/lib/gif-alt-text' import { - EmbedPlayerParams, + type EmbedPlayerParams, parseEmbedPlayerFromUrl, } from '#/lib/strings/embed-player' import {isAndroid} from '#/platform/detection' import {useResolveGifQuery} from '#/state/queries/resolve-link' -import {Gif} from '#/state/queries/tenor' +import {type Gif} from '#/state/queries/tenor' import {AltTextCounterWrapper} from '#/view/com/composer/AltTextCounterWrapper' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' -import {DialogControlProps} from '#/components/Dialog' +import {type DialogControlProps} from '#/components/Dialog' import * as TextField from '#/components/forms/TextField' import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' +import {GifEmbed} from '#/components/Post/Embed/ExternalEmbed/Gif' import {Text} from '#/components/Typography' -import {GifEmbed} from '../util/post-embeds/GifEmbed' import {AltTextReminder} from './photos/Gallery' export function GifAltTextDialog({ diff --git a/src/view/com/composer/labels/LabelsBtn.tsx b/src/view/com/composer/labels/LabelsBtn.tsx index 9548ed065..902d89b7b 100644 --- a/src/view/com/composer/labels/LabelsBtn.tsx +++ b/src/view/com/composer/labels/LabelsBtn.tsx @@ -4,10 +4,10 @@ import {useLingui} from '@lingui/react' import { ADULT_CONTENT_LABELS, - AdultSelfLabel, + type AdultSelfLabel, OTHER_SELF_LABELS, - OtherSelfLabel, - SelfLabel, + type OtherSelfLabel, + type SelfLabel, } from '#/lib/moderation' import {isWeb} from '#/platform/detection' import {atoms as a, native, useTheme, web} from '#/alf' diff --git a/src/view/com/composer/photos/ImageAltTextDialog.tsx b/src/view/com/composer/photos/ImageAltTextDialog.tsx index c0ce32af3..724149937 100644 --- a/src/view/com/composer/photos/ImageAltTextDialog.tsx +++ b/src/view/com/composer/photos/ImageAltTextDialog.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {ImageStyle, useWindowDimensions, View} from 'react-native' +import {type ImageStyle, useWindowDimensions, View} from 'react-native' import {Image} from 'expo-image' import {msg, Plural, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -7,12 +7,12 @@ import {useLingui} from '@lingui/react' import {MAX_ALT_TEXT} from '#/lib/constants' import {enforceLen} from '#/lib/strings/helpers' import {isAndroid, isWeb} from '#/platform/detection' -import {ComposerImage} from '#/state/gallery' +import {type ComposerImage} from '#/state/gallery' import {AltTextCounterWrapper} from '#/view/com/composer/AltTextCounterWrapper' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' -import {DialogControlProps} from '#/components/Dialog' +import {type DialogControlProps} from '#/components/Dialog' import * as TextField from '#/components/forms/TextField' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {Text} from '#/components/Typography' diff --git a/src/view/com/composer/photos/OpenCameraBtn.tsx b/src/view/com/composer/photos/OpenCameraBtn.tsx index 1c9440eb1..8bd1aa27b 100644 --- a/src/view/com/composer/photos/OpenCameraBtn.tsx +++ b/src/view/com/composer/photos/OpenCameraBtn.tsx @@ -8,7 +8,7 @@ import {useCameraPermission} from '#/lib/hooks/usePermissions' import {openCamera} from '#/lib/media/picker' import {logger} from '#/logger' import {isMobileWeb, isNative} from '#/platform/detection' -import {ComposerImage, createComposerImage} from '#/state/gallery' +import {type ComposerImage, createComposerImage} from '#/state/gallery' import {atoms as a, useTheme} from '#/alf' import {Button} from '#/components/Button' import {Camera_Stroke2_Corner0_Rounded as Camera} from '#/components/icons/Camera' diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 5184047cb..15f5539c9 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -46,7 +46,6 @@ import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn' import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' import {Link, TextLink} from '#/view/com/util/Link' import {formatCount} from '#/view/com/util/numeric/format' -import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds' import {PostMeta} from '#/view/com/util/PostMeta' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, useTheme} from '#/alf' @@ -62,6 +61,7 @@ import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' import {PostAlerts} from '#/components/moderation/PostAlerts' import {PostHider} from '#/components/moderation/PostHider' import {type AppModerationCause} from '#/components/Pills' +import {Embed, PostEmbedViewContext} from '#/components/Post/Embed' import {PostControls} from '#/components/PostControls' import * as Prompt from '#/components/Prompt' import {RichText} from '#/components/RichText' @@ -465,7 +465,7 @@ let PostThreadItemLoaded = ({ ) : undefined} {post.embed && ( <View style={[a.py_xs]}> - <PostEmbeds + <Embed embed={post.embed} moderation={moderation} viewContext={PostEmbedViewContext.ThreadHighlighted} @@ -697,7 +697,7 @@ let PostThreadItemLoaded = ({ ) : undefined} {post.embed && ( <View style={[a.pb_xs]}> - <PostEmbeds + <Embed embed={post.embed} moderation={moderation} viewContext={PostEmbedViewContext.Feed} diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index 1a48d64d8..d92ea6a9d 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -28,7 +28,6 @@ import {useModerationOpts} from '#/state/preferences/moderation-opts' import {precacheProfile} from '#/state/queries/profile' import {useSession} from '#/state/session' import {Link, TextLink} from '#/view/com/util/Link' -import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds' import {PostMeta} from '#/view/com/util/PostMeta' import {Text} from '#/view/com/util/text/Text' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' @@ -37,6 +36,7 @@ import {atoms as a} from '#/alf' import {ContentHider} from '#/components/moderation/ContentHider' import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' import {PostAlerts} from '#/components/moderation/PostAlerts' +import {Embed, PostEmbedViewContext} from '#/components/Post/Embed' import {PostControls} from '#/components/PostControls' import {ProfileHoverCard} from '#/components/ProfileHoverCard' import {RichText} from '#/components/RichText' @@ -248,7 +248,7 @@ function PostInner({ /> ) : undefined} {post.embed ? ( - <PostEmbeds + <Embed embed={post.embed} moderation={moderation} viewContext={PostEmbedViewContext.Feed} diff --git a/src/view/com/posts/PostFeedItem.tsx b/src/view/com/posts/PostFeedItem.tsx index fd0d1c707..a5a7a777e 100644 --- a/src/view/com/posts/PostFeedItem.tsx +++ b/src/view/com/posts/PostFeedItem.tsx @@ -42,7 +42,6 @@ import { } from '#/state/unstable-post-source' import {FeedNameText} from '#/view/com/util/FeedInfoText' import {Link, TextLink, TextLinkOnWebOnly} from '#/view/com/util/Link' -import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds' import {PostMeta} from '#/view/com/util/PostMeta' import {Text} from '#/view/com/util/text/Text' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' @@ -53,6 +52,8 @@ import {ContentHider} from '#/components/moderation/ContentHider' import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' import {PostAlerts} from '#/components/moderation/PostAlerts' import {type AppModerationCause} from '#/components/Pills' +import {Embed} from '#/components/Post/Embed' +import {PostEmbedViewContext} from '#/components/Post/Embed/types' import {PostControls} from '#/components/PostControls' import {DiscoverDebug} from '#/components/PostControls/DiscoverDebug' import {ProfileHoverCard} from '#/components/ProfileHoverCard' @@ -568,7 +569,7 @@ let PostContent = ({ ) : undefined} {postEmbed ? ( <View style={[a.pb_xs]}> - <PostEmbeds + <Embed embed={postEmbed} moderation={moderation} onOpen={onOpenEmbed} diff --git a/src/view/com/util/Views.web.tsx b/src/view/com/util/Views.web.tsx index 6a11c7eaa..9a3e8a4ae 100644 --- a/src/view/com/util/Views.web.tsx +++ b/src/view/com/util/Views.web.tsx @@ -14,12 +14,12 @@ import React from 'react' import { - FlatList, - FlatListProps, - ScrollViewProps, + type FlatList, + type FlatListProps, + type ScrollViewProps, StyleSheet, View, - ViewProps, + type ViewProps, } from 'react-native' import Animated from 'react-native-reanimated' diff --git a/src/view/com/util/images/Gallery.tsx b/src/view/com/util/images/Gallery.tsx index 1d35c88c5..323264ea4 100644 --- a/src/view/com/util/images/Gallery.tsx +++ b/src/view/com/util/images/Gallery.tsx @@ -8,9 +8,9 @@ import type React from 'react' import {type Dimensions} from '#/lib/media/types' import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' -import {PostEmbedViewContext} from '#/view/com/util/post-embeds/types' import {atoms as a, useTheme} from '#/alf' import {MediaInsetBorder} from '#/components/MediaInsetBorder' +import {PostEmbedViewContext} from '#/components/Post/Embed/types' import {Text} from '#/components/Typography' type EventFunction = (index: number) => void diff --git a/src/view/com/util/images/ImageLayoutGrid.tsx b/src/view/com/util/images/ImageLayoutGrid.tsx index b91d7a7ad..757d952a1 100644 --- a/src/view/com/util/images/ImageLayoutGrid.tsx +++ b/src/view/com/util/images/ImageLayoutGrid.tsx @@ -3,8 +3,8 @@ import {type StyleProp, StyleSheet, View, type ViewStyle} from 'react-native' import {type AnimatedRef, useAnimatedRef} from 'react-native-reanimated' import {type AppBskyEmbedImages} from '@atproto/api' -import {PostEmbedViewContext} from '#/view/com/util/post-embeds/types' import {atoms as a, useBreakpoints} from '#/alf' +import {PostEmbedViewContext} from '#/components/Post/Embed/types' import {type Dimensions} from '../../lightbox/ImageViewing/@types' import {GalleryItem} from './Gallery' diff --git a/src/view/com/util/post-embeds/QuoteEmbed.tsx b/src/view/com/util/post-embeds/QuoteEmbed.tsx deleted file mode 100644 index f788af1f8..000000000 --- a/src/view/com/util/post-embeds/QuoteEmbed.tsx +++ /dev/null @@ -1,337 +0,0 @@ -import React from 'react' -import { - StyleProp, - StyleSheet, - TouchableOpacity, - View, - ViewStyle, -} from 'react-native' -import { - AppBskyEmbedExternal, - AppBskyEmbedImages, - AppBskyEmbedRecord, - AppBskyEmbedRecordWithMedia, - AppBskyEmbedVideo, - AppBskyFeedDefs, - AppBskyFeedPost, - moderatePost, - 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 {useModerationOpts} from '#/state/preferences/moderation-opts' -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 {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 {QuoteEmbedViewContext} from './types' - -export function MaybeQuoteEmbed({ - embed, - onOpen, - style, - allowNestedQuotes, - viewContext, -}: { - embed: AppBskyEmbedRecord.View - onOpen?: () => void - style?: StyleProp<ViewStyle> - allowNestedQuotes?: boolean - viewContext?: QuoteEmbedViewContext -}) { - const t = useTheme() - const pal = usePalette('default') - const {currentAccount} = useSession() - if ( - AppBskyEmbedRecord.isViewRecord(embed.record) && - AppBskyFeedPost.isRecord(embed.record.value) && - AppBskyFeedPost.validateRecord(embed.record.value).success - ) { - return ( - <QuoteEmbedModerated - viewRecord={embed.record} - onOpen={onOpen} - style={style} - allowNestedQuotes={allowNestedQuotes} - viewContext={viewContext} - /> - ) - } else if (AppBskyEmbedRecord.isViewBlocked(embed.record)) { - return ( - <View - style={[styles.errorContainer, a.border, t.atoms.border_contrast_low]}> - <InfoCircleIcon size={18} style={pal.text} /> - <Text type="lg" style={pal.text}> - <Trans>Blocked</Trans> - </Text> - </View> - ) - } else if (AppBskyEmbedRecord.isViewNotFound(embed.record)) { - return ( - <View - style={[styles.errorContainer, a.border, t.atoms.border_contrast_low]}> - <InfoCircleIcon size={18} style={pal.text} /> - <Text type="lg" style={pal.text}> - <Trans>Deleted</Trans> - </Text> - </View> - ) - } else if (AppBskyEmbedRecord.isViewDetached(embed.record)) { - const isViewerOwner = currentAccount?.did - ? embed.record.uri.includes(currentAccount.did) - : false - return ( - <View - style={[styles.errorContainer, a.border, t.atoms.border_contrast_low]}> - <InfoCircleIcon size={18} style={pal.text} /> - <Text type="lg" style={pal.text}> - {isViewerOwner ? ( - <Trans>Removed by you</Trans> - ) : ( - <Trans>Removed by author</Trans> - )} - </Text> - </View> - ) - } - return null -} - -function QuoteEmbedModerated({ - viewRecord, - onOpen, - style, - allowNestedQuotes, - viewContext, -}: { - viewRecord: AppBskyEmbedRecord.ViewRecord - onOpen?: () => void - style?: StyleProp<ViewStyle> - allowNestedQuotes?: boolean - viewContext?: QuoteEmbedViewContext -}) { - const moderationOpts = useModerationOpts() - const postView = React.useMemo( - () => viewRecordToPostView(viewRecord), - [viewRecord], - ) - const moderation = React.useMemo(() => { - return moderationOpts ? moderatePost(postView, moderationOpts) : undefined - }, [postView, moderationOpts]) - - return ( - <QuoteEmbed - quote={postView} - moderation={moderation} - onOpen={onOpen} - style={style} - allowNestedQuotes={allowNestedQuotes} - viewContext={viewContext} - /> - ) -} - -export function QuoteEmbed({ - quote, - moderation, - onOpen, - style, - allowNestedQuotes, -}: { - quote: AppBskyFeedDefs.PostView - moderation?: ModerationDecision - onOpen?: () => void - style?: StyleProp<ViewStyle> - allowNestedQuotes?: boolean - viewContext?: QuoteEmbedViewContext -}) { - 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<AppBskyFeedPost.Record>( - 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 ( - <View - onPointerEnter={() => { - setHover(true) - }} - onPointerLeave={() => { - setHover(false) - }}> - <ContentHider - modui={moderation?.ui('contentList')} - style={[ - a.rounded_md, - a.p_md, - a.mt_sm, - a.border, - t.atoms.border_contrast_low, - style, - ]} - childContainerStyle={[a.pt_sm]}> - <SubtleWebHover hover={hover} /> - <Link - hoverStyle={{borderColor: pal.colors.borderLinkHover}} - href={itemHref} - title={itemTitle} - onBeforePress={onBeforePress}> - <View pointerEvents="none"> - <PostMeta - author={quote.author} - moderation={moderation} - showAvatar - postHref={itemHref} - timestamp={quote.indexedAt} - /> - </View> - {moderation ? ( - <PostAlerts - modui={moderation.ui('contentView')} - style={[a.py_xs]} - /> - ) : null} - {richText ? ( - <RichText - value={richText} - style={a.text_md} - numberOfLines={20} - disableLinks - /> - ) : null} - {embed && <PostEmbeds embed={embed} moderation={moderation} />} - </Link> - </ContentHider> - </View> - ) -} - -export function QuoteX({onRemove}: {onRemove: () => void}) { - const {_} = useLingui() - return ( - <TouchableOpacity - style={[ - a.absolute, - a.p_xs, - a.rounded_full, - a.align_center, - a.justify_center, - { - top: 16, - right: 10, - backgroundColor: 'rgba(0, 0, 0, 0.75)', - }, - ]} - onPress={onRemove} - accessibilityRole="button" - accessibilityLabel={_(msg`Remove quote`)} - accessibilityHint={_(msg`Removes quoted post`)} - onAccessibilityEscape={onRemove} - hitSlop={HITSLOP_20}> - <FontAwesomeIcon size={12} icon="xmark" style={s.white} /> - </TouchableOpacity> - ) -} - -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 <QuoteEmbed quote={data.view} moderation={moderation} /> -} - -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, - }, -}) diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx deleted file mode 100644 index 4cf71f948..000000000 --- a/src/view/com/util/post-embeds/index.tsx +++ /dev/null @@ -1,327 +0,0 @@ -import React from 'react' -import { - InteractionManager, - type StyleProp, - StyleSheet, - View, - type ViewStyle, -} from 'react-native' -import { - type AnimatedRef, - measure, - type MeasuredDimensions, - runOnJS, - runOnUI, -} from 'react-native-reanimated' -import {Image} from 'expo-image' -import { - AppBskyEmbedExternal, - AppBskyEmbedImages, - AppBskyEmbedRecord, - AppBskyEmbedRecordWithMedia, - AppBskyEmbedVideo, - AppBskyFeedDefs, - AppBskyGraphDefs, - moderateFeedGenerator, - moderateUserList, - type ModerationDecision, -} from '@atproto/api' - -import {usePalette} from '#/lib/hooks/usePalette' -import {useLightboxControls} from '#/state/lightbox' -import {useModerationOpts} from '#/state/preferences/moderation-opts' -import {FeedSourceCard} from '#/view/com/feeds/FeedSourceCard' -import {atoms as a, useTheme} from '#/alf' -import * as ListCard from '#/components/ListCard' -import {Embed as StarterPackCard} from '#/components/StarterPack/StarterPackCard' -import {ContentHider} from '../../../../components/moderation/ContentHider' -import {type Dimensions} from '../../lightbox/ImageViewing/@types' -import {AutoSizedImage} from '../images/AutoSizedImage' -import {ImageLayoutGrid} from '../images/ImageLayoutGrid' -import {ExternalLinkEmbed} from './ExternalLinkEmbed' -import {MaybeQuoteEmbed} from './QuoteEmbed' -import {PostEmbedViewContext, QuoteEmbedViewContext} from './types' -import {VideoEmbed} from './VideoEmbed' - -export * from './types' - -type Embed = - | AppBskyEmbedRecord.View - | AppBskyEmbedImages.View - | AppBskyEmbedVideo.View - | AppBskyEmbedExternal.View - | AppBskyEmbedRecordWithMedia.View - | {$type: string; [k: string]: unknown} - -export function PostEmbeds({ - embed, - moderation, - onOpen, - style, - allowNestedQuotes, - viewContext, -}: { - embed?: Embed - moderation?: ModerationDecision - onOpen?: () => void - style?: StyleProp<ViewStyle> - allowNestedQuotes?: boolean - viewContext?: PostEmbedViewContext -}) { - const {openLightbox} = useLightboxControls() - - // quote post with media - // = - if (AppBskyEmbedRecordWithMedia.isView(embed)) { - return ( - <View style={style}> - <PostEmbeds - embed={embed.media} - moderation={moderation} - onOpen={onOpen} - viewContext={viewContext} - /> - <MaybeQuoteEmbed - embed={embed.record} - onOpen={onOpen} - viewContext={ - viewContext === PostEmbedViewContext.Feed - ? QuoteEmbedViewContext.FeedEmbedRecordWithMedia - : undefined - } - /> - </View> - ) - } - - if (AppBskyEmbedRecord.isView(embed)) { - // custom feed embed (i.e. generator view) - if (AppBskyFeedDefs.isGeneratorView(embed.record)) { - return ( - <View style={a.mt_sm}> - <MaybeFeedCard view={embed.record} /> - </View> - ) - } - - // list embed - if (AppBskyGraphDefs.isListView(embed.record)) { - return ( - <View style={a.mt_sm}> - <MaybeListCard view={embed.record} /> - </View> - ) - } - - // starter pack embed - if (AppBskyGraphDefs.isStarterPackViewBasic(embed.record)) { - return ( - <View style={a.mt_sm}> - <StarterPackCard starterPack={embed.record} /> - </View> - ) - } - - // quote post - // = - return ( - <MaybeQuoteEmbed - embed={embed} - style={style} - onOpen={onOpen} - allowNestedQuotes={allowNestedQuotes} - /> - ) - } - - // image embed - // = - if (AppBskyEmbedImages.isView(embed)) { - const {images} = embed - - if (images.length > 0) { - const items = embed.images.map(img => ({ - uri: img.fullsize, - thumbUri: img.thumb, - alt: img.alt, - dimensions: img.aspectRatio ?? null, - })) - const _openLightbox = ( - index: number, - thumbRects: (MeasuredDimensions | null)[], - fetchedDims: (Dimensions | null)[], - ) => { - openLightbox({ - images: items.map((item, i) => ({ - ...item, - thumbRect: thumbRects[i] ?? null, - thumbDimensions: fetchedDims[i] ?? null, - type: 'image', - })), - index, - }) - } - const onPress = ( - index: number, - refs: AnimatedRef<any>[], - fetchedDims: (Dimensions | null)[], - ) => { - runOnUI(() => { - 'worklet' - const rects: (MeasuredDimensions | null)[] = [] - for (const r of refs) { - rects.push(measure(r)) - } - runOnJS(_openLightbox)(index, rects, fetchedDims) - })() - } - const onPressIn = (_: number) => { - InteractionManager.runAfterInteractions(() => { - Image.prefetch(items.map(i => i.uri)) - }) - } - - if (images.length === 1) { - const image = images[0] - return ( - <ContentHider modui={moderation?.ui('contentMedia')}> - <View style={[a.mt_sm, style]}> - <AutoSizedImage - crop={ - viewContext === PostEmbedViewContext.ThreadHighlighted - ? 'none' - : viewContext === - PostEmbedViewContext.FeedEmbedRecordWithMedia - ? 'square' - : 'constrained' - } - image={image} - onPress={(containerRef, dims) => - onPress(0, [containerRef], [dims]) - } - onPressIn={() => onPressIn(0)} - hideBadge={ - viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia - } - /> - </View> - </ContentHider> - ) - } - - return ( - <ContentHider modui={moderation?.ui('contentMedia')}> - <View style={[a.mt_sm, style]}> - <ImageLayoutGrid - images={embed.images} - onPress={onPress} - onPressIn={onPressIn} - viewContext={viewContext} - /> - </View> - </ContentHider> - ) - } - } - - // external link embed - // = - if (AppBskyEmbedExternal.isView(embed)) { - const link = embed.external - return ( - <ContentHider modui={moderation?.ui('contentMedia')}> - <ExternalLinkEmbed - link={link} - onOpen={onOpen} - style={[a.mt_sm, style]} - /> - </ContentHider> - ) - } - - // video embed - // = - if (AppBskyEmbedVideo.isView(embed)) { - return ( - <ContentHider modui={moderation?.ui('contentMedia')}> - <VideoEmbed - embed={embed} - crop={ - viewContext === PostEmbedViewContext.ThreadHighlighted - ? 'none' - : viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia - ? 'square' - : 'constrained' - } - /> - </ContentHider> - ) - } - - return <View /> -} - -export function MaybeFeedCard({view}: {view: AppBskyFeedDefs.GeneratorView}) { - const pal = usePalette('default') - const moderationOpts = useModerationOpts() - const moderation = React.useMemo(() => { - return moderationOpts - ? moderateFeedGenerator(view, moderationOpts) - : undefined - }, [view, moderationOpts]) - - return ( - <ContentHider modui={moderation?.ui('contentList')}> - <FeedSourceCard - feedUri={view.uri} - style={[pal.view, pal.border, styles.customFeedOuter]} - showLikes - /> - </ContentHider> - ) -} - -export function MaybeListCard({view}: {view: AppBskyGraphDefs.ListView}) { - const moderationOpts = useModerationOpts() - const moderation = React.useMemo(() => { - return moderationOpts ? moderateUserList(view, moderationOpts) : undefined - }, [view, moderationOpts]) - const t = useTheme() - - return ( - <ContentHider modui={moderation?.ui('contentList')}> - <View - style={[ - a.border, - t.atoms.border_contrast_medium, - a.p_md, - a.rounded_sm, - ]}> - <ListCard.Default view={view} /> - </View> - </ContentHider> - ) -} - -const styles = StyleSheet.create({ - altContainer: { - backgroundColor: 'rgba(0, 0, 0, 0.75)', - borderRadius: 6, - paddingHorizontal: 6, - paddingVertical: 3, - position: 'absolute', - right: 6, - bottom: 6, - }, - alt: { - color: 'white', - fontSize: 7, - fontWeight: '600', - }, - customFeedOuter: { - borderWidth: StyleSheet.hairlineWidth, - borderRadius: 8, - paddingHorizontal: 12, - paddingVertical: 12, - }, -}) diff --git a/src/view/com/util/post-embeds/types.ts b/src/view/com/util/post-embeds/types.ts deleted file mode 100644 index 08e903276..000000000 --- a/src/view/com/util/post-embeds/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -export enum PostEmbedViewContext { - ThreadHighlighted = 'ThreadHighlighted', - Feed = 'Feed', - FeedEmbedRecordWithMedia = 'FeedEmbedRecordWithMedia', -} - -export enum QuoteEmbedViewContext { - FeedEmbedRecordWithMedia = PostEmbedViewContext.FeedEmbedRecordWithMedia, -} |