diff options
Diffstat (limited to 'src/view/com')
23 files changed, 227 insertions, 113 deletions
diff --git a/src/view/com/composer/ComposerReplyTo.tsx b/src/view/com/composer/ComposerReplyTo.tsx index 678c8581f..39a1473a3 100644 --- a/src/view/com/composer/ComposerReplyTo.tsx +++ b/src/view/com/composer/ComposerReplyTo.tsx @@ -83,7 +83,11 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) { accessibilityHint={_( msg`Expand or collapse the full post you are replying to`, )}> - <UserAvatar avatar={replyTo.author.avatar} size={50} /> + <UserAvatar + avatar={replyTo.author.avatar} + size={50} + moderation={replyTo.moderation?.avatar} + /> <View style={styles.replyToPost}> <Text type="xl-medium" style={[pal.text]}> {sanitizeDisplayName( @@ -99,7 +103,7 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) { {replyTo.text} </Text> </View> - {images && ( + {images && !replyTo.moderation?.embed.blur && ( <ComposerReplyToImages images={images} showFull={showFull} /> )} </View> diff --git a/src/view/com/composer/select-language/SuggestedLanguage.tsx b/src/view/com/composer/select-language/SuggestedLanguage.tsx index 987d89d36..0bf62ae0d 100644 --- a/src/view/com/composer/select-language/SuggestedLanguage.tsx +++ b/src/view/com/composer/select-language/SuggestedLanguage.tsx @@ -23,7 +23,9 @@ const onIdle = globalThis.requestIdleCallback || (cb => setTimeout(cb, 1)) const cancelIdle = globalThis.cancelIdleCallback || clearTimeout export function SuggestedLanguage({text}: {text: string}) { - const [suggestedLanguage, setSuggestedLanguage] = useState<string>() + const [suggestedLanguage, setSuggestedLanguage] = useState< + string | undefined + >() const langPrefs = useLanguagePrefs() const setLangPrefs = useLanguagePrefsApi() const pal = usePalette('default') @@ -40,14 +42,7 @@ export function SuggestedLanguage({text}: {text: string}) { } const idle = onIdle(() => { - // Only select languages that have a high confidence and convert to code2 - const result = lande(textTrimmed).filter( - ([lang, value]) => value >= 0.97 && code3ToCode2Strict(lang), - ) - - setSuggestedLanguage( - result.length > 0 ? code3ToCode2Strict(result[0][0]) : undefined, - ) + setSuggestedLanguage(guessLanguage(textTrimmed)) }) return () => cancelIdle(idle) @@ -99,3 +94,22 @@ const styles = StyleSheet.create({ marginBottom: 10, }, }) + +/** + * This function is using the lande language model to attempt to detect the language + * We want to only make suggestions when we feel a high degree of certainty + * The magic numbers are based on debugging sessions against some test strings + */ +function guessLanguage(text: string): string | undefined { + const scores = lande(text).filter(([_lang, value]) => value >= 0.0002) + // if the model has multiple items with a score higher than 0.0002, it isn't certain enough + if (scores.length !== 1) { + return undefined + } + const [lang, value] = scores[0] + // if the model doesn't give a score of 0.97 or above, it isn't certain enough + if (value < 0.97) { + return undefined + } + return code3ToCode2Strict(lang) +} diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index f2012a630..af0d18743 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -316,7 +316,6 @@ function getImageFromUri( const type = item.type if (type === 'text/plain') { - console.log('hit') item.getAsString(async itemString => { if (isUriImage(itemString)) { const response = await fetch(itemString) diff --git a/src/view/com/feeds/ProfileFeedgens.tsx b/src/view/com/feeds/ProfileFeedgens.tsx index 96a04bad0..e9cf9e535 100644 --- a/src/view/com/feeds/ProfileFeedgens.tsx +++ b/src/view/com/feeds/ProfileFeedgens.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {Dimensions, StyleProp, StyleSheet, View, ViewStyle} from 'react-native' +import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' import {useQueryClient} from '@tanstack/react-query' import {List, ListRef} from '../util/List' import {FeedSourceCardLoaded} from './FeedSourceCard' @@ -180,9 +180,7 @@ export const ProfileFeedgens = React.forwardRef< refreshing={isPTRing} onRefresh={onRefresh} headerOffset={headerOffset} - contentContainerStyle={{ - minHeight: Dimensions.get('window').height * 1.5, - }} + contentContainerStyle={isNative && {paddingBottom: headerOffset + 100}} indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'} removeClippedSubviews={true} // @ts-ignore our .web version only -prf diff --git a/src/view/com/lists/ProfileLists.tsx b/src/view/com/lists/ProfileLists.tsx index ba3e95b54..a47b25bed 100644 --- a/src/view/com/lists/ProfileLists.tsx +++ b/src/view/com/lists/ProfileLists.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {Dimensions, StyleProp, StyleSheet, View, ViewStyle} from 'react-native' +import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' import {useQueryClient} from '@tanstack/react-query' import {List, ListRef} from '../util/List' import {ListCard} from './ListCard' @@ -182,9 +182,9 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>( refreshing={isPTRing} onRefresh={onRefresh} headerOffset={headerOffset} - contentContainerStyle={{ - minHeight: Dimensions.get('window').height * 1.5, - }} + contentContainerStyle={ + isNative && {paddingBottom: headerOffset + 100} + } indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'} removeClippedSubviews={true} // @ts-ignore our .web version only -prf diff --git a/src/view/com/modals/AltImage.tsx b/src/view/com/modals/AltImage.tsx index 5156511d6..7671c29c8 100644 --- a/src/view/com/modals/AltImage.tsx +++ b/src/view/com/modals/AltImage.tsx @@ -4,7 +4,9 @@ import { StyleSheet, TouchableOpacity, View, + TextInput as RNTextInput, useWindowDimensions, + ScrollView as RNScrollView, } from 'react-native' import {ScrollView, TextInput} from './util' import {Image} from 'expo-image' @@ -13,6 +15,7 @@ import {gradients, s} from 'lib/styles' import {enforceLen} from 'lib/strings/helpers' import {MAX_ALT_TEXT} from 'lib/constants' import {useTheme} from 'lib/ThemeContext' +import {useIsKeyboardVisible} from 'lib/hooks/useIsKeyboardVisible' import {Text} from '../util/text/Text' import LinearGradient from 'react-native-linear-gradient' import {isWeb} from 'platform/detection' @@ -34,6 +37,24 @@ export function Component({image}: Props) { const [altText, setAltText] = useState(image.altText) const windim = useWindowDimensions() const {closeModal} = useModalControls() + const inputRef = React.useRef<RNTextInput>(null) + const scrollViewRef = React.useRef<RNScrollView>(null) + const keyboardShown = useIsKeyboardVisible() + + // Autofocus hack when we open the modal. We have to wait for the animation to complete first + React.useEffect(() => { + setTimeout(() => { + inputRef.current?.focus() + }, 500) + }, []) + + // We'd rather be at the bottom here so that we can easily dismiss the modal instead of having to scroll + // (especially on android, it acts weird) + React.useEffect(() => { + if (keyboardShown[0]) { + scrollViewRef.current?.scrollToEnd() + } + }, [keyboardShown]) const imageStyles = useMemo<ImageStyle>(() => { const maxWidth = isWeb ? 450 : windim.width @@ -71,6 +92,7 @@ export function Component({image}: Props) { testID="altTextImageModal" style={[pal.view, styles.scrollContainer]} keyboardShouldPersistTaps="always" + ref={scrollViewRef} nativeID="imageAltText"> <View style={styles.scrollInner}> <View style={[pal.viewLight, styles.imageContainer]}> @@ -97,7 +119,8 @@ export function Component({image}: Props) { accessibilityLabel={_(msg`Image alt text`)} accessibilityHint="" accessibilityLabelledBy="imageAltText" - autoFocus + // @ts-ignore This is fine, type is weird on the BottomSheetTextInput + ref={inputRef} /> <View style={styles.buttonControls}> <TouchableOpacity diff --git a/src/view/com/modals/ProfilePreview.tsx b/src/view/com/modals/ProfilePreview.tsx index 77e68db70..88b0df71d 100644 --- a/src/view/com/modals/ProfilePreview.tsx +++ b/src/view/com/modals/ProfilePreview.tsx @@ -27,12 +27,12 @@ export function Component({did}: {did: string}) { data: profile, error: profileError, refetch: refetchProfile, - isFetching: isFetchingProfile, + isLoading: isLoadingProfile, } = useProfileQuery({ did: did, }) - if (isFetchingProfile || !moderationOpts) { + if (isLoadingProfile || !moderationOpts) { return ( <CenteredView style={[pal.view, s.flex1]}> <ProfileHeader diff --git a/src/view/com/modals/UserAddRemoveLists.tsx b/src/view/com/modals/UserAddRemoveLists.tsx index 23adbe1a8..8452f2513 100644 --- a/src/view/com/modals/UserAddRemoveLists.tsx +++ b/src/view/com/modals/UserAddRemoveLists.tsx @@ -1,5 +1,10 @@ import React, {useCallback} from 'react' -import {ActivityIndicator, StyleSheet, View} from 'react-native' +import { + ActivityIndicator, + StyleSheet, + useWindowDimensions, + View, +} from 'react-native' import {AppBskyGraphDefs as GraphDefs} from '@atproto/api' import {Text} from '../util/text/Text' import {UserAvatar} from '../util/UserAvatar' @@ -10,7 +15,7 @@ import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeHandle} from 'lib/strings/handles' import {s} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' -import {isWeb, isAndroid} from 'platform/detection' +import {isWeb, isAndroid, isMobileWeb} from 'platform/detection' import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useModalControls} from '#/state/modals' @@ -41,6 +46,7 @@ export function Component({ }) { const {closeModal} = useModalControls() const pal = usePalette('default') + const {height: screenHeight} = useWindowDimensions() const {_} = useLingui() const {data: memberships} = useDangerousListMembershipsQuery() @@ -48,6 +54,16 @@ export function Component({ closeModal() }, [closeModal]) + const listStyle = React.useMemo(() => { + if (isMobileWeb) { + return [pal.border, {height: screenHeight / 2}] + } else if (isWeb) { + return [pal.border, {height: screenHeight / 1.5}] + } + + return [pal.border, {flex: 1}] + }, [pal.border, screenHeight]) + return ( <View testID="userAddRemoveListsModal" style={s.hContentRegion}> <Text style={[styles.title, pal.text]}> @@ -68,7 +84,7 @@ export function Component({ onRemove={onRemove} /> )} - style={[styles.list, pal.border]} + style={listStyle} /> <View style={[styles.btns, pal.border]}> <Button diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx index 0dfac2a83..f037097df 100644 --- a/src/view/com/notifications/FeedItem.tsx +++ b/src/view/com/notifications/FeedItem.tsx @@ -167,9 +167,7 @@ let FeedItem = ({ icon = 'user-plus' iconStyle = [s.blue3 as FontAwesomeIconStyle] } else if (item.type === 'feedgen-like') { - action = item.subjectUri - ? _(msg`liked your custom feed '${new AtUri(item.subjectUri).rkey}'`) - : _(msg`liked your custom feed`) + action = _(msg`liked your custom feed`) icon = 'HeartIconSolid' iconStyle = [ s.likeColor as FontAwesomeIconStyle, diff --git a/src/view/com/pager/FeedsTabBar.web.tsx b/src/view/com/pager/FeedsTabBar.web.tsx index 385da5544..9fe03b7e9 100644 --- a/src/view/com/pager/FeedsTabBar.web.tsx +++ b/src/view/com/pager/FeedsTabBar.web.tsx @@ -88,11 +88,17 @@ function FeedsTabBarTablet( const navigation = useNavigation<NavigationProp>() const {headerMinimalShellTransform} = useMinimalShellMode() const {headerHeight} = useShellLayout() - const pinnedDisplayNames = hasSession ? feeds.map(f => f.displayName) : [] - const showFeedsLinkInTabBar = hasSession && !hasPinnedCustom - const items = showFeedsLinkInTabBar - ? pinnedDisplayNames.concat('Feeds ✨') - : pinnedDisplayNames + + const items = React.useMemo(() => { + if (!hasSession) return [] + + const pinnedNames = feeds.map(f => f.displayName) + + if (!hasPinnedCustom) { + return pinnedNames.concat('Feeds ✨') + } + return pinnedNames + }, [hasSession, hasPinnedCustom, feeds]) const onPressDiscoverFeeds = React.useCallback(() => { if (isWeb) { @@ -105,13 +111,13 @@ function FeedsTabBarTablet( const onSelect = React.useCallback( (index: number) => { - if (showFeedsLinkInTabBar && index === items.length - 1) { + if (hasSession && !hasPinnedCustom && index === items.length - 1) { onPressDiscoverFeeds() } else if (props.onSelect) { props.onSelect(index) } }, - [items.length, onPressDiscoverFeeds, props, showFeedsLinkInTabBar], + [items.length, onPressDiscoverFeeds, props, hasSession, hasPinnedCustom], ) return ( diff --git a/src/view/com/pager/FeedsTabBarMobile.tsx b/src/view/com/pager/FeedsTabBarMobile.tsx index b9959a6d9..4eba241ae 100644 --- a/src/view/com/pager/FeedsTabBarMobile.tsx +++ b/src/view/com/pager/FeedsTabBarMobile.tsx @@ -36,11 +36,17 @@ export function FeedsTabBar( const {feeds, hasPinnedCustom} = usePinnedFeedsInfos() const {headerHeight} = useShellLayout() const {headerMinimalShellTransform} = useMinimalShellMode() - const pinnedDisplayNames = hasSession ? feeds.map(f => f.displayName) : [] - const showFeedsLinkInTabBar = hasSession && !hasPinnedCustom - const items = showFeedsLinkInTabBar - ? pinnedDisplayNames.concat('Feeds ✨') - : pinnedDisplayNames + + const items = React.useMemo(() => { + if (!hasSession) return [] + + const pinnedNames = feeds.map(f => f.displayName) + + if (!hasPinnedCustom) { + return pinnedNames.concat('Feeds ✨') + } + return pinnedNames + }, [hasSession, hasPinnedCustom, feeds]) const onPressFeedsLink = React.useCallback(() => { if (isWeb) { @@ -53,13 +59,13 @@ export function FeedsTabBar( const onSelect = React.useCallback( (index: number) => { - if (showFeedsLinkInTabBar && index === items.length - 1) { + if (hasSession && !hasPinnedCustom && index === items.length - 1) { onPressFeedsLink() } else if (props.onSelect) { props.onSelect(index) } }, - [items.length, onPressFeedsLink, props, showFeedsLinkInTabBar], + [items.length, onPressFeedsLink, props, hasSession, hasPinnedCustom], ) const onPressAvi = React.useCallback(() => { diff --git a/src/view/com/pager/TabBar.tsx b/src/view/com/pager/TabBar.tsx index c3a95c5c0..dadcfcebd 100644 --- a/src/view/com/pager/TabBar.tsx +++ b/src/view/com/pager/TabBar.tsx @@ -78,7 +78,7 @@ export function TabBar({ return ( <PressableWithHover testID={`${testID}-selector-${i}`} - key={item} + key={`${item}-${i}`} onLayout={e => onItemLayout(e, i)} style={[styles.item, selected && indicatorStyle]} hoverStyle={pal.viewLight} diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 95fd5aefb..d11c2781b 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -40,6 +40,7 @@ import {useLingui} from '@lingui/react' import {useLanguagePrefs} from '#/state/preferences' import {useComposerControls} from '#/state/shell/composer' import {useModerationOpts} from '#/state/queries/preferences' +import {useOpenLink} from '#/state/preferences/in-app-browser' import {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow' import {ThreadPost} from '#/state/queries/post-thread' import {useSession} from '#/state/session' @@ -216,10 +217,11 @@ let PostThreadItemLoaded = ({ avatar: post.author.avatar, }, embed: post.embed, + moderation, }, onPost: onPostReply, }) - }, [openComposer, post, record, onPostReply]) + }, [openComposer, post, record, onPostReply, moderation]) const onPressShowMore = React.useCallback(() => { setLimitLines(false) @@ -250,13 +252,7 @@ let PostThreadItemLoaded = ({ <View testID={`postThreadItem-by-${post.author.handle}`} - style={[ - styles.outer, - styles.outerHighlighted, - rootUri === post.uri && styles.outerHighlightedRoot, - pal.border, - pal.view, - ]} + style={[styles.outer, styles.outerHighlighted, pal.border, pal.view]} accessible={false}> <PostSandboxWarning /> <View style={styles.layout}> @@ -707,17 +703,23 @@ function ExpandedPostDetails({ }) { const pal = usePalette('default') const {_} = useLingui() + const openLink = useOpenLink() + const onTranslatePress = React.useCallback( + () => openLink(translatorUrl), + [openLink, translatorUrl], + ) return ( <View style={[s.flexRow, s.mt2, s.mb10]}> <Text style={pal.textLight}>{niceDate(post.indexedAt)}</Text> {needsTranslation && ( <> <Text style={pal.textLight}> · </Text> - <Link href={translatorUrl} title={_(msg`Translate`)}> - <Text style={pal.link}> - <Trans>Translate</Trans> - </Text> - </Link> + <Text + style={pal.link} + title={_(msg`Translate`)} + onPress={onTranslatePress}> + <Trans>Translate</Trans> + </Text> </> )} </View> @@ -732,15 +734,10 @@ const useStyles = () => { paddingLeft: 8, }, outerHighlighted: { - borderTopWidth: 0, - paddingTop: 4, + paddingTop: 16, paddingLeft: 8, paddingRight: 8, }, - outerHighlightedRoot: { - borderTopWidth: 1, - paddingTop: 16, - }, noTopBorder: { borderTopWidth: 0, }, @@ -779,6 +776,7 @@ const useStyles = () => { }, postTextLargeContainer: { paddingHorizontal: 0, + paddingRight: 0, paddingBottom: 10, }, translateLink: { diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index f035c32ad..2f1c0d37b 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -122,9 +122,10 @@ function PostInner({ avatar: post.author.avatar, }, embed: post.embed, + moderation, }, }) - }, [openComposer, post, record]) + }, [openComposer, post, record, moderation]) const onPressShowMore = React.useCallback(() => { setLimitLines(false) diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index 225607ca9..920409ec6 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -135,9 +135,10 @@ let FeedItemInner = ({ avatar: post.author.avatar, }, embed: post.embed, + moderation, }, }) - }, [post, record, openComposer]) + }, [post, record, openComposer, moderation]) const outerStyles = [ styles.outer, diff --git a/src/view/com/util/List.web.tsx b/src/view/com/util/List.web.tsx index 3e81a8c37..29bad2db8 100644 --- a/src/view/com/util/List.web.tsx +++ b/src/view/com/util/List.web.tsx @@ -300,6 +300,9 @@ export const List = memo(React.forwardRef(ListImpl)) as <ItemT>( props: ListProps<ItemT> & {ref?: React.Ref<ListMethods>}, ) => React.ReactElement +// https://stackoverflow.com/questions/7944460/detect-safari-browser +const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent) + const styles = StyleSheet.create({ contentContainer: { borderLeftWidth: 1, @@ -313,7 +316,7 @@ const styles = StyleSheet.create({ }, row: { // @ts-ignore web only - contentVisibility: 'auto', + contentVisibility: isSafari ? '' : 'auto', // Safari support for this is buggy. }, minHeightViewport: { // @ts-ignore web only diff --git a/src/view/com/util/LoadingPlaceholder.tsx b/src/view/com/util/LoadingPlaceholder.tsx index 8941bfb9c..6dfe12598 100644 --- a/src/view/com/util/LoadingPlaceholder.tsx +++ b/src/view/com/util/LoadingPlaceholder.tsx @@ -67,28 +67,36 @@ export function PostLoadingPlaceholder({ <LoadingPlaceholder width="95%" height={6} style={{marginBottom: 8}} /> <LoadingPlaceholder width="80%" height={6} style={{marginBottom: 11}} /> <View style={styles.postCtrls}> - <View style={[styles.postCtrl, {paddingLeft: 0}]}> - <CommentBottomArrow - style={[{color: theme.palette.default.icon, marginTop: 1}]} - strokeWidth={3} - size={15} - /> + <View style={styles.postCtrl}> + <View style={[styles.postBtn, {paddingLeft: 0}]}> + <CommentBottomArrow + style={[{color: theme.palette.default.icon, marginTop: 1}]} + strokeWidth={3} + size={15} + /> + </View> + </View> + <View style={styles.postCtrl}> + <View style={styles.postBtn}> + <RepostIcon + style={{color: theme.palette.default.icon}} + strokeWidth={3} + size={20} + /> + </View> </View> <View style={styles.postCtrl}> - <RepostIcon - style={{color: theme.palette.default.icon}} - strokeWidth={3} - size={20} - /> + <View style={styles.postBtn}> + <HeartIcon + style={{color: theme.palette.default.icon} as ViewStyle} + size={16} + strokeWidth={3} + /> + </View> </View> <View style={styles.postCtrl}> - <HeartIcon - style={{color: theme.palette.default.icon} as ViewStyle} - size={16} - strokeWidth={3} - /> + <View style={styles.postBtn} /> </View> - <View style={{width: 30, height: 30}} /> </View> </View> </View> @@ -279,6 +287,9 @@ const styles = StyleSheet.create({ justifyContent: 'space-between', }, postCtrl: { + flex: 1, + }, + postBtn: { padding: 5, flex: 1, flexDirection: 'row', diff --git a/src/view/com/util/UserInfoText.tsx b/src/view/com/util/UserInfoText.tsx index e5d2ceb03..9cb9997f6 100644 --- a/src/view/com/util/UserInfoText.tsx +++ b/src/view/com/util/UserInfoText.tsx @@ -9,6 +9,7 @@ import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeHandle} from 'lib/strings/handles' import {makeProfileLink} from 'lib/routes/links' import {useProfileQuery} from '#/state/queries/profile' +import {STALE} from '#/state/queries' export function UserInfoText({ type = 'md', @@ -29,7 +30,10 @@ export function UserInfoText({ attr = attr || 'handle' failed = failed || 'user' - const {data: profile, isError} = useProfileQuery({did}) + const {data: profile, isError} = useProfileQuery({ + did, + staleTime: STALE.INFINITY, + }) let inner if (isError) { diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx index 940f39057..e56c88d2c 100644 --- a/src/view/com/util/forms/PostDropdownBtn.tsx +++ b/src/view/com/util/forms/PostDropdownBtn.tsx @@ -1,5 +1,5 @@ import React, {memo} from 'react' -import {Linking, StyleProp, View, ViewStyle} from 'react-native' +import {StyleProp, View, ViewStyle} from 'react-native' import Clipboard from '@react-native-clipboard/clipboard' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import { @@ -24,6 +24,7 @@ import {usePostDeleteMutation} from '#/state/queries/post' import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads' import {useLanguagePrefs} from '#/state/preferences' import {useHiddenPosts, useHiddenPostsApi} from '#/state/preferences' +import {useOpenLink} from '#/state/preferences/in-app-browser' import {logger} from '#/logger' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -61,6 +62,7 @@ let PostDropdownBtn = ({ const postDeleteMutation = usePostDeleteMutation() const hiddenPosts = useHiddenPosts() const {hidePost} = useHiddenPostsApi() + const openLink = useOpenLink() const rootUri = record.reply?.root?.uri || postUri const isThreadMuted = mutedThreads.includes(rootUri) @@ -111,8 +113,8 @@ let PostDropdownBtn = ({ }, [_, richText]) const onOpenTranslate = React.useCallback(() => { - Linking.openURL(translatorUrl) - }, [translatorUrl]) + openLink(translatorUrl) + }, [openLink, translatorUrl]) const onHidePost = React.useCallback(() => { hidePost({uri: postUri}) diff --git a/src/view/com/util/images/Gallery.tsx b/src/view/com/util/images/Gallery.tsx index e7110372c..7de3b093a 100644 --- a/src/view/com/util/images/Gallery.tsx +++ b/src/view/com/util/images/Gallery.tsx @@ -4,6 +4,7 @@ import {StyleSheet, Text, Pressable, View} from 'react-native' import {Image} from 'expo-image' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {isWeb} from 'platform/detection' type EventFunction = (index: number) => void @@ -70,8 +71,10 @@ const styles = StyleSheet.create({ paddingHorizontal: 6, paddingVertical: 3, position: 'absolute', - left: 8, - bottom: 8, + // Related to margin/gap hack. This keeps the alt label in the same position + // on all platforms + left: isWeb ? 8 : 5, + bottom: isWeb ? 8 : 5, }, alt: { color: 'white', diff --git a/src/view/com/util/images/ImageLayoutGrid.tsx b/src/view/com/util/images/ImageLayoutGrid.tsx index 23e807b6a..ba6c04f50 100644 --- a/src/view/com/util/images/ImageLayoutGrid.tsx +++ b/src/view/com/util/images/ImageLayoutGrid.tsx @@ -2,6 +2,7 @@ import React from 'react' import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' import {AppBskyEmbedImages} from '@atproto/api' import {GalleryItem} from './Gallery' +import {isWeb} from 'platform/detection' interface ImageLayoutGridProps { images: AppBskyEmbedImages.ViewImage[] @@ -47,10 +48,10 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) { case 3: return ( <View style={styles.flexRow}> - <View style={{flex: 2, aspectRatio: 1}}> + <View style={styles.threeSingle}> <GalleryItem {...props} index={0} imageStyle={styles.image} /> </View> - <View style={{flex: 1}}> + <View style={styles.threeDouble}> <View style={styles.smallItem}> <GalleryItem {...props} index={1} imageStyle={styles.image} /> </View> @@ -88,18 +89,38 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) { } } -// This is used to compute margins (rather than flexbox gap) due to Yoga bugs: +// On web we use margin to calculate gap, as aspectRatio does not properly size +// all images on web. On native though we cannot rely on margin, since the +// negative margin interferes with the swipe controls on pagers. // https://github.com/facebook/yoga/issues/1418 +// https://github.com/bluesky-social/social-app/issues/2601 const IMAGE_GAP = 5 const styles = StyleSheet.create({ - container: { - marginHorizontal: -IMAGE_GAP / 2, - marginVertical: -IMAGE_GAP / 2, + container: isWeb + ? { + marginHorizontal: -IMAGE_GAP / 2, + marginVertical: -IMAGE_GAP / 2, + } + : { + gap: IMAGE_GAP, + }, + flexRow: { + flexDirection: 'row', + gap: isWeb ? undefined : IMAGE_GAP, }, - flexRow: {flexDirection: 'row'}, smallItem: {flex: 1, aspectRatio: 1}, - image: { - margin: IMAGE_GAP / 2, + image: isWeb + ? { + margin: IMAGE_GAP / 2, + } + : {}, + threeSingle: { + flex: 2, + aspectRatio: isWeb ? 1 : undefined, + }, + threeDouble: { + flex: 1, + gap: isWeb ? undefined : IMAGE_GAP, }, }) diff --git a/src/view/com/util/load-latest/LoadLatestBtn.tsx b/src/view/com/util/load-latest/LoadLatestBtn.tsx index 970d3a73a..5fad11760 100644 --- a/src/view/com/util/load-latest/LoadLatestBtn.tsx +++ b/src/view/com/util/load-latest/LoadLatestBtn.tsx @@ -10,6 +10,7 @@ import Animated from 'react-native-reanimated' const AnimatedTouchableOpacity = Animated.createAnimatedComponent(TouchableOpacity) import {isWeb} from 'platform/detection' +import {useSession} from 'state/session' export function LoadLatestBtn({ onPress, @@ -21,9 +22,14 @@ export function LoadLatestBtn({ showIndicator: boolean }) { const pal = usePalette('default') - const {isDesktop, isTablet, isMobile} = useWebMediaQueries() + const {hasSession} = useSession() + const {isDesktop, isTablet, isMobile, isTabletOrMobile} = useWebMediaQueries() const {fabMinimalShellTransform} = useMinimalShellMode() + // Adjust height of the fab if we have a session only on mobile web. If we don't have a session, we want to adjust + // it on both tablet and mobile since we are showing the bottom bar (see createNativeStackNavigatorWithAuth) + const showBottomBar = hasSession ? isMobile : isTabletOrMobile + return ( <AnimatedTouchableOpacity style={[ @@ -32,7 +38,7 @@ export function LoadLatestBtn({ isTablet && styles.loadLatestTablet, pal.borderDark, pal.view, - isMobile && fabMinimalShellTransform, + showBottomBar && fabMinimalShellTransform, ]} onPress={onPress} hitSlop={HITSLOP_20} diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index a6d7e38c3..249111a04 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -149,7 +149,7 @@ let PostCtrls = ({ ) : undefined} </TouchableOpacity> </View> - <View style={[styles.ctrl]}> + <View style={styles.ctrl}> <RepostButton big={big} isReposted={!!post.viewer?.repost} @@ -194,19 +194,19 @@ let PostCtrls = ({ </TouchableOpacity> </View> {big ? undefined : ( - <PostDropdownBtn - testID="postDropdownBtn" - postAuthor={post.author} - postCid={post.cid} - postUri={post.uri} - record={record} - richText={richText} - showAppealLabelItem={showAppealLabelItem} - style={styles.btnPad} - /> + <View style={styles.ctrl}> + <PostDropdownBtn + testID="postDropdownBtn" + postAuthor={post.author} + postCid={post.cid} + postUri={post.uri} + record={record} + richText={richText} + showAppealLabelItem={showAppealLabelItem} + style={styles.btnPad} + /> + </View> )} - {/* used for adding pad to the right side */} - <View /> </View> ) } |