Merge remote-tracking branch 'origin/main' into halo/base
* origin/main: (47 commits) Add option to remove quoted post in composer (#3670) Add border radius to GIF loading state (#3669) use mp4 over webm (#3666) Release 1.79 (#3661) Update Japanese translation (#3656) [Statsig] Add test gates and events (#3659) Quick fix to ensure disable 2fa dialog adjusts to keyboard (#3658) Email auth factor (#3602) GIF Viewer (#3605) Ungate profile scroll fix (#3655) rm country param (#3653) [GIFs] Remove consent flow (#3652) [GIFs] Replace GIPHY with Tenor (#3651) properly close the switch account dialog (#3558) [GIFs] Reset scroll on query change (#3642) Update French translations (#3644) [GIFs] Add error boundary to GIF picker (#3643) Revert "Add layout transitions to the composer photo gallery on iOS (#3609)" (#3649) Update Finnish translations (#3630) Update Japanese translations (#3632) ...
This commit is contained in:
@@ -5,7 +5,6 @@ import {
|
||||
Keyboard,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
@@ -19,6 +18,7 @@ import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
|
||||
import {LikelyType} from '#/lib/link-meta/link-meta'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {logger} from '#/logger'
|
||||
import {emitPostCreated} from '#/state/events'
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
useLanguagePrefsApi,
|
||||
} from '#/state/preferences/languages'
|
||||
import {useProfileQuery} from '#/state/queries/profile'
|
||||
import {Gif} from '#/state/queries/tenor'
|
||||
import {ThreadgateSetting} from '#/state/queries/threadgate'
|
||||
import {getAgent, useSession} from '#/state/session'
|
||||
import {useComposerControls} from '#/state/shell/composer'
|
||||
@@ -48,8 +49,11 @@ import {useDialogStateControlContext} from 'state/dialogs'
|
||||
import {GalleryModel} from 'state/models/media/gallery'
|
||||
import {ComposerOpts} from 'state/shell/composer'
|
||||
import {ComposerReplyTo} from 'view/com/composer/ComposerReplyTo'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {QuoteEmbed} from '../util/post-embeds/QuoteEmbed'
|
||||
import {QuoteEmbed, QuoteX} from '../util/post-embeds/QuoteEmbed'
|
||||
import {Text} from '../util/text/Text'
|
||||
import * as Toast from '../util/Toast'
|
||||
import {UserAvatar} from '../util/UserAvatar'
|
||||
@@ -58,6 +62,7 @@ import {ExternalEmbed} from './ExternalEmbed'
|
||||
import {LabelsBtn} from './labels/LabelsBtn'
|
||||
import {Gallery} from './photos/Gallery'
|
||||
import {OpenCameraBtn} from './photos/OpenCameraBtn'
|
||||
import {SelectGifBtn} from './photos/SelectGifBtn'
|
||||
import {SelectPhotoBtn} from './photos/SelectPhotoBtn'
|
||||
import {SelectLangBtn} from './select-language/SelectLangBtn'
|
||||
import {SuggestedLanguage} from './select-language/SuggestedLanguage'
|
||||
@@ -116,6 +121,7 @@ export const ComposePost = observer(function ComposePost({
|
||||
initQuote,
|
||||
)
|
||||
const {extLink, setExtLink} = useExternalLinkFetch({setQuote})
|
||||
const [extGif, setExtGif] = useState<Gif>()
|
||||
const [labels, setLabels] = useState<string[]>([])
|
||||
const [threadgate, setThreadgate] = useState<ThreadgateSetting[]>([])
|
||||
const gallery = useMemo(
|
||||
@@ -299,13 +305,35 @@ export const ComposePost = observer(function ComposePost({
|
||||
? _(msg`Write your reply`)
|
||||
: _(msg`What's up?`)
|
||||
|
||||
const canSelectImages = useMemo(() => gallery.size < 4, [gallery.size])
|
||||
const canSelectImages = gallery.size < 4 && !extLink
|
||||
const hasMedia = gallery.size > 0 || Boolean(extLink)
|
||||
|
||||
const onEmojiButtonPress = useCallback(() => {
|
||||
openPicker?.(textInput.current?.getCursorPosition())
|
||||
}, [openPicker])
|
||||
|
||||
const focusTextInput = useCallback(() => {
|
||||
textInput.current?.focus()
|
||||
}, [])
|
||||
|
||||
const onSelectGif = useCallback(
|
||||
(gif: Gif) => {
|
||||
setExtLink({
|
||||
uri: `${gif.media_formats.gif.url}?hh=${gif.media_formats.gif.dims[1]}&ww=${gif.media_formats.gif.dims[0]}`,
|
||||
isLoading: true,
|
||||
meta: {
|
||||
url: gif.media_formats.gif.url,
|
||||
image: gif.media_formats.preview.url,
|
||||
likelyType: LikelyType.HTML,
|
||||
title: gif.content_description,
|
||||
description: `ALT: ${gif.content_description}`,
|
||||
},
|
||||
})
|
||||
setExtGif(gif)
|
||||
},
|
||||
[setExtLink],
|
||||
)
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
testID="composePostView"
|
||||
@@ -447,36 +475,47 @@ export const ComposePost = observer(function ComposePost({
|
||||
{gallery.isEmpty && extLink && (
|
||||
<ExternalEmbed
|
||||
link={extLink}
|
||||
onRemove={() => setExtLink(undefined)}
|
||||
gif={extGif}
|
||||
onRemove={() => {
|
||||
setExtLink(undefined)
|
||||
setExtGif(undefined)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{quote ? (
|
||||
<View style={[s.mt5, isWeb && s.mb10, {pointerEvents: 'none'}]}>
|
||||
<QuoteEmbed quote={quote} />
|
||||
<View style={[s.mt5, isWeb && s.mb10]}>
|
||||
<View style={{pointerEvents: 'none'}}>
|
||||
<QuoteEmbed quote={quote} />
|
||||
</View>
|
||||
{quote.uri !== initQuote?.uri && (
|
||||
<QuoteX onRemove={() => setQuote(undefined)} />
|
||||
)}
|
||||
</View>
|
||||
) : undefined}
|
||||
</ScrollView>
|
||||
<SuggestedLanguage text={richtext.text} />
|
||||
<View style={[pal.border, styles.bottomBar]}>
|
||||
{canSelectImages ? (
|
||||
<>
|
||||
<SelectPhotoBtn gallery={gallery} />
|
||||
<OpenCameraBtn gallery={gallery} />
|
||||
</>
|
||||
) : null}
|
||||
{!isMobile ? (
|
||||
<Pressable
|
||||
onPress={onEmojiButtonPress}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Open emoji picker`)}
|
||||
accessibilityHint={_(msg`Open emoji picker`)}>
|
||||
<FontAwesomeIcon
|
||||
icon={['far', 'face-smile']}
|
||||
color={pal.colors.link}
|
||||
size={22}
|
||||
/>
|
||||
</Pressable>
|
||||
) : null}
|
||||
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
|
||||
<SelectPhotoBtn gallery={gallery} disabled={!canSelectImages} />
|
||||
<OpenCameraBtn gallery={gallery} disabled={!canSelectImages} />
|
||||
<SelectGifBtn
|
||||
onClose={focusTextInput}
|
||||
onSelectGif={onSelectGif}
|
||||
disabled={hasMedia}
|
||||
/>
|
||||
{!isMobile ? (
|
||||
<Button
|
||||
onPress={onEmojiButtonPress}
|
||||
style={a.p_sm}
|
||||
label={_(msg`Open emoji picker`)}
|
||||
accessibilityHint={_(msg`Open emoji picker`)}
|
||||
variant="ghost"
|
||||
shape="round"
|
||||
color="primary">
|
||||
<EmojiSmile size="lg" />
|
||||
</Button>
|
||||
) : null}
|
||||
</View>
|
||||
<View style={s.flex1} />
|
||||
<SelectLangBtn />
|
||||
<CharProgress count={graphemeLength} />
|
||||
@@ -571,7 +610,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
bottomBar: {
|
||||
flexDirection: 'row',
|
||||
paddingVertical: 10,
|
||||
paddingVertical: 4,
|
||||
paddingLeft: 15,
|
||||
paddingRight: 20,
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React from 'react'
|
||||
import {TouchableOpacity, View} from 'react-native'
|
||||
import {StyleProp, TouchableOpacity, View, ViewStyle} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {ExternalEmbedDraft} from 'lib/api/index'
|
||||
import {s} from 'lib/styles'
|
||||
import {Gif} from 'state/queries/tenor'
|
||||
import {ExternalLinkEmbed} from 'view/com/util/post-embeds/ExternalLinkEmbed'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Loader} from '#/components/Loader'
|
||||
@@ -14,9 +15,11 @@ import {Text} from '#/components/Typography'
|
||||
export const ExternalEmbed = ({
|
||||
link,
|
||||
onRemove,
|
||||
gif,
|
||||
}: {
|
||||
link?: ExternalEmbedDraft
|
||||
onRemove: () => void
|
||||
gif?: Gif
|
||||
}) => {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
@@ -34,45 +37,38 @@ export const ExternalEmbed = ({
|
||||
|
||||
if (!link) return null
|
||||
|
||||
const loadingStyle: ViewStyle | undefined = gif
|
||||
? {
|
||||
aspectRatio:
|
||||
gif.media_formats.gif.dims[0] / gif.media_formats.gif.dims[1],
|
||||
width: '100%',
|
||||
}
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.border,
|
||||
a.rounded_sm,
|
||||
a.mt_2xl,
|
||||
a.mb_xl,
|
||||
a.overflow_hidden,
|
||||
t.atoms.border_contrast_medium,
|
||||
]}>
|
||||
<View style={[a.mb_xl, a.overflow_hidden, t.atoms.border_contrast_medium]}>
|
||||
{link.isLoading ? (
|
||||
<View
|
||||
style={[
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
a.py_5xl,
|
||||
t.atoms.bg_contrast_25,
|
||||
]}>
|
||||
<Container style={loadingStyle}>
|
||||
<Loader size="xl" />
|
||||
</View>
|
||||
</Container>
|
||||
) : link.meta?.error ? (
|
||||
<View
|
||||
style={[a.justify_center, a.p_md, a.gap_xs, t.atoms.bg_contrast_25]}>
|
||||
<Container style={[a.align_start, a.p_md, a.gap_xs]}>
|
||||
<Text numberOfLines={1} style={t.atoms.text_contrast_high}>
|
||||
{link.uri}
|
||||
</Text>
|
||||
<Text numberOfLines={2} style={[{color: t.palette.negative_400}]}>
|
||||
{link.meta.error}
|
||||
{link.meta?.error}
|
||||
</Text>
|
||||
</View>
|
||||
</Container>
|
||||
) : linkInfo ? (
|
||||
<View style={{pointerEvents: 'none'}}>
|
||||
<View style={{pointerEvents: !gif ? 'none' : 'auto'}}>
|
||||
<ExternalLinkEmbed link={linkInfo} />
|
||||
</View>
|
||||
) : null}
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 10,
|
||||
top: 16,
|
||||
right: 10,
|
||||
height: 36,
|
||||
width: 36,
|
||||
@@ -91,3 +87,29 @@ export const ExternalEmbed = ({
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function Container({
|
||||
style,
|
||||
children,
|
||||
}: {
|
||||
style?: StyleProp<ViewStyle>
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.mt_sm,
|
||||
a.rounded_sm,
|
||||
a.border,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
a.py_5xl,
|
||||
t.atoms.bg_contrast_25,
|
||||
t.atoms.border_contrast_medium,
|
||||
style,
|
||||
]}>
|
||||
{children}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,32 +1,31 @@
|
||||
import React, {useCallback} from 'react'
|
||||
import {TouchableOpacity, StyleSheet} from 'react-native'
|
||||
import * as MediaLibrary from 'expo-media-library'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconStyle,
|
||||
} from '@fortawesome/react-native-fontawesome'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {openCamera} from 'lib/media/picker'
|
||||
import {useCameraPermission} from 'lib/hooks/usePermissions'
|
||||
import {HITSLOP_10, POST_IMG_MAX} from 'lib/constants'
|
||||
import {GalleryModel} from 'state/models/media/gallery'
|
||||
import {isMobileWeb, isNative} from 'platform/detection'
|
||||
import {logger} from '#/logger'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useAnalytics} from '#/lib/analytics/analytics'
|
||||
import {POST_IMG_MAX} from '#/lib/constants'
|
||||
import {useCameraPermission} from '#/lib/hooks/usePermissions'
|
||||
import {openCamera} from '#/lib/media/picker'
|
||||
import {logger} from '#/logger'
|
||||
import {isMobileWeb, isNative} from '#/platform/detection'
|
||||
import {GalleryModel} from '#/state/models/media/gallery'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import {Camera_Stroke2_Corner0_Rounded as Camera} from '#/components/icons/Camera'
|
||||
|
||||
type Props = {
|
||||
gallery: GalleryModel
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export function OpenCameraBtn({gallery}: Props) {
|
||||
const pal = usePalette('default')
|
||||
export function OpenCameraBtn({gallery, disabled}: Props) {
|
||||
const {track} = useAnalytics()
|
||||
const {_} = useLingui()
|
||||
const {requestCameraAccessIfNeeded} = useCameraPermission()
|
||||
const [mediaPermissionRes, requestMediaPermission] =
|
||||
MediaLibrary.usePermissions()
|
||||
const t = useTheme()
|
||||
|
||||
const onPressTakePicture = useCallback(async () => {
|
||||
track('Composer:CameraOpened')
|
||||
@@ -68,25 +67,17 @@ export function OpenCameraBtn({gallery}: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
<Button
|
||||
testID="openCameraButton"
|
||||
onPress={onPressTakePicture}
|
||||
style={styles.button}
|
||||
hitSlop={HITSLOP_10}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Camera`)}
|
||||
accessibilityHint={_(msg`Opens camera on device`)}>
|
||||
<FontAwesomeIcon
|
||||
icon="camera"
|
||||
style={pal.link as FontAwesomeIconStyle}
|
||||
size={24}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
label={_(msg`Camera`)}
|
||||
accessibilityHint={_(msg`Opens camera on device`)}
|
||||
style={a.p_sm}
|
||||
variant="ghost"
|
||||
shape="round"
|
||||
color="primary"
|
||||
disabled={disabled}>
|
||||
<Camera size="lg" style={disabled && t.atoms.text_contrast_low} />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
paddingHorizontal: 15,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import React, {useCallback} from 'react'
|
||||
import {Keyboard} from 'react-native'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {Gif} from '#/state/queries/tenor'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
import {GifSelectDialog} from '#/components/dialogs/GifSelect'
|
||||
import {GifSquare_Stroke2_Corner0_Rounded as GifIcon} from '#/components/icons/Gif'
|
||||
|
||||
type Props = {
|
||||
onClose: () => void
|
||||
onSelectGif: (gif: Gif) => void
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export function SelectGifBtn({onClose, onSelectGif, disabled}: Props) {
|
||||
const {_} = useLingui()
|
||||
const control = useDialogControl()
|
||||
const t = useTheme()
|
||||
|
||||
const onPressSelectGif = useCallback(async () => {
|
||||
logEvent('composer:gif:open', {})
|
||||
Keyboard.dismiss()
|
||||
control.open()
|
||||
}, [control])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
testID="openGifBtn"
|
||||
onPress={onPressSelectGif}
|
||||
label={_(msg`Select GIF`)}
|
||||
accessibilityHint={_(msg`Opens GIF select dialog`)}
|
||||
style={a.p_sm}
|
||||
variant="ghost"
|
||||
shape="round"
|
||||
color="primary"
|
||||
disabled={disabled}>
|
||||
<GifIcon size="lg" style={disabled && t.atoms.text_contrast_low} />
|
||||
</Button>
|
||||
|
||||
<GifSelectDialog
|
||||
control={control}
|
||||
onClose={onClose}
|
||||
onSelectGif={onSelectGif}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,27 +1,26 @@
|
||||
/* eslint-disable react-native-a11y/has-valid-accessibility-ignores-invert-colors */
|
||||
import React, {useCallback} from 'react'
|
||||
import {TouchableOpacity, StyleSheet} from 'react-native'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconStyle,
|
||||
} from '@fortawesome/react-native-fontawesome'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {usePhotoLibraryPermission} from 'lib/hooks/usePermissions'
|
||||
import {GalleryModel} from 'state/models/media/gallery'
|
||||
import {HITSLOP_10} from 'lib/constants'
|
||||
import {isNative} from 'platform/detection'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useAnalytics} from '#/lib/analytics/analytics'
|
||||
import {usePhotoLibraryPermission} from '#/lib/hooks/usePermissions'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {GalleryModel} from '#/state/models/media/gallery'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import {Image_Stroke2_Corner0_Rounded as Image} from '#/components/icons/Image'
|
||||
|
||||
type Props = {
|
||||
gallery: GalleryModel
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export function SelectPhotoBtn({gallery}: Props) {
|
||||
const pal = usePalette('default')
|
||||
export function SelectPhotoBtn({gallery, disabled}: Props) {
|
||||
const {track} = useAnalytics()
|
||||
const {_} = useLingui()
|
||||
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
|
||||
const t = useTheme()
|
||||
|
||||
const onPressSelectPhotos = useCallback(async () => {
|
||||
track('Composer:GalleryOpened')
|
||||
@@ -34,25 +33,17 @@ export function SelectPhotoBtn({gallery}: Props) {
|
||||
}, [track, requestPhotoAccessIfNeeded, gallery])
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
<Button
|
||||
testID="openGalleryBtn"
|
||||
onPress={onPressSelectPhotos}
|
||||
style={styles.button}
|
||||
hitSlop={HITSLOP_10}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Gallery`)}
|
||||
accessibilityHint={_(msg`Opens device photo gallery`)}>
|
||||
<FontAwesomeIcon
|
||||
icon={['far', 'image']}
|
||||
style={pal.link as FontAwesomeIconStyle}
|
||||
size={24}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
label={_(msg`Gallery`)}
|
||||
accessibilityHint={_(msg`Opens device photo gallery`)}
|
||||
style={a.p_sm}
|
||||
variant="ghost"
|
||||
shape="round"
|
||||
color="primary"
|
||||
disabled={disabled}>
|
||||
<Image size="lg" style={disabled && t.atoms.text_contrast_low} />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
paddingHorizontal: 15,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -107,17 +107,11 @@ export function FeedPage({
|
||||
})
|
||||
}, [scrollToTop, feed, queryClient, setHasNew])
|
||||
|
||||
let feedPollInterval
|
||||
if (
|
||||
feed === // Discover
|
||||
'feedgen|at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot' &&
|
||||
// TODO: This gate check is still too early. Move it to where the polling happens.
|
||||
gate('disable_poll_on_discover')
|
||||
) {
|
||||
feedPollInterval = undefined
|
||||
} else {
|
||||
feedPollInterval = POLL_FREQ
|
||||
}
|
||||
const isDiscoverFeed =
|
||||
feed ===
|
||||
'feedgen|at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot'
|
||||
const adjustedHasNew =
|
||||
hasNew && !(isDiscoverFeed && gate('disable_poll_on_discover_v2'))
|
||||
|
||||
return (
|
||||
<View testID={testID} style={s.h100pct}>
|
||||
@@ -127,7 +121,7 @@ export function FeedPage({
|
||||
enabled={isPageFocused}
|
||||
feed={feed}
|
||||
feedParams={feedParams}
|
||||
pollInterval={feedPollInterval}
|
||||
pollInterval={POLL_FREQ}
|
||||
disablePoll={hasNew}
|
||||
scrollElRef={scrollElRef}
|
||||
onScrolledDownChange={setIsScrolledDown}
|
||||
@@ -138,11 +132,11 @@ export function FeedPage({
|
||||
savedFeedConfig={savedFeedConfig}
|
||||
/>
|
||||
</MainScrollProvider>
|
||||
{(isScrolledDown || hasNew) && (
|
||||
{(isScrolledDown || adjustedHasNew) && (
|
||||
<LoadLatestBtn
|
||||
onPress={onPressLoadLatest}
|
||||
label={_(msg`Load new posts`)}
|
||||
showIndicator={hasNew}
|
||||
showIndicator={adjustedHasNew}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -6,23 +6,24 @@ import {
|
||||
StyleSheet,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {Svg, Circle, Path} from 'react-native-svg'
|
||||
import {ScrollView, TextInput} from './util'
|
||||
import {Circle, Path, Svg} from 'react-native-svg'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {Button} from '../util/forms/Button'
|
||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||
import * as Toast from '../util/Toast'
|
||||
import {s, colors} from 'lib/styles'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {getAgent, useSession, useSessionApi} from '#/state/session'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {cleanError} from 'lib/strings/errors'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {useSession, useSessionApi, getAgent} from '#/state/session'
|
||||
import {logger} from '#/logger'
|
||||
import {colors, s} from 'lib/styles'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||
import {Button} from '../util/forms/Button'
|
||||
import {Text} from '../util/text/Text'
|
||||
import * as Toast from '../util/Toast'
|
||||
import {ScrollView, TextInput} from './util'
|
||||
|
||||
export const snapPoints = ['90%']
|
||||
|
||||
@@ -32,7 +33,13 @@ enum Stages {
|
||||
ConfirmCode,
|
||||
}
|
||||
|
||||
export function Component({showReminder}: {showReminder?: boolean}) {
|
||||
export function Component({
|
||||
showReminder,
|
||||
onSuccess,
|
||||
}: {
|
||||
showReminder?: boolean
|
||||
onSuccess?: () => void
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const {currentAccount} = useSession()
|
||||
const {updateCurrentAccount} = useSessionApi()
|
||||
@@ -77,6 +84,7 @@ export function Component({showReminder}: {showReminder?: boolean}) {
|
||||
updateCurrentAccount({emailConfirmed: true})
|
||||
Toast.show(_(msg`Email verified`))
|
||||
closeModal()
|
||||
onSuccess?.()
|
||||
} catch (e) {
|
||||
setError(cleanError(String(e)))
|
||||
} finally {
|
||||
|
||||
@@ -140,7 +140,7 @@ function PostThreadFollowBtnLoaded({
|
||||
style={[!isFollowing ? palInverted.text : pal.text, s.bold]}
|
||||
numberOfLines={1}>
|
||||
{!isFollowing ? (
|
||||
isFollowedBy && gate('show_follow_back_label') ? (
|
||||
isFollowedBy && gate('show_follow_back_label_v2') ? (
|
||||
<Trans>Follow Back</Trans>
|
||||
) : (
|
||||
<Trans>Follow</Trans>
|
||||
|
||||
@@ -27,6 +27,7 @@ import {sanitizeHandle} from 'lib/strings/handles'
|
||||
import {countLines, pluralize} from 'lib/strings/helpers'
|
||||
import {niceDate} from 'lib/strings/time'
|
||||
import {s} from 'lib/styles'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {useSession} from 'state/session'
|
||||
import {PostThreadFollowBtn} from 'view/com/post-thread/PostThreadFollowBtn'
|
||||
import {atoms as a} from '#/alf'
|
||||
@@ -478,7 +479,12 @@ let PostThreadItemLoaded = ({
|
||||
avatarSize={28}
|
||||
displayNameType="md-bold"
|
||||
displayNameStyle={isThreadedChild && s.ml2}
|
||||
style={isThreadedChild && s.mb2}
|
||||
style={
|
||||
isThreadedChild && {
|
||||
alignItems: 'center',
|
||||
paddingBottom: isWeb ? 5 : 2,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<LabelsOnMyPost post={post} />
|
||||
<PostAlerts
|
||||
@@ -669,7 +675,6 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
layoutContentThreaded: {
|
||||
flex: 1,
|
||||
paddingRight: 10,
|
||||
},
|
||||
meta: {
|
||||
flexDirection: 'row',
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import React, {Component, ErrorInfo, ReactNode} from 'react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {ErrorScreen} from './error/ErrorScreen'
|
||||
import {CenteredView} from './Views'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {logger} from '#/logger'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
interface Props {
|
||||
children?: ReactNode
|
||||
renderError?: (error: any) => ReactNode
|
||||
}
|
||||
|
||||
interface State {
|
||||
@@ -30,6 +32,10 @@ export class ErrorBoundary extends Component<Props, State> {
|
||||
|
||||
public render() {
|
||||
if (this.state.hasError) {
|
||||
if (this.props.renderError) {
|
||||
return this.props.renderError(this.state.error)
|
||||
}
|
||||
|
||||
return (
|
||||
<CenteredView style={{height: '100%', flex: 1}}>
|
||||
<TranslatedErrorScreen details={this.state.error.toString()} />
|
||||
|
||||
@@ -54,10 +54,12 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
<Text numberOfLines={1} style={[styles.maxWidth, pal.textLight]}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={[styles.maxWidth, pal.textLight, opts.displayNameStyle]}>
|
||||
<TextLinkOnWebOnly
|
||||
type={opts.displayNameType || 'lg-bold'}
|
||||
style={[pal.text, opts.displayNameStyle]}
|
||||
style={[pal.text]}
|
||||
lineHeight={1.2}
|
||||
disableMismatchWarning
|
||||
text={
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import React from 'react'
|
||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
import {CenteredView} from './Views'
|
||||
import {Text} from './text/Text'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
|
||||
import Animated from 'react-native-reanimated'
|
||||
import {useSetDrawerOpen} from '#/state/shell'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {useSetDrawerOpen} from '#/state/shell'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
import {useTheme} from '#/alf'
|
||||
import {Text} from './text/Text'
|
||||
import {CenteredView} from './Views'
|
||||
|
||||
const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20}
|
||||
|
||||
@@ -62,6 +63,7 @@ export function ViewHeader({
|
||||
return (
|
||||
<DesktopWebHeader
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
renderButton={renderButton}
|
||||
showBorder={showBorder}
|
||||
/>
|
||||
@@ -136,14 +138,17 @@ export function ViewHeader({
|
||||
|
||||
function DesktopWebHeader({
|
||||
title,
|
||||
subtitle,
|
||||
renderButton,
|
||||
showBorder = true,
|
||||
}: {
|
||||
title: string
|
||||
subtitle?: string
|
||||
renderButton?: () => JSX.Element
|
||||
showBorder?: boolean
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const t = useTheme()
|
||||
return (
|
||||
<CenteredView
|
||||
style={[
|
||||
@@ -153,13 +158,30 @@ function DesktopWebHeader({
|
||||
{
|
||||
borderBottomWidth: showBorder ? 1 : 0,
|
||||
},
|
||||
{display: 'flex', flexDirection: 'column'},
|
||||
]}>
|
||||
<View style={styles.titleContainer} pointerEvents="none">
|
||||
<Text type="title-lg" style={[pal.text, styles.title]}>
|
||||
{title}
|
||||
</Text>
|
||||
<View>
|
||||
<View style={styles.titleContainer} pointerEvents="none">
|
||||
<Text type="title-lg" style={[pal.text, styles.title]}>
|
||||
{title}
|
||||
</Text>
|
||||
</View>
|
||||
{renderButton?.()}
|
||||
</View>
|
||||
{renderButton?.()}
|
||||
{subtitle ? (
|
||||
<View>
|
||||
<View style={[styles.titleContainer]} pointerEvents="none">
|
||||
<Text
|
||||
style={[
|
||||
pal.text,
|
||||
styles.subtitleDesktop,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
{subtitle}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
) : null}
|
||||
</CenteredView>
|
||||
)
|
||||
}
|
||||
@@ -236,6 +258,9 @@ const styles = StyleSheet.create({
|
||||
subtitle: {
|
||||
fontSize: 13,
|
||||
},
|
||||
subtitleDesktop: {
|
||||
fontSize: 15,
|
||||
},
|
||||
backBtn: {
|
||||
width: 30,
|
||||
height: 30,
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
import React from 'react'
|
||||
import React, {useCallback} from 'react'
|
||||
import {StyleProp, View, ViewStyle} from 'react-native'
|
||||
import {Image} from 'expo-image'
|
||||
import {Text} from '../text/Text'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import {AppBskyEmbedExternal} from '@atproto/api'
|
||||
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {AppBskyEmbedExternal} from '@atproto/api'
|
||||
import {toNiceDomain} from 'lib/strings/url-helpers'
|
||||
import {shareUrl} from 'lib/sharing'
|
||||
import {parseEmbedPlayerFromUrl} from 'lib/strings/embed-player'
|
||||
import {ExternalPlayer} from 'view/com/util/post-embeds/ExternalPlayerEmbed'
|
||||
import {ExternalGifEmbed} from 'view/com/util/post-embeds/ExternalGifEmbed'
|
||||
import {toNiceDomain} from 'lib/strings/url-helpers'
|
||||
import {isNative} from 'platform/detection'
|
||||
import {useExternalEmbedsPrefs} from 'state/preferences'
|
||||
import {Link} from 'view/com/util/Link'
|
||||
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 {Text} from '../text/Text'
|
||||
|
||||
export const ExternalLinkEmbed = ({
|
||||
link,
|
||||
style,
|
||||
}: {
|
||||
link: AppBskyEmbedExternal.ViewExternal
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) => {
|
||||
const pal = usePalette('default')
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
@@ -28,62 +36,95 @@ export const ExternalLinkEmbed = ({
|
||||
}
|
||||
}, [link.uri, externalEmbedPrefs])
|
||||
|
||||
if (embedPlayerParams?.source === 'tenor') {
|
||||
return <GifEmbed params={embedPlayerParams} link={link} />
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{link.thumb && !embedPlayerParams ? (
|
||||
<Image
|
||||
style={{aspectRatio: 1.91}}
|
||||
source={{uri: link.thumb}}
|
||||
accessibilityIgnoresInvertColors
|
||||
/>
|
||||
) : undefined}
|
||||
{(embedPlayerParams?.isGif && (
|
||||
<ExternalGifEmbed link={link} params={embedPlayerParams} />
|
||||
)) ||
|
||||
(embedPlayerParams && (
|
||||
<ExternalPlayer link={link} params={embedPlayerParams} />
|
||||
))}
|
||||
<View style={[styles.info, {paddingHorizontal: isMobile ? 10 : 14}]}>
|
||||
<Text
|
||||
type="sm"
|
||||
numberOfLines={1}
|
||||
style={[pal.textLight, styles.extUri]}>
|
||||
{toNiceDomain(link.uri)}
|
||||
</Text>
|
||||
{!embedPlayerParams?.isGif && (
|
||||
<Text type="lg-bold" numberOfLines={3} style={[pal.text]}>
|
||||
{link.title || link.uri}
|
||||
</Text>
|
||||
)}
|
||||
{link.description && !embedPlayerParams?.hideDetails ? (
|
||||
<Text
|
||||
type="md"
|
||||
numberOfLines={link.thumb ? 2 : 4}
|
||||
style={[pal.text, styles.extDescription]}>
|
||||
{link.description}
|
||||
</Text>
|
||||
<View style={[a.flex_col, a.rounded_sm, a.overflow_hidden, a.mt_sm]}>
|
||||
<LinkWrapper link={link} style={style}>
|
||||
{link.thumb && !embedPlayerParams ? (
|
||||
<Image
|
||||
style={{
|
||||
aspectRatio: 1.91,
|
||||
borderTopRightRadius: 6,
|
||||
borderTopLeftRadius: 6,
|
||||
}}
|
||||
source={{uri: link.thumb}}
|
||||
accessibilityIgnoresInvertColors
|
||||
/>
|
||||
) : undefined}
|
||||
</View>
|
||||
{embedPlayerParams?.isGif ? (
|
||||
<ExternalGifEmbed link={link} params={embedPlayerParams} />
|
||||
) : embedPlayerParams ? (
|
||||
<ExternalPlayer link={link} params={embedPlayerParams} />
|
||||
) : undefined}
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.py_sm,
|
||||
{
|
||||
paddingHorizontal: isMobile ? 10 : 14,
|
||||
},
|
||||
]}>
|
||||
<Text
|
||||
type="sm"
|
||||
numberOfLines={1}
|
||||
style={[pal.textLight, {marginVertical: 2}]}>
|
||||
{toNiceDomain(link.uri)}
|
||||
</Text>
|
||||
|
||||
{!embedPlayerParams?.isGif && !embedPlayerParams?.dimensions && (
|
||||
<Text type="lg-bold" numberOfLines={3} style={[pal.text]}>
|
||||
{link.title || link.uri}
|
||||
</Text>
|
||||
)}
|
||||
{link.description ? (
|
||||
<Text
|
||||
type="md"
|
||||
numberOfLines={link.thumb ? 2 : 4}
|
||||
style={[pal.text, a.mt_xs]}>
|
||||
{link.description}
|
||||
</Text>
|
||||
) : undefined}
|
||||
</View>
|
||||
</LinkWrapper>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'column',
|
||||
borderRadius: 6,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
info: {
|
||||
width: '100%',
|
||||
bottom: 0,
|
||||
paddingTop: 8,
|
||||
paddingBottom: 10,
|
||||
},
|
||||
extUri: {
|
||||
marginTop: 2,
|
||||
},
|
||||
extDescription: {
|
||||
marginTop: 4,
|
||||
},
|
||||
})
|
||||
function LinkWrapper({
|
||||
link,
|
||||
style,
|
||||
children,
|
||||
}: {
|
||||
link: AppBskyEmbedExternal.ViewExternal
|
||||
style?: StyleProp<ViewStyle>
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const t = useTheme()
|
||||
|
||||
const onShareExternal = useCallback(() => {
|
||||
if (link.uri && isNative) {
|
||||
shareUrl(link.uri)
|
||||
}
|
||||
}, [link.uri])
|
||||
|
||||
return (
|
||||
<Link
|
||||
asAnchor
|
||||
anchorNoUnderline
|
||||
href={link.uri}
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.border,
|
||||
a.rounded_sm,
|
||||
t.atoms.border_contrast_medium,
|
||||
style,
|
||||
]}
|
||||
hoverStyle={t.atoms.border_contrast_high}
|
||||
onLongPress={onShareExternal}>
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import React from 'react'
|
||||
import {Pressable, View} from 'react-native'
|
||||
import {AppBskyEmbedExternal} from '@atproto/api'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {EmbedPlayerParams} from 'lib/strings/embed-player'
|
||||
import {useAutoplayDisabled} from 'state/preferences'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {GifView} from '../../../../../modules/expo-bluesky-gif-view'
|
||||
import {GifViewStateChangeEvent} from '../../../../../modules/expo-bluesky-gif-view/src/GifView.types'
|
||||
|
||||
function PlaybackControls({
|
||||
onPress,
|
||||
isPlaying,
|
||||
isLoaded,
|
||||
}: {
|
||||
onPress: () => void
|
||||
isPlaying: boolean
|
||||
isLoaded: boolean
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityHint={_(msg`Play or pause the GIF`)}
|
||||
accessibilityLabel={isPlaying ? _(msg`Pause`) : _(msg`Play`)}
|
||||
style={[
|
||||
a.absolute,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
!isLoaded && a.border,
|
||||
t.atoms.border_contrast_medium,
|
||||
a.inset_0,
|
||||
a.w_full,
|
||||
a.h_full,
|
||||
a.rounded_sm,
|
||||
{
|
||||
zIndex: 2,
|
||||
backgroundColor: !isLoaded
|
||||
? t.atoms.bg_contrast_25.backgroundColor
|
||||
: !isPlaying
|
||||
? 'rgba(0, 0, 0, 0.3)'
|
||||
: undefined,
|
||||
},
|
||||
]}
|
||||
onPress={onPress}>
|
||||
{!isLoaded ? (
|
||||
<View>
|
||||
<View style={[a.align_center, a.justify_center]}>
|
||||
<Loader size="xl" />
|
||||
</View>
|
||||
</View>
|
||||
) : !isPlaying ? (
|
||||
<View
|
||||
style={[
|
||||
a.rounded_full,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
{
|
||||
backgroundColor: t.palette.primary_500,
|
||||
width: 60,
|
||||
height: 60,
|
||||
},
|
||||
]}>
|
||||
<FontAwesomeIcon
|
||||
icon="play"
|
||||
size={42}
|
||||
color="white"
|
||||
style={{marginLeft: 8}}
|
||||
/>
|
||||
</View>
|
||||
) : undefined}
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
export function GifEmbed({
|
||||
params,
|
||||
link,
|
||||
}: {
|
||||
params: EmbedPlayerParams
|
||||
link: AppBskyEmbedExternal.ViewExternal
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const autoplayDisabled = useAutoplayDisabled()
|
||||
|
||||
const playerRef = React.useRef<GifView>(null)
|
||||
|
||||
const [playerState, setPlayerState] = React.useState<{
|
||||
isPlaying: boolean
|
||||
isLoaded: boolean
|
||||
}>({
|
||||
isPlaying: !autoplayDisabled,
|
||||
isLoaded: false,
|
||||
})
|
||||
|
||||
const onPlayerStateChange = React.useCallback(
|
||||
(e: GifViewStateChangeEvent) => {
|
||||
setPlayerState(e.nativeEvent)
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const onPress = React.useCallback(() => {
|
||||
playerRef.current?.toggleAsync()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<View style={[a.rounded_sm, a.overflow_hidden, a.mt_sm]}>
|
||||
<View
|
||||
style={[
|
||||
a.rounded_sm,
|
||||
a.overflow_hidden,
|
||||
{
|
||||
aspectRatio: params.dimensions!.width / params.dimensions!.height,
|
||||
},
|
||||
]}>
|
||||
<PlaybackControls
|
||||
onPress={onPress}
|
||||
isPlaying={playerState.isPlaying}
|
||||
isLoaded={playerState.isLoaded}
|
||||
/>
|
||||
<GifView
|
||||
source={params.playerUri}
|
||||
placeholderSource={link.thumb}
|
||||
style={[a.flex_1, a.rounded_sm]}
|
||||
autoplay={!autoplayDisabled}
|
||||
onPlayerStateChange={onPlayerStateChange}
|
||||
ref={playerRef}
|
||||
accessibilityHint={_(msg`Animated GIF`)}
|
||||
accessibilityLabel={link.description.replace('ALT: ', '')}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,11 @@
|
||||
import React from 'react'
|
||||
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
|
||||
import {
|
||||
StyleProp,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
import {
|
||||
AppBskyEmbedExternal,
|
||||
AppBskyEmbedImages,
|
||||
@@ -12,9 +18,13 @@ import {
|
||||
RichText as RichTextAPI,
|
||||
} from '@atproto/api'
|
||||
import {AtUri} from '@atproto/api'
|
||||
import {Trans} from '@lingui/macro'
|
||||
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 {s} from '#/lib/styles'
|
||||
import {useModerationOpts} from '#/state/queries/preferences'
|
||||
import {RQKEY as RQKEY_URI} from '#/state/queries/resolve-uri'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
@@ -177,6 +187,33 @@ export function QuoteEmbed({
|
||||
)
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
function viewRecordToPostView(
|
||||
viewRecord: AppBskyEmbedRecord.ViewRecord,
|
||||
): AppBskyFeedDefs.PostView {
|
||||
|
||||
@@ -1,34 +1,32 @@
|
||||
import React, {useCallback} from 'react'
|
||||
import React from 'react'
|
||||
import {
|
||||
StyleSheet,
|
||||
InteractionManager,
|
||||
StyleProp,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
ViewStyle,
|
||||
Text,
|
||||
InteractionManager,
|
||||
} from 'react-native'
|
||||
import {Image} from 'expo-image'
|
||||
import {
|
||||
AppBskyEmbedImages,
|
||||
AppBskyEmbedExternal,
|
||||
AppBskyEmbedImages,
|
||||
AppBskyEmbedRecord,
|
||||
AppBskyEmbedRecordWithMedia,
|
||||
AppBskyFeedDefs,
|
||||
AppBskyGraphDefs,
|
||||
ModerationDecision,
|
||||
} from '@atproto/api'
|
||||
import {Link} from '../Link'
|
||||
import {ImageLayoutGrid} from '../images/ImageLayoutGrid'
|
||||
import {useLightboxControls, ImagesLightbox} from '#/state/lightbox'
|
||||
|
||||
import {ImagesLightbox, useLightboxControls} from '#/state/lightbox'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {ExternalLinkEmbed} from './ExternalLinkEmbed'
|
||||
import {MaybeQuoteEmbed} from './QuoteEmbed'
|
||||
import {AutoSizedImage} from '../images/AutoSizedImage'
|
||||
import {ListEmbed} from './ListEmbed'
|
||||
import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
|
||||
import {ContentHider} from '../../../../components/moderation/ContentHider'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {shareUrl} from '#/lib/sharing'
|
||||
import {AutoSizedImage} from '../images/AutoSizedImage'
|
||||
import {ImageLayoutGrid} from '../images/ImageLayoutGrid'
|
||||
import {ExternalLinkEmbed} from './ExternalLinkEmbed'
|
||||
import {ListEmbed} from './ListEmbed'
|
||||
import {MaybeQuoteEmbed} from './QuoteEmbed'
|
||||
|
||||
type Embed =
|
||||
| AppBskyEmbedRecord.View
|
||||
@@ -49,16 +47,6 @@ export function PostEmbeds({
|
||||
const pal = usePalette('default')
|
||||
const {openLightbox} = useLightboxControls()
|
||||
|
||||
const externalUri = AppBskyEmbedExternal.isView(embed)
|
||||
? embed.external.uri
|
||||
: null
|
||||
|
||||
const onShareExternal = useCallback(() => {
|
||||
if (externalUri && isNative) {
|
||||
shareUrl(externalUri)
|
||||
}
|
||||
}, [externalUri])
|
||||
|
||||
// quote post with media
|
||||
// =
|
||||
if (AppBskyEmbedRecordWithMedia.isView(embed)) {
|
||||
@@ -161,18 +149,9 @@ export function PostEmbeds({
|
||||
// =
|
||||
if (AppBskyEmbedExternal.isView(embed)) {
|
||||
const link = embed.external
|
||||
|
||||
return (
|
||||
<ContentHider modui={moderation?.ui('contentMedia')}>
|
||||
<Link
|
||||
asAnchor
|
||||
anchorNoUnderline
|
||||
href={link.uri}
|
||||
style={[styles.extOuter, pal.view, pal.borderDark, style]}
|
||||
hoverStyle={{borderColor: pal.colors.borderLinkHover}}
|
||||
onLongPress={onShareExternal}>
|
||||
<ExternalLinkEmbed link={link} />
|
||||
</Link>
|
||||
<ExternalLinkEmbed link={link} style={style} />
|
||||
</ContentHider>
|
||||
)
|
||||
}
|
||||
@@ -187,11 +166,6 @@ const styles = StyleSheet.create({
|
||||
singleImage: {
|
||||
borderRadius: 8,
|
||||
},
|
||||
extOuter: {
|
||||
borderWidth: 1,
|
||||
borderRadius: 8,
|
||||
marginTop: 4,
|
||||
},
|
||||
altContainer: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.75)',
|
||||
borderRadius: 6,
|
||||
|
||||
Reference in New Issue
Block a user