diff options
Diffstat (limited to 'src/view/com')
-rw-r--r-- | src/view/com/notifications/Feed.tsx | 5 | ||||
-rw-r--r-- | src/view/com/pager/FeedsTabBar.web.tsx | 8 | ||||
-rw-r--r-- | src/view/com/pager/FeedsTabBarMobile.tsx | 13 | ||||
-rw-r--r-- | src/view/com/post-thread/PostThread.tsx | 2 | ||||
-rw-r--r-- | src/view/com/post-thread/PostThreadItem.tsx | 9 | ||||
-rw-r--r-- | src/view/com/post/Post.tsx | 22 | ||||
-rw-r--r-- | src/view/com/posts/FeedItem.tsx | 57 | ||||
-rw-r--r-- | src/view/com/posts/FeedSlice.tsx | 2 | ||||
-rw-r--r-- | src/view/com/posts/FollowingEmptyState.tsx | 2 | ||||
-rw-r--r-- | src/view/com/posts/MultiFeed.tsx | 256 | ||||
-rw-r--r-- | src/view/com/util/Link.tsx | 15 | ||||
-rw-r--r-- | src/view/com/util/LoadingPlaceholder.tsx | 54 | ||||
-rw-r--r-- | src/view/com/util/SimpleViewHeader.tsx | 105 | ||||
-rw-r--r-- | src/view/com/util/UserAvatar.tsx | 2 | ||||
-rw-r--r-- | src/view/com/util/forms/SearchInput.tsx | 104 | ||||
-rw-r--r-- | src/view/com/util/load-latest/LoadLatestBtn.tsx | 87 | ||||
-rw-r--r-- | src/view/com/util/load-latest/LoadLatestBtn.web.tsx | 109 | ||||
-rw-r--r-- | src/view/com/util/load-latest/LoadLatestBtnMobile.tsx | 69 |
18 files changed, 418 insertions, 503 deletions
diff --git a/src/view/com/notifications/Feed.tsx b/src/view/com/notifications/Feed.tsx index d457d7136..4ca22282d 100644 --- a/src/view/com/notifications/Feed.tsx +++ b/src/view/com/notifications/Feed.tsx @@ -21,11 +21,13 @@ export const Feed = observer(function Feed({ scrollElRef, onPressTryAgain, onScroll, + ListHeaderComponent, }: { view: NotificationsFeedModel scrollElRef?: MutableRefObject<FlatList<any> | null> onPressTryAgain?: () => void onScroll?: OnScrollCb + ListHeaderComponent?: () => JSX.Element }) { const pal = usePalette('default') const [isPTRing, setIsPTRing] = React.useState(false) @@ -142,6 +144,7 @@ export const Feed = observer(function Feed({ data={data} keyExtractor={item => item._reactKey} renderItem={renderItem} + ListHeaderComponent={ListHeaderComponent} ListFooterComponent={FeedFooter} refreshControl={ <RefreshControl @@ -156,6 +159,8 @@ export const Feed = observer(function Feed({ onScroll={onScroll} scrollEventThrottle={100} contentContainerStyle={s.contentContainer} + // @ts-ignore our .web version only -prf + desktopFixedHeight /> ) : null} </View> diff --git a/src/view/com/pager/FeedsTabBar.web.tsx b/src/view/com/pager/FeedsTabBar.web.tsx index 0083e953b..02aa623cc 100644 --- a/src/view/com/pager/FeedsTabBar.web.tsx +++ b/src/view/com/pager/FeedsTabBar.web.tsx @@ -12,15 +12,17 @@ import {FeedsTabBar as FeedsTabBarMobile} from './FeedsTabBarMobile' export const FeedsTabBar = observer(function FeedsTabBarImpl( props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void}, ) { - const {isMobile} = useWebMediaQueries() + const {isMobile, isTablet} = useWebMediaQueries() if (isMobile) { return <FeedsTabBarMobile {...props} /> + } else if (isTablet) { + return <FeedsTabBarTablet {...props} /> } else { - return <FeedsTabBarDesktop {...props} /> + return null } }) -const FeedsTabBarDesktop = observer(function FeedsTabBarDesktopImpl( +const FeedsTabBarTablet = observer(function FeedsTabBarTabletImpl( props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void}, ) { const store = useStores() diff --git a/src/view/com/pager/FeedsTabBarMobile.tsx b/src/view/com/pager/FeedsTabBarMobile.tsx index 5ce2906b3..30a712541 100644 --- a/src/view/com/pager/FeedsTabBarMobile.tsx +++ b/src/view/com/pager/FeedsTabBarMobile.tsx @@ -9,8 +9,8 @@ import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' import {Link} from '../util/Link' import {Text} from '../util/text/Text' -import {CogIcon} from 'lib/icons' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome' import {s} from 'lib/styles' import {HITSLOP_10} from 'lib/constants' @@ -67,12 +67,15 @@ export const FeedsTabBar = observer(function FeedsTabBarImpl( </Text> <View style={[pal.view]}> <Link - href="/settings/saved-feeds" + href="/settings/home-feed" hitSlop={HITSLOP_10} accessibilityRole="button" - accessibilityLabel="Edit Saved Feeds" - accessibilityHint="Opens screen to edit Saved Feeds"> - <CogIcon size={21} strokeWidth={2} style={pal.textLight} /> + accessibilityLabel="Home Feed Preferences" + accessibilityHint=""> + <FontAwesomeIcon + icon="sliders" + style={pal.textLight as FontAwesomeIconStyle} + /> </Link> </View> </View> diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index 7a5a45771..1cc177d17 100644 --- a/src/view/com/post-thread/PostThread.tsx +++ b/src/view/com/post-thread/PostThread.tsx @@ -357,6 +357,8 @@ export const PostThread = observer(function PostThread({ } onScrollToIndexFailed={onScrollToIndexFailed} style={s.hContentRegion} + // @ts-ignore our .web version only -prf + desktopFixedHeight /> ) }) diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 5b5fee0ca..37c7ece47 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -483,15 +483,6 @@ export const PostThreadItem = observer(function PostThreadItem({ /> </ContentHider> )} - {needsTranslation && ( - <View style={[pal.borderDark, styles.translateLink]}> - <Link href={translatorUrl} title="Translate"> - <Text type="sm" style={pal.link}> - Translate this post - </Text> - </Link> - </View> - )} <PostCtrls itemUri={itemUri} itemCid={itemCid} diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index 0855f25bf..d7559e3c4 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -1,4 +1,4 @@ -import React, {useState, useMemo} from 'react' +import React, {useState} from 'react' import { ActivityIndicator, Linking, @@ -28,7 +28,7 @@ import {PreviewableUserAvatar} from '../util/UserAvatar' import {useStores} from 'state/index' import {s, colors} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' -import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers' +import {getTranslatorLink} from '../../../locale/helpers' import {makeProfileLink} from 'lib/routes/links' export const Post = observer(function PostImpl({ @@ -116,12 +116,6 @@ const PostLoaded = observer(function PostLoadedImpl({ } const translatorUrl = getTranslatorLink(record?.text || '') - const needsTranslation = useMemo( - () => - store.preferences.contentLanguages.length > 0 && - !isPostInLanguage(item.post, store.preferences.contentLanguages), - [item.post, store.preferences.contentLanguages], - ) const onPressReply = React.useCallback(() => { store.shell.openComposer({ @@ -256,15 +250,6 @@ const PostLoaded = observer(function PostLoadedImpl({ /> </ContentHider> ) : null} - {needsTranslation && ( - <View style={[pal.borderDark, styles.translateLink]}> - <Link href={translatorUrl} title="Translate"> - <Text type="sm" style={pal.link}> - Translate this post - </Text> - </Link> - </View> - )} </ContentHider> <PostCtrls itemUri={itemUri} @@ -322,9 +307,6 @@ const styles = StyleSheet.create({ alignItems: 'center', flexWrap: 'wrap', }, - translateLink: { - marginBottom: 12, - }, replyLine: { position: 'absolute', left: 36, diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index bc7b7a7e6..59ab28d72 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -8,6 +8,7 @@ import { FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' import {PostsFeedItemModel} from 'state/models/feeds/post' +import {FeedSourceInfo} from 'lib/api/feed/types' import {Link, DesktopWebTextLink} from '../util/Link' import {Text} from '../util/text/Text' import {UserInfoText} from '../util/UserInfoText' @@ -26,17 +27,19 @@ import {usePalette} from 'lib/hooks/usePalette' import {useAnalytics} from 'lib/analytics/analytics' import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeHandle} from 'lib/strings/handles' -import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers' +import {getTranslatorLink} from '../../../locale/helpers' import {makeProfileLink} from 'lib/routes/links' import {isEmbedByEmbedder} from 'lib/embeds' export const FeedItem = observer(function FeedItemImpl({ item, + source, isThreadChild, isThreadLastChild, isThreadParent, }: { item: PostsFeedItemModel + source?: FeedSourceInfo isThreadChild?: boolean isThreadLastChild?: boolean isThreadParent?: boolean @@ -62,12 +65,6 @@ export const FeedItem = observer(function FeedItemImpl({ return urip.hostname }, [record?.reply]) const translatorUrl = getTranslatorLink(record?.text || '') - const needsTranslation = useMemo( - () => - store.preferences.contentLanguages.length > 0 && - !isPostInLanguage(item.post, store.preferences.contentLanguages), - [item.post, store.preferences.contentLanguages], - ) const onPressReply = React.useCallback(() => { track('FeedItem:PostReply') @@ -179,7 +176,27 @@ export const FeedItem = observer(function FeedItemImpl({ </View> <View style={{paddingTop: 12}}> - {item.reasonRepost && ( + {source ? ( + <Link + title={sanitizeDisplayName(source.displayName)} + href={source.uri}> + <Text + type="sm-bold" + style={pal.textLight} + lineHeight={1.2} + numberOfLines={1}> + From{' '} + <DesktopWebTextLink + type="sm-bold" + style={pal.textLight} + lineHeight={1.2} + numberOfLines={1} + text={sanitizeDisplayName(source.displayName)} + href={source.uri} + /> + </Text> + </Link> + ) : item.reasonRepost ? ( <Link style={styles.includeReason} href={makeProfileLink(item.reasonRepost.by)} @@ -188,10 +205,10 @@ export const FeedItem = observer(function FeedItemImpl({ )}> <FontAwesomeIcon icon="retweet" - style={[ - styles.includeReasonIcon, - {color: pal.colors.textLight} as FontAwesomeIconStyle, - ]} + style={{ + marginRight: 4, + color: pal.colors.textLight, + }} /> <Text type="sm-bold" @@ -212,7 +229,7 @@ export const FeedItem = observer(function FeedItemImpl({ /> </Text> </Link> - )} + ) : null} </View> </View> @@ -304,15 +321,6 @@ export const FeedItem = observer(function FeedItemImpl({ /> </ContentHider> ) : null} - {needsTranslation && ( - <View style={[pal.borderDark, styles.translateLink]}> - <Link href={translatorUrl} title="Translate"> - <Text type="sm" style={pal.link}> - Translate this post - </Text> - </Link> - </View> - )} </ContentHider> <PostCtrls itemUri={itemUri} @@ -362,12 +370,9 @@ const styles = StyleSheet.create({ includeReason: { flexDirection: 'row', marginTop: 2, - marginBottom: 4, + marginBottom: 2, marginLeft: -20, }, - includeReasonIcon: { - marginRight: 4, - }, layout: { flexDirection: 'row', marginTop: 1, diff --git a/src/view/com/posts/FeedSlice.tsx b/src/view/com/posts/FeedSlice.tsx index 47313ee27..1d26f6cbd 100644 --- a/src/view/com/posts/FeedSlice.tsx +++ b/src/view/com/posts/FeedSlice.tsx @@ -28,6 +28,7 @@ export const FeedSlice = observer(function FeedSliceImpl({ <FeedItem key={slice.items[0]._reactKey} item={slice.items[0]} + source={slice.source} isThreadParent={slice.isThreadParentAt(0)} isThreadChild={slice.isThreadChildAt(0)} /> @@ -55,6 +56,7 @@ export const FeedSlice = observer(function FeedSliceImpl({ <FeedItem key={item._reactKey} item={item} + source={i === 0 ? slice.source : undefined} isThreadParent={slice.isThreadParentAt(i)} isThreadChild={slice.isThreadChildAt(i)} isThreadLastChild={ diff --git a/src/view/com/posts/FollowingEmptyState.tsx b/src/view/com/posts/FollowingEmptyState.tsx index 4491b2526..a73ffb68b 100644 --- a/src/view/com/posts/FollowingEmptyState.tsx +++ b/src/view/com/posts/FollowingEmptyState.tsx @@ -28,7 +28,7 @@ export function FollowingEmptyState() { }, [navigation]) const onPressDiscoverFeeds = React.useCallback(() => { - navigation.navigate('DiscoverFeeds') + navigation.navigate('Feeds') }, [navigation]) return ( diff --git a/src/view/com/posts/MultiFeed.tsx b/src/view/com/posts/MultiFeed.tsx deleted file mode 100644 index 9c8f4f246..000000000 --- a/src/view/com/posts/MultiFeed.tsx +++ /dev/null @@ -1,256 +0,0 @@ -import React, {MutableRefObject} from 'react' -import {observer} from 'mobx-react-lite' -import { - ActivityIndicator, - RefreshControl, - StyleProp, - StyleSheet, - View, - ViewStyle, -} from 'react-native' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {FlatList} from '../util/Views' -import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder' -import {ErrorMessage} from '../util/error/ErrorMessage' -import {PostsMultiFeedModel, MultiFeedItem} from 'state/models/feeds/multi-feed' -import {FeedSlice} from './FeedSlice' -import {Text} from '../util/text/Text' -import {Link} from '../util/Link' -import {UserAvatar} from '../util/UserAvatar' -import {OnScrollCb} from 'lib/hooks/useOnMainScroll' -import {s} from 'lib/styles' -import {useAnalytics} from 'lib/analytics/analytics' -import {usePalette} from 'lib/hooks/usePalette' -import {useTheme} from 'lib/ThemeContext' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {CogIcon} from 'lib/icons' - -export const MultiFeed = observer(function Feed({ - multifeed, - style, - scrollElRef, - onScroll, - scrollEventThrottle, - testID, - headerOffset = 0, - extraData, -}: { - multifeed: PostsMultiFeedModel - style?: StyleProp<ViewStyle> - scrollElRef?: MutableRefObject<FlatList<any> | null> - onPressTryAgain?: () => void - onScroll?: OnScrollCb - scrollEventThrottle?: number - renderEmptyState?: () => JSX.Element - testID?: string - headerOffset?: number - extraData?: any -}) { - const pal = usePalette('default') - const theme = useTheme() - const {isMobile} = useWebMediaQueries() - const {track} = useAnalytics() - const [isRefreshing, setIsRefreshing] = React.useState(false) - - // events - // = - - const onRefresh = React.useCallback(async () => { - track('MultiFeed:onRefresh') - setIsRefreshing(true) - try { - await multifeed.refresh() - } catch (err) { - multifeed.rootStore.log.error('Failed to refresh posts feed', err) - } - setIsRefreshing(false) - }, [multifeed, track, setIsRefreshing]) - - const onEndReached = React.useCallback(async () => { - track('MultiFeed:onEndReached') - try { - await multifeed.loadMore() - } catch (err) { - multifeed.rootStore.log.error('Failed to load more posts', err) - } - }, [multifeed, track]) - - // rendering - // = - - const renderItem = React.useCallback( - ({item}: {item: MultiFeedItem}) => { - if (item.type === 'header') { - if (!isMobile) { - return ( - <> - <View style={[pal.view, pal.border, styles.headerDesktop]}> - <Text type="2xl-bold" style={pal.text}> - My Feeds - </Text> - <Link href="/settings/saved-feeds"> - <CogIcon strokeWidth={1.5} style={pal.icon} size={28} /> - </Link> - </View> - <DiscoverLink /> - </> - ) - } - return ( - <> - <View style={[styles.header, pal.border]} /> - <DiscoverLink /> - </> - ) - } else if (item.type === 'feed-header') { - return ( - <View style={styles.feedHeader}> - <UserAvatar type="algo" avatar={item.avatar} size={28} /> - <Text type="title-lg" style={[pal.text, styles.feedHeaderTitle]}> - {item.title} - </Text> - </View> - ) - } else if (item.type === 'feed-slice') { - return <FeedSlice slice={item.slice} /> - } else if (item.type === 'feed-loading') { - return <PostFeedLoadingPlaceholder /> - } else if (item.type === 'feed-error') { - return <ErrorMessage message={item.error} /> - } else if (item.type === 'feed-footer') { - return ( - <Link - href={item.uri} - style={[styles.feedFooter, pal.border, pal.view]}> - <Text type="lg" style={pal.link}> - See more from {item.title} - </Text> - <FontAwesomeIcon - icon="angle-right" - size={18} - color={pal.colors.link} - /> - </Link> - ) - } else if (item.type === 'footer') { - return <DiscoverLink /> - } - return null - }, - [pal, isMobile], - ) - - const ListFooter = React.useCallback( - () => - multifeed.isLoading && !isRefreshing ? ( - <View style={styles.loadMore}> - <ActivityIndicator color={pal.colors.text} /> - </View> - ) : ( - <View /> - ), - [multifeed.isLoading, isRefreshing, pal], - ) - - return ( - <View testID={testID} style={style}> - {multifeed.items.length > 0 && ( - <FlatList - testID={testID ? `${testID}-flatlist` : undefined} - ref={scrollElRef} - data={multifeed.items} - keyExtractor={item => item._reactKey} - renderItem={renderItem} - ListFooterComponent={ListFooter} - refreshControl={ - <RefreshControl - refreshing={isRefreshing} - onRefresh={onRefresh} - tintColor={pal.colors.text} - titleColor={pal.colors.text} - progressViewOffset={headerOffset} - /> - } - contentContainerStyle={s.contentContainer} - style={[{paddingTop: headerOffset}, pal.view, styles.container]} - onScroll={onScroll} - scrollEventThrottle={scrollEventThrottle} - indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'} - onEndReached={onEndReached} - onEndReachedThreshold={0.6} - removeClippedSubviews={true} - contentOffset={{x: 0, y: headerOffset * -1}} - extraData={extraData} - // @ts-ignore our .web version only -prf - desktopFixedHeight - /> - )} - </View> - ) -}) - -function DiscoverLink() { - const pal = usePalette('default') - return ( - <Link style={[styles.discoverLink, pal.viewLight]} href="/search/feeds"> - <FontAwesomeIcon icon="search" size={18} color={pal.colors.text} /> - <Text type="xl-medium" style={pal.text}> - Discover new feeds - </Text> - </Link> - ) -} - -const styles = StyleSheet.create({ - container: { - height: '100%', - }, - header: { - borderTopWidth: 1, - marginBottom: 4, - }, - headerDesktop: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - borderBottomWidth: 1, - marginBottom: 4, - paddingHorizontal: 16, - paddingVertical: 8, - }, - feedHeader: { - flexDirection: 'row', - gap: 8, - alignItems: 'center', - paddingHorizontal: 16, - paddingBottom: 8, - marginTop: 12, - }, - feedHeaderTitle: { - fontWeight: 'bold', - }, - feedFooter: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - paddingHorizontal: 16, - paddingVertical: 16, - marginBottom: 12, - borderTopWidth: 1, - borderBottomWidth: 1, - }, - discoverLink: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - borderRadius: 8, - paddingHorizontal: 14, - paddingVertical: 12, - marginHorizontal: 8, - marginVertical: 8, - gap: 8, - }, - loadMore: { - paddingTop: 10, - }, -}) diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx index 321b6ab63..d4df2bec4 100644 --- a/src/view/com/util/Link.tsx +++ b/src/view/com/util/Link.tsx @@ -26,6 +26,7 @@ import {useStores, RootStoreModel} from 'state/index' import {convertBskyAppUrlIfNeeded, isExternalUrl} from 'lib/strings/url-helpers' import {isAndroid, isDesktopWeb} from 'platform/detection' import {sanitizeUrl} from '@braintree/sanitize-url' +import {PressableWithHover} from './PressableWithHover' import FixedTouchableHighlight from '../pager/FixedTouchableHighlight' type Event = @@ -38,6 +39,7 @@ interface Props extends ComponentProps<typeof TouchableOpacity> { href?: string title?: string children?: React.ReactNode + hoverStyle?: StyleProp<ViewStyle> noFeedback?: boolean asAnchor?: boolean anchorNoUnderline?: boolean @@ -112,8 +114,9 @@ export const Link = observer(function Link({ props.accessibilityLabel = title } + const Com = props.hoverStyle ? PressableWithHover : Pressable return ( - <Pressable + <Com testID={testID} style={style} onPress={onPress} @@ -123,7 +126,7 @@ export const Link = observer(function Link({ href={asAnchor ? sanitizeUrl(href) : undefined} {...props}> {children ? children : <Text>{title || 'link'}</Text>} - </Pressable> + </Com> ) }) @@ -137,6 +140,7 @@ export const TextLink = observer(function TextLink({ lineHeight, dataSet, title, + onPress, }: { testID?: string type?: TypographyVariant @@ -154,9 +158,14 @@ export const TextLink = observer(function TextLink({ props.onPress = React.useCallback( (e?: Event) => { + if (onPress) { + e?.preventDefault?.() + // @ts-ignore function signature differs by platform -prf + return onPress() + } return onPressInner(store, navigation, sanitizeUrl(href), e) }, - [store, navigation, href], + [onPress, store, navigation, href], ) const hrefAttrs = useMemo(() => { const isExternal = isExternalUrl(href) diff --git a/src/view/com/util/LoadingPlaceholder.tsx b/src/view/com/util/LoadingPlaceholder.tsx index bf39fd50c..d7ab1be54 100644 --- a/src/view/com/util/LoadingPlaceholder.tsx +++ b/src/view/com/util/LoadingPlaceholder.tsx @@ -174,6 +174,60 @@ export function ProfileCardFeedLoadingPlaceholder() { ) } +export function FeedLoadingPlaceholder({ + style, +}: { + style?: StyleProp<ViewStyle> +}) { + const pal = usePalette('default') + return ( + <View + style={[ + {paddingHorizontal: 12, paddingVertical: 18, borderTopWidth: 1}, + pal.border, + style, + ]}> + <View style={[pal.view, {flexDirection: 'row', marginBottom: 10}]}> + <LoadingPlaceholder + width={36} + height={36} + style={[styles.avatar, {borderRadius: 6}]} + /> + <View style={[s.flex1]}> + <LoadingPlaceholder width={100} height={8} style={[s.mt5, s.mb10]} /> + <LoadingPlaceholder width={120} height={8} /> + </View> + </View> + <View style={{paddingHorizontal: 5}}> + <LoadingPlaceholder + width={260} + height={8} + style={{marginVertical: 12}} + /> + <LoadingPlaceholder width={120} height={8} /> + </View> + </View> + ) +} + +export function FeedFeedLoadingPlaceholder() { + return ( + <> + <FeedLoadingPlaceholder /> + <FeedLoadingPlaceholder /> + <FeedLoadingPlaceholder /> + <FeedLoadingPlaceholder /> + <FeedLoadingPlaceholder /> + <FeedLoadingPlaceholder /> + <FeedLoadingPlaceholder /> + <FeedLoadingPlaceholder /> + <FeedLoadingPlaceholder /> + <FeedLoadingPlaceholder /> + <FeedLoadingPlaceholder /> + </> + ) +} + const styles = StyleSheet.create({ loadingPlaceholder: { borderRadius: 6, diff --git a/src/view/com/util/SimpleViewHeader.tsx b/src/view/com/util/SimpleViewHeader.tsx new file mode 100644 index 000000000..4eff38a31 --- /dev/null +++ b/src/view/com/util/SimpleViewHeader.tsx @@ -0,0 +1,105 @@ +import React from 'react' +import {observer} from 'mobx-react-lite' +import { + StyleProp, + StyleSheet, + TouchableOpacity, + View, + ViewStyle, +} from 'react-native' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {useNavigation} from '@react-navigation/native' +import {CenteredView} from './Views' +import {useStores} from 'state/index' +import {usePalette} from 'lib/hooks/usePalette' +import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +import {useAnalytics} from 'lib/analytics/analytics' +import {NavigationProp} from 'lib/routes/types' + +const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20} + +export const SimpleViewHeader = observer(function SimpleViewHeaderImpl({ + showBackButton = true, + style, + children, +}: React.PropsWithChildren<{ + showBackButton?: boolean + style?: StyleProp<ViewStyle> +}>) { + const pal = usePalette('default') + const store = useStores() + const navigation = useNavigation<NavigationProp>() + const {track} = useAnalytics() + const {isMobile} = useWebMediaQueries() + const canGoBack = navigation.canGoBack() + + const onPressBack = React.useCallback(() => { + if (navigation.canGoBack()) { + navigation.goBack() + } else { + navigation.navigate('Home') + } + }, [navigation]) + + const onPressMenu = React.useCallback(() => { + track('ViewHeader:MenuButtonClicked') + store.shell.openDrawer() + }, [track, store]) + + const Container = isMobile ? View : CenteredView + return ( + <Container style={[styles.header, isMobile && styles.headerMobile, style]}> + {showBackButton ? ( + <TouchableOpacity + testID="viewHeaderDrawerBtn" + onPress={canGoBack ? onPressBack : onPressMenu} + hitSlop={BACK_HITSLOP} + style={canGoBack ? styles.backBtn : styles.backBtnWide} + accessibilityRole="button" + accessibilityLabel={canGoBack ? 'Back' : 'Menu'} + accessibilityHint=""> + {canGoBack ? ( + <FontAwesomeIcon + size={18} + icon="angle-left" + style={[styles.backIcon, pal.text]} + /> + ) : ( + <FontAwesomeIcon + size={18} + icon="bars" + style={[styles.backIcon, pal.textLight]} + /> + )} + </TouchableOpacity> + ) : null} + {children} + </Container> + ) +}) + +const styles = StyleSheet.create({ + header: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 18, + paddingVertical: 12, + width: '100%', + }, + headerMobile: { + paddingHorizontal: 12, + paddingVertical: 10, + }, + backBtn: { + width: 30, + height: 30, + }, + backBtnWide: { + width: 30, + height: 30, + paddingHorizontal: 6, + }, + backIcon: { + marginTop: 6, + }, +}) diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index 0f34f75aa..7a42ab4d3 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -118,7 +118,7 @@ export function UserAvatar({ return { width: size, height: size, - borderRadius: 8, + borderRadius: size > 32 ? 8 : 3, } } return { diff --git a/src/view/com/util/forms/SearchInput.tsx b/src/view/com/util/forms/SearchInput.tsx new file mode 100644 index 000000000..c1eb82bd4 --- /dev/null +++ b/src/view/com/util/forms/SearchInput.tsx @@ -0,0 +1,104 @@ +import React from 'react' +import { + StyleProp, + StyleSheet, + TextInput, + TouchableOpacity, + View, + ViewStyle, +} from 'react-native' +import { + FontAwesomeIcon, + FontAwesomeIconStyle, +} from '@fortawesome/react-native-fontawesome' +import {MagnifyingGlassIcon} from 'lib/icons' +import {useTheme} from 'lib/ThemeContext' +import {usePalette} from 'lib/hooks/usePalette' + +interface Props { + query: string + setIsInputFocused?: (v: boolean) => void + onChangeQuery: (v: string) => void + onPressCancelSearch: () => void + onSubmitQuery: () => void + style?: StyleProp<ViewStyle> +} +export function SearchInput({ + query, + setIsInputFocused, + onChangeQuery, + onPressCancelSearch, + onSubmitQuery, + style, +}: Props) { + const theme = useTheme() + const pal = usePalette('default') + const textInput = React.useRef<TextInput>(null) + + const onPressCancelSearchInner = React.useCallback(() => { + onPressCancelSearch() + textInput.current?.blur() + }, [onPressCancelSearch, textInput]) + + return ( + <View style={[pal.viewLight, styles.container, style]}> + <MagnifyingGlassIcon style={[pal.icon, styles.icon]} size={21} /> + <TextInput + testID="searchTextInput" + ref={textInput} + placeholder="Search" + placeholderTextColor={pal.colors.textLight} + selectTextOnFocus + returnKeyType="search" + value={query} + style={[pal.text, styles.input]} + keyboardAppearance={theme.colorScheme} + onFocus={() => setIsInputFocused?.(true)} + onBlur={() => setIsInputFocused?.(false)} + onChangeText={onChangeQuery} + onSubmitEditing={onSubmitQuery} + accessibilityRole="search" + accessibilityLabel="Search" + accessibilityHint="" + autoCorrect={false} + autoCapitalize="none" + /> + {query ? ( + <TouchableOpacity + onPress={onPressCancelSearchInner} + accessibilityRole="button" + accessibilityLabel="Clear search query" + accessibilityHint=""> + <FontAwesomeIcon + icon="xmark" + size={16} + style={pal.textLight as FontAwesomeIconStyle} + /> + </TouchableOpacity> + ) : undefined} + </View> + ) +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + borderRadius: 30, + paddingHorizontal: 12, + paddingVertical: 8, + }, + icon: { + marginRight: 6, + alignSelf: 'center', + }, + input: { + flex: 1, + fontSize: 17, + minWidth: 0, // overflow mitigation for firefox + }, + cancelBtn: { + paddingLeft: 10, + }, +}) diff --git a/src/view/com/util/load-latest/LoadLatestBtn.tsx b/src/view/com/util/load-latest/LoadLatestBtn.tsx index ae9cb9361..6b73edd4b 100644 --- a/src/view/com/util/load-latest/LoadLatestBtn.tsx +++ b/src/view/com/util/load-latest/LoadLatestBtn.tsx @@ -1 +1,86 @@ -export * from './LoadLatestBtnMobile' +import React from 'react' +import {StyleSheet, TouchableOpacity, View} from 'react-native' +import {observer} from 'mobx-react-lite' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {useSafeAreaInsets} from 'react-native-safe-area-context' +import {clamp} from 'lodash' +import {useStores} from 'state/index' +import {usePalette} from 'lib/hooks/usePalette' +import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +import {colors} from 'lib/styles' +import {HITSLOP_20} from 'lib/constants' + +export const LoadLatestBtn = observer(function LoadLatestBtnImpl({ + onPress, + label, + showIndicator, +}: { + onPress: () => void + label: string + showIndicator: boolean + minimalShellMode?: boolean // NOTE not used on mobile -prf +}) { + const store = useStores() + const pal = usePalette('default') + const {isDesktop, isTablet, isMobile} = useWebMediaQueries() + const safeAreaInsets = useSafeAreaInsets() + return ( + <TouchableOpacity + style={[ + styles.loadLatest, + isDesktop && styles.loadLatestDesktop, + isTablet && styles.loadLatestTablet, + pal.borderDark, + pal.view, + isMobile && + !store.shell.minimalShellMode && { + bottom: 60 + clamp(safeAreaInsets.bottom, 15, 30), + }, + ]} + onPress={onPress} + hitSlop={HITSLOP_20} + accessibilityRole="button" + accessibilityLabel={label} + accessibilityHint=""> + <FontAwesomeIcon icon="angle-up" color={pal.colors.text} size={19} /> + {showIndicator && <View style={[styles.indicator, pal.borderDark]} />} + </TouchableOpacity> + ) +}) + +const styles = StyleSheet.create({ + loadLatest: { + position: 'absolute', + left: 18, + bottom: 35, + borderWidth: 1, + width: 52, + height: 52, + borderRadius: 26, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + }, + loadLatestTablet: { + // @ts-ignore web only + left: '50vw', + // @ts-ignore web only -prf + transform: 'translateX(-282px)', + }, + loadLatestDesktop: { + // @ts-ignore web only + left: '50vw', + // @ts-ignore web only -prf + transform: 'translateX(-382px)', + }, + indicator: { + position: 'absolute', + top: 3, + right: 3, + backgroundColor: colors.blue3, + width: 12, + height: 12, + borderRadius: 6, + borderWidth: 1, + }, +}) diff --git a/src/view/com/util/load-latest/LoadLatestBtn.web.tsx b/src/view/com/util/load-latest/LoadLatestBtn.web.tsx deleted file mode 100644 index 83c696f7e..000000000 --- a/src/view/com/util/load-latest/LoadLatestBtn.web.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import React from 'react' -import {StyleSheet, TouchableOpacity} from 'react-native' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {Text} from '../text/Text' -import {usePalette} from 'lib/hooks/usePalette' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {LoadLatestBtn as LoadLatestBtnMobile} from './LoadLatestBtnMobile' -import {HITSLOP_20} from 'lib/constants' - -export const LoadLatestBtn = ({ - onPress, - label, - showIndicator, - minimalShellMode, -}: { - onPress: () => void - label: string - showIndicator: boolean - minimalShellMode?: boolean -}) => { - const pal = usePalette('default') - const {isMobile} = useWebMediaQueries() - if (isMobile) { - return ( - <LoadLatestBtnMobile - onPress={onPress} - label={label} - showIndicator={showIndicator} - /> - ) - } - return ( - <> - {showIndicator && ( - <TouchableOpacity - style={[ - pal.view, - pal.borderDark, - styles.loadLatestCentered, - minimalShellMode && styles.loadLatestCenteredMinimal, - ]} - onPress={onPress} - hitSlop={HITSLOP_20} - accessibilityRole="button" - accessibilityLabel={label} - accessibilityHint=""> - <Text type="md-bold" style={pal.text}> - {label} - </Text> - </TouchableOpacity> - )} - <TouchableOpacity - style={[pal.view, pal.borderDark, styles.loadLatest]} - onPress={onPress} - hitSlop={HITSLOP_20} - accessibilityRole="button" - accessibilityLabel={label} - accessibilityHint=""> - <Text type="md-bold" style={pal.text}> - <FontAwesomeIcon - icon="angle-up" - size={21} - style={[pal.text, styles.icon]} - /> - </Text> - </TouchableOpacity> - </> - ) -} - -const styles = StyleSheet.create({ - loadLatest: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - position: 'absolute', - // @ts-ignore web only - left: '50vw', - // @ts-ignore web only -prf - transform: 'translateX(-282px)', - bottom: 40, - width: 54, - height: 54, - borderRadius: 30, - borderWidth: 1, - }, - icon: { - position: 'relative', - top: 2, - }, - loadLatestCentered: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - position: 'absolute', - // @ts-ignore web only - left: '50vw', - // @ts-ignore web only -prf - transform: 'translateX(-50%)', - top: 60, - paddingHorizontal: 24, - paddingVertical: 14, - borderRadius: 30, - borderWidth: 1, - }, - loadLatestCenteredMinimal: { - top: 20, - }, -}) diff --git a/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx b/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx deleted file mode 100644 index 3e8add5e9..000000000 --- a/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React from 'react' -import {StyleSheet, TouchableOpacity, View} from 'react-native' -import {observer} from 'mobx-react-lite' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {useSafeAreaInsets} from 'react-native-safe-area-context' -import {clamp} from 'lodash' -import {useStores} from 'state/index' -import {usePalette} from 'lib/hooks/usePalette' -import {colors} from 'lib/styles' -import {HITSLOP_20} from 'lib/constants' - -export const LoadLatestBtn = observer(function LoadLatestBtnImpl({ - onPress, - label, - showIndicator, -}: { - onPress: () => void - label: string - showIndicator: boolean - minimalShellMode?: boolean // NOTE not used on mobile -prf -}) { - const store = useStores() - const pal = usePalette('default') - const safeAreaInsets = useSafeAreaInsets() - return ( - <TouchableOpacity - style={[ - styles.loadLatest, - pal.borderDark, - pal.view, - !store.shell.minimalShellMode && { - bottom: 60 + clamp(safeAreaInsets.bottom, 15, 30), - }, - ]} - onPress={onPress} - hitSlop={HITSLOP_20} - accessibilityRole="button" - accessibilityLabel={label} - accessibilityHint=""> - <FontAwesomeIcon icon="angle-up" color={pal.colors.text} size={19} /> - {showIndicator && <View style={[styles.indicator, pal.borderDark]} />} - </TouchableOpacity> - ) -}) - -const styles = StyleSheet.create({ - loadLatest: { - position: 'absolute', - left: 18, - bottom: 35, - borderWidth: 1, - width: 52, - height: 52, - borderRadius: 26, - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - }, - indicator: { - position: 'absolute', - top: 3, - right: 3, - backgroundColor: colors.blue3, - width: 12, - height: 12, - borderRadius: 6, - borderWidth: 1, - }, -}) |