diff options
Diffstat (limited to 'src/view')
-rw-r--r-- | src/view/com/discover/SuggestedPosts.tsx | 2 | ||||
-rw-r--r-- | src/view/com/onboard/FeatureExplainer.tsx | 196 | ||||
-rw-r--r-- | src/view/com/onboard/FeatureExplainer.web.tsx | 202 | ||||
-rw-r--r-- | src/view/com/onboard/Follows.tsx | 55 | ||||
-rw-r--r-- | src/view/com/onboard/Follows.web.tsx | 47 | ||||
-rw-r--r-- | src/view/com/post-thread/PostThreadItem.tsx | 2 | ||||
-rw-r--r-- | src/view/com/post/Post.tsx | 2 | ||||
-rw-r--r-- | src/view/com/posts/Feed.tsx | 41 | ||||
-rw-r--r-- | src/view/com/posts/FeedItem.tsx | 5 | ||||
-rw-r--r-- | src/view/com/profile/FollowButton.tsx | 57 | ||||
-rw-r--r-- | src/view/com/profile/ProfileCard.tsx | 49 | ||||
-rw-r--r-- | src/view/com/util/PostMeta.tsx | 80 | ||||
-rw-r--r-- | src/view/com/util/WelcomeBanner.tsx | 33 | ||||
-rw-r--r-- | src/view/screens/Home.tsx | 4 | ||||
-rw-r--r-- | src/view/screens/Onboard.tsx | 40 | ||||
-rw-r--r-- | src/view/shell/mobile/index.tsx | 12 | ||||
-rw-r--r-- | src/view/shell/web/index.tsx | 10 |
17 files changed, 204 insertions, 633 deletions
diff --git a/src/view/com/discover/SuggestedPosts.tsx b/src/view/com/discover/SuggestedPosts.tsx index 86a6bd394..9c7745dfa 100644 --- a/src/view/com/discover/SuggestedPosts.tsx +++ b/src/view/com/discover/SuggestedPosts.tsx @@ -33,7 +33,7 @@ export const SuggestedPosts = observer(() => { <> <View style={[pal.border, styles.bottomBorder]}> {suggestedPostsView.posts.map(item => ( - <Post item={item} key={item._reactKey} /> + <Post item={item} key={item._reactKey} showFollowBtn /> ))} </View> </> diff --git a/src/view/com/onboard/FeatureExplainer.tsx b/src/view/com/onboard/FeatureExplainer.tsx deleted file mode 100644 index 323b1ba14..000000000 --- a/src/view/com/onboard/FeatureExplainer.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import React, {useState} from 'react' -import { - Animated, - Image, - SafeAreaView, - StyleSheet, - TouchableOpacity, - useWindowDimensions, - View, -} from 'react-native' -import {TabView, SceneMap, Route, TabBarProps} from 'react-native-tab-view' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' -import {Text} from '../util/text/Text' -import {useStores} from 'state/index' -import {s} from 'lib/styles' -import {TABS_EXPLAINER} from 'lib/assets' -import {TABS_ENABLED} from 'lib/build-flags' - -const ROUTES = TABS_ENABLED - ? [ - {key: 'intro', title: 'Intro'}, - {key: 'tabs', title: 'Tabs'}, - ] - : [{key: 'intro', title: 'Intro'}] - -const Intro = () => ( - <View style={styles.explainer}> - <Text - style={[styles.explainerHeading, s.normal, styles.explainerHeadingIntro]}> - Welcome to{' '} - <Text style={[s.bold, s.blue3, styles.explainerHeadingBrand]}> - Bluesky - </Text> - </Text> - <Text style={[styles.explainerDesc, styles.explainerDescIntro]}> - This is an early beta. Your feedback is appreciated! - </Text> - </View> -) - -const Tabs = () => ( - <View style={styles.explainer}> - <View style={styles.explainerIcon}> - <View style={s.flex1} /> - <FontAwesomeIcon - icon={['far', 'clone']} - style={[s.black as FontAwesomeIconStyle, s.mb5]} - size={36} - /> - <View style={s.flex1} /> - </View> - <Text style={styles.explainerHeading}>Tabs</Text> - <Text style={styles.explainerDesc}> - Never lose your place! Long-press to open posts and profiles in a new tab. - </Text> - <Text style={styles.explainerDesc}> - <Image source={TABS_EXPLAINER} style={styles.explainerImg} /> - </Text> - </View> -) - -const SCENE_MAP = { - intro: Intro, - tabs: Tabs, -} -const renderScene = SceneMap(SCENE_MAP) - -export const FeatureExplainer = () => { - const layout = useWindowDimensions() - const store = useStores() - const [index, setIndex] = useState(0) - - const onPressSkip = () => store.onboard.next() - const onPressNext = () => { - if (index >= ROUTES.length - 1) { - store.onboard.next() - } else { - setIndex(index + 1) - } - } - - const renderTabBar = (props: TabBarProps<Route>) => { - const inputRange = props.navigationState.routes.map((x, i) => i) - return ( - <View style={styles.tabBar}> - <View style={s.flex1} /> - {props.navigationState.routes.map((route, i) => { - const opacity = props.position.interpolate({ - inputRange, - outputRange: inputRange.map(inputIndex => - inputIndex === i ? 1 : 0.5, - ), - }) - - return ( - <TouchableOpacity - key={i} - style={styles.tabItem} - onPress={() => setIndex(i)}> - <Animated.Text style={{opacity}}>°</Animated.Text> - </TouchableOpacity> - ) - })} - <View style={s.flex1} /> - </View> - ) - } - - const FirstExplainer = SCENE_MAP[ROUTES[0]?.key as keyof typeof SCENE_MAP] - return ( - <SafeAreaView style={styles.container}> - {ROUTES.length > 1 ? ( - <TabView - navigationState={{index, routes: ROUTES}} - renderScene={renderScene} - renderTabBar={renderTabBar} - onIndexChange={setIndex} - initialLayout={{width: layout.width}} - tabBarPosition="bottom" - /> - ) : FirstExplainer ? ( - <FirstExplainer /> - ) : ( - <View /> - )} - <View style={styles.footer}> - <TouchableOpacity - onPress={onPressSkip} - testID="onboardFeatureExplainerSkipBtn"> - <Text style={[s.blue3, s.f18]}>Skip</Text> - </TouchableOpacity> - <View style={s.flex1} /> - <TouchableOpacity - onPress={onPressNext} - testID="onboardFeatureExplainerNextBtn"> - <Text style={[s.blue3, s.f18]}>Next</Text> - </TouchableOpacity> - </View> - </SafeAreaView> - ) -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - - tabBar: { - flexDirection: 'row', - }, - tabItem: { - alignItems: 'center', - padding: 16, - }, - - explainer: { - flex: 1, - paddingHorizontal: 16, - paddingTop: 80, - }, - explainerIcon: { - flexDirection: 'row', - }, - explainerHeading: { - fontSize: 42, - fontWeight: 'bold', - textAlign: 'center', - marginBottom: 16, - }, - explainerHeadingIntro: { - lineHeight: 60, - paddingTop: 50, - paddingBottom: 50, - }, - explainerHeadingBrand: {fontSize: 56}, - explainerDesc: { - fontSize: 18, - textAlign: 'center', - marginBottom: 16, - }, - explainerDescIntro: {fontSize: 24}, - explainerImg: { - resizeMode: 'contain', - maxWidth: '100%', - maxHeight: 330, - }, - - footer: { - flexDirection: 'row', - paddingHorizontal: 32, - paddingBottom: 24, - }, -}) diff --git a/src/view/com/onboard/FeatureExplainer.web.tsx b/src/view/com/onboard/FeatureExplainer.web.tsx deleted file mode 100644 index 177ac58dd..000000000 --- a/src/view/com/onboard/FeatureExplainer.web.tsx +++ /dev/null @@ -1,202 +0,0 @@ -import React, {useState} from 'react' -import { - Animated, - Image, - StyleSheet, - TouchableOpacity, - useWindowDimensions, - View, -} from 'react-native' -import {TabView, SceneMap, Route, TabBarProps} from 'react-native-tab-view' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' -import {CenteredView} from '../util/Views.web' -import {Text} from '../util/text/Text' -import {useStores} from 'state/index' -import {s, colors} from 'lib/styles' -import {TABS_EXPLAINER} from 'lib/assets' -import {TABS_ENABLED} from 'lib/build-flags' - -const ROUTES = TABS_ENABLED - ? [ - {key: 'intro', title: 'Intro'}, - {key: 'tabs', title: 'Tabs'}, - ] - : [{key: 'intro', title: 'Intro'}] - -const Intro = () => ( - <View style={styles.explainer}> - <Text - style={[styles.explainerHeading, s.normal, styles.explainerHeadingIntro]}> - Welcome to{' '} - <Text style={[s.bold, s.blue3, styles.explainerHeadingBrand]}> - Bluesky - </Text> - </Text> - <Text style={[styles.explainerDesc, styles.explainerDescIntro]}> - This is an early beta. Your feedback is appreciated! - </Text> - </View> -) - -const Tabs = () => ( - <View style={styles.explainer}> - <View style={styles.explainerIcon}> - <View style={s.flex1} /> - <FontAwesomeIcon - icon={['far', 'clone']} - style={[s.black as FontAwesomeIconStyle, s.mb5]} - size={36} - /> - <View style={s.flex1} /> - </View> - <Text style={styles.explainerHeading}>Tabs</Text> - <Text style={styles.explainerDesc}> - Never lose your place! Long-press to open posts and profiles in a new tab. - </Text> - <Text style={styles.explainerDesc}> - <Image source={TABS_EXPLAINER} style={styles.explainerImg} /> - </Text> - </View> -) - -const SCENE_MAP = { - intro: Intro, - tabs: Tabs, -} -const renderScene = SceneMap(SCENE_MAP) - -export const FeatureExplainer = () => { - const layout = useWindowDimensions() - const store = useStores() - const [index, setIndex] = useState(0) - - const onPressSkip = () => store.onboard.next() - const onPressNext = () => { - if (index >= ROUTES.length - 1) { - store.onboard.next() - } else { - setIndex(index + 1) - } - } - - const renderTabBar = (props: TabBarProps<Route>) => { - const inputRange = props.navigationState.routes.map((x, i) => i) - return ( - <View style={styles.tabBar}> - <View style={s.flex1} /> - {props.navigationState.routes.map((route, i) => { - const opacity = props.position.interpolate({ - inputRange, - outputRange: inputRange.map(inputIndex => - inputIndex === i ? 1 : 0.5, - ), - }) - - return ( - <TouchableOpacity - key={i} - style={styles.tabItem} - onPress={() => setIndex(i)}> - <Animated.Text style={{opacity}}>°</Animated.Text> - </TouchableOpacity> - ) - })} - <View style={s.flex1} /> - </View> - ) - } - - const FirstExplainer = SCENE_MAP[ROUTES[0]?.key as keyof typeof SCENE_MAP] - return ( - <CenteredView style={styles.container}> - {ROUTES.length > 1 ? ( - <TabView - navigationState={{index, routes: ROUTES}} - renderScene={renderScene} - renderTabBar={renderTabBar} - onIndexChange={setIndex} - initialLayout={{width: layout.width}} - tabBarPosition="bottom" - /> - ) : FirstExplainer ? ( - <FirstExplainer /> - ) : ( - <View /> - )} - <View style={styles.footer}> - <TouchableOpacity onPress={onPressSkip}> - <Text style={styles.footerBtn}>Skip</Text> - </TouchableOpacity> - <TouchableOpacity onPress={onPressNext}> - <Text style={[styles.footerBtn, styles.footerBtnNext]}>Next</Text> - </TouchableOpacity> - </View> - </CenteredView> - ) -} - -const styles = StyleSheet.create({ - container: { - height: '100%', - justifyContent: 'center', - paddingBottom: '10%', - }, - - tabBar: { - flexDirection: 'row', - }, - tabItem: { - alignItems: 'center', - padding: 16, - }, - - explainer: { - paddingHorizontal: 16, - }, - explainerIcon: { - flexDirection: 'row', - }, - explainerHeading: { - fontSize: 42, - fontWeight: 'bold', - textAlign: 'center', - marginBottom: 16, - }, - explainerHeadingIntro: { - lineHeight: 40, - }, - explainerHeadingBrand: {fontSize: 56}, - explainerDesc: { - fontSize: 18, - textAlign: 'center', - marginBottom: 16, - color: colors.gray5, - }, - explainerDescIntro: {fontSize: 24}, - explainerImg: { - resizeMode: 'contain', - maxWidth: '100%', - maxHeight: 330, - }, - - footer: { - flexDirection: 'row', - justifyContent: 'center', - paddingTop: 24, - }, - footerBtn: { - color: colors.blue3, - fontSize: 19, - paddingHorizontal: 36, - paddingVertical: 8, - }, - footerBtnNext: { - marginLeft: 10, - borderWidth: 1, - borderColor: colors.blue3, - borderRadius: 6, - }, -}) diff --git a/src/view/com/onboard/Follows.tsx b/src/view/com/onboard/Follows.tsx deleted file mode 100644 index e7de82b39..000000000 --- a/src/view/com/onboard/Follows.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react' -import {SafeAreaView, StyleSheet, TouchableOpacity, View} from 'react-native' -import {observer} from 'mobx-react-lite' -import {SuggestedFollows} from '../discover/SuggestedFollows' -import {Text} from '../util/text/Text' -import {useStores} from 'state/index' -import {s} from 'lib/styles' - -export const Follows = observer(() => { - const store = useStores() - - const onNoSuggestions = () => { - // no suggestions, bounce from this view - store.onboard.next() - } - const onPressNext = () => store.onboard.next() - - return ( - <SafeAreaView style={styles.container}> - <Text style={styles.title}>Suggested follows</Text> - <View style={s.flex1}> - <SuggestedFollows onNoSuggestions={onNoSuggestions} /> - </View> - <View style={styles.footer}> - <TouchableOpacity onPress={onPressNext} testID="onboardFollowsSkipBtn"> - <Text style={[s.blue3, s.f18]}>Skip</Text> - </TouchableOpacity> - <View style={s.flex1} /> - <TouchableOpacity onPress={onPressNext} testID="onboardFollowsNextBtn"> - <Text style={[s.blue3, s.f18]}>Next</Text> - </TouchableOpacity> - </View> - </SafeAreaView> - ) -}) - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - - title: { - fontSize: 24, - fontWeight: 'bold', - paddingHorizontal: 16, - paddingBottom: 12, - }, - - footer: { - flexDirection: 'row', - paddingHorizontal: 32, - paddingBottom: 24, - paddingTop: 16, - }, -}) diff --git a/src/view/com/onboard/Follows.web.tsx b/src/view/com/onboard/Follows.web.tsx deleted file mode 100644 index 6b015bb09..000000000 --- a/src/view/com/onboard/Follows.web.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react' -import {SafeAreaView, StyleSheet, TouchableOpacity} from 'react-native' -import {observer} from 'mobx-react-lite' -import {SuggestedFollows} from '../discover/SuggestedFollows' -import {CenteredView} from '../util/Views.web' -import {Text} from '../util/text/Text' -import {useStores} from 'state/index' -import {s} from 'lib/styles' - -export const Follows = observer(() => { - const store = useStores() - - const onNoSuggestions = () => { - // no suggestions, bounce from this view - store.onboard.next() - } - const onPressNext = () => store.onboard.next() - - return ( - <SafeAreaView style={styles.container}> - <CenteredView style={styles.header}> - <Text type="title-lg"> - Follow these people to see their posts in your feed - </Text> - <TouchableOpacity onPress={onPressNext}> - <Text style={[styles.title, s.blue3, s.pr10]}>Next »</Text> - </TouchableOpacity> - </CenteredView> - <SuggestedFollows onNoSuggestions={onNoSuggestions} /> - </SafeAreaView> - ) -}) - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - title: { - fontSize: 24, - fontWeight: 'bold', - }, - - header: { - paddingTop: 30, - paddingBottom: 40, - }, -}) diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 98d44267d..65bae0192 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -305,6 +305,8 @@ export const PostThreadItem = observer(function PostThreadItem({ authorHandle={item.post.author.handle} authorDisplayName={item.post.author.displayName} timestamp={item.post.indexedAt} + did={item.post.author.did} + declarationCid={item.post.author.declaration.cid} /> {item.post.author.viewer?.muted ? ( <View style={[styles.mutedWarning, pal.btn]}> diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index 1550f8620..c0ff95416 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -156,6 +156,8 @@ export const Post = observer(function Post({ authorHandle={item.post.author.handle} authorDisplayName={item.post.author.displayName} timestamp={item.post.indexedAt} + did={item.post.author.did} + declarationCid={item.post.author.declaration.cid} /> {replyAuthorDid !== '' && ( <View style={[s.flexRow, s.mb2, s.alignCenter]}> diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx index 7e5d166d2..03a719f16 100644 --- a/src/view/com/posts/Feed.tsx +++ b/src/view/com/posts/Feed.tsx @@ -13,16 +13,21 @@ import {EmptyState} from '../util/EmptyState' import {ErrorMessage} from '../util/error/ErrorMessage' import {FeedModel} from 'state/models/feed-view' import {FeedItem} from './FeedItem' +import {WelcomeBanner} from '../util/WelcomeBanner' import {OnScrollCb} from 'lib/hooks/useOnMainScroll' import {s} from 'lib/styles' import {useAnalytics} from 'lib/analytics' +import {useStores} from 'state/index' const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} const ERROR_FEED_ITEM = {_reactKey: '__error__'} +const WELCOME_FEED_ITEM = {_reactKey: '__welcome__'} export const Feed = observer(function Feed({ feed, style, + showWelcomeBanner, + showPostFollowBtn, scrollElRef, onPressTryAgain, onScroll, @@ -31,6 +36,8 @@ export const Feed = observer(function Feed({ }: { feed: FeedModel style?: StyleProp<ViewStyle> + showWelcomeBanner?: boolean + showPostFollowBtn?: boolean scrollElRef?: MutableRefObject<FlatList<any> | null> onPressTryAgain?: () => void onScroll?: OnScrollCb @@ -38,7 +45,9 @@ export const Feed = observer(function Feed({ headerOffset?: number }) { const {track} = useAnalytics() + const store = useStores() const [isRefreshing, setIsRefreshing] = React.useState(false) + const [isNewUser, setIsNewUser] = React.useState<boolean>(false) const data = React.useMemo(() => { let feedItems: any[] = [] @@ -46,6 +55,9 @@ export const Feed = observer(function Feed({ if (feed.hasError) { feedItems = feedItems.concat([ERROR_FEED_ITEM]) } + if (showWelcomeBanner && isNewUser) { + feedItems = feedItems.concat([WELCOME_FEED_ITEM]) + } if (feed.isEmpty) { feedItems = feedItems.concat([EMPTY_FEED_ITEM]) } else { @@ -53,21 +65,39 @@ export const Feed = observer(function Feed({ } } return feedItems - }, [feed.hasError, feed.hasLoaded, feed.isEmpty, feed.feed]) + }, [ + feed.hasError, + feed.hasLoaded, + feed.isEmpty, + feed.feed, + showWelcomeBanner, + isNewUser, + ]) // events // = + const checkWelcome = React.useCallback(async () => { + if (showWelcomeBanner) { + await store.me.follows.fetchIfNeeded() + setIsNewUser(store.me.follows.isEmpty) + } + }, [showWelcomeBanner, store.me.follows]) + React.useEffect(() => { + checkWelcome() + }, [checkWelcome]) + const onRefresh = React.useCallback(async () => { track('Feed:onRefresh') setIsRefreshing(true) + checkWelcome() try { await feed.refresh() } catch (err) { feed.rootStore.log.error('Failed to refresh posts feed', err) } setIsRefreshing(false) - }, [feed, track, setIsRefreshing]) + }, [feed, track, setIsRefreshing, checkWelcome]) const onEndReached = React.useCallback(async () => { track('Feed:onEndReached') try { @@ -101,10 +131,12 @@ export const Feed = observer(function Feed({ onPressTryAgain={onPressTryAgain} /> ) + } else if (item === WELCOME_FEED_ITEM) { + return <WelcomeBanner /> } - return <FeedItem item={item} /> + return <FeedItem item={item} showFollowBtn={showPostFollowBtn} /> }, - [feed, onPressTryAgain], + [feed, onPressTryAgain, showPostFollowBtn], ) const FeedFooter = React.useCallback( @@ -123,6 +155,7 @@ export const Feed = observer(function Feed({ <View testID={testID} style={style}> {feed.isLoading && data.length === 0 && ( <CenteredView style={{paddingTop: headerOffset}}> + {showWelcomeBanner && isNewUser && <WelcomeBanner />} <PostFeedLoadingPlaceholder /> </CenteredView> )} diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index 474afb55b..c3e9f61fa 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -26,10 +26,12 @@ import {useAnalytics} from 'lib/analytics' export const FeedItem = observer(function ({ item, showReplyLine, + showFollowBtn, ignoreMuteFor, }: { item: FeedItemModel showReplyLine?: boolean + showFollowBtn?: boolean ignoreMuteFor?: string }) { const store = useStores() @@ -175,6 +177,9 @@ export const FeedItem = observer(function ({ authorHandle={item.post.author.handle} authorDisplayName={item.post.author.displayName} timestamp={item.post.indexedAt} + did={item.post.author.did} + declarationCid={item.post.author.declaration.cid} + showFollowBtn={showFollowBtn} /> {!isChild && replyAuthorDid !== '' && ( <View style={[s.flexRow, s.mb2, s.alignCenter]}> diff --git a/src/view/com/profile/FollowButton.tsx b/src/view/com/profile/FollowButton.tsx new file mode 100644 index 000000000..71462bea8 --- /dev/null +++ b/src/view/com/profile/FollowButton.tsx @@ -0,0 +1,57 @@ +import React from 'react' +import {StyleSheet, TouchableOpacity, View} from 'react-native' +import {observer} from 'mobx-react-lite' +import {Text} from '../util/text/Text' +import {useStores} from 'state/index' +import * as apilib from 'lib/api/index' +import * as Toast from '../util/Toast' +import {usePalette} from 'lib/hooks/usePalette' + +const FollowButton = observer( + ({did, declarationCid}: {did: string; declarationCid: string}) => { + const store = useStores() + const pal = usePalette('default') + const isFollowing = store.me.follows.isFollowing(did) + + const onToggleFollow = async () => { + if (store.me.follows.isFollowing(did)) { + try { + await apilib.unfollow(store, store.me.follows.getFollowUri(did)) + store.me.follows.removeFollow(did) + } catch (e: any) { + store.log.error('Failed fo delete follow', e) + Toast.show('An issue occurred, please try again.') + } + } else { + try { + const res = await apilib.follow(store, did, declarationCid) + store.me.follows.addFollow(did, res.uri) + } catch (e: any) { + store.log.error('Failed fo create follow', e) + Toast.show('An issue occurred, please try again.') + } + } + } + + return ( + <TouchableOpacity onPress={onToggleFollow}> + <View style={[styles.btn, pal.btn]}> + <Text type="button" style={[pal.text]}> + {isFollowing ? 'Unfollow' : 'Follow'} + </Text> + </View> + </TouchableOpacity> + ) + }, +) + +export default FollowButton + +const styles = StyleSheet.create({ + btn: { + paddingVertical: 7, + borderRadius: 50, + marginLeft: 6, + paddingHorizontal: 14, + }, +}) diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx index 6a136a02d..3c487b70f 100644 --- a/src/view/com/profile/ProfileCard.tsx +++ b/src/view/com/profile/ProfileCard.tsx @@ -1,14 +1,13 @@ import React from 'react' -import {StyleSheet, TouchableOpacity, View} from 'react-native' +import {StyleSheet, View} from 'react-native' import {observer} from 'mobx-react-lite' import {Link} from '../util/Link' import {Text} from '../util/text/Text' import {UserAvatar} from '../util/UserAvatar' -import * as Toast from '../util/Toast' import {s} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' import {useStores} from 'state/index' -import * as apilib from 'lib/api/index' +import FollowButton from './FollowButton' export function ProfileCard({ handle, @@ -102,26 +101,7 @@ export const ProfileCardWithFollowBtn = observer( }) => { const store = useStores() const isMe = store.me.handle === handle - const isFollowing = store.me.follows.isFollowing(did) - const onToggleFollow = async () => { - if (store.me.follows.isFollowing(did)) { - try { - await apilib.unfollow(store, store.me.follows.getFollowUri(did)) - store.me.follows.removeFollow(did) - } catch (e: any) { - store.log.error('Failed fo delete follow', e) - Toast.show('An issue occurred, please try again.') - } - } else { - try { - const res = await apilib.follow(store, did, declarationCid) - store.me.follows.addFollow(did, res.uri) - } catch (e: any) { - store.log.error('Failed fo create follow', e) - Toast.show('An issue occurred, please try again.') - } - } - } + return ( <ProfileCard handle={handle} @@ -132,34 +112,13 @@ export const ProfileCardWithFollowBtn = observer( renderButton={ isMe ? undefined - : () => ( - <FollowBtn isFollowing={isFollowing} onPress={onToggleFollow} /> - ) + : () => <FollowButton did={did} declarationCid={declarationCid} /> } /> ) }, ) -function FollowBtn({ - isFollowing, - onPress, -}: { - isFollowing: boolean - onPress: () => void -}) { - const pal = usePalette('default') - return ( - <TouchableOpacity onPress={onPress}> - <View style={[styles.btn, pal.btn]}> - <Text type="button" style={[pal.text]}> - {isFollowing ? 'Unfollow' : 'Follow'} - </Text> - </View> - </TouchableOpacity> - ) -} - const styles = StyleSheet.create({ outer: { borderTopWidth: 1, diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx index 6ba6fac1b..a07d91899 100644 --- a/src/view/com/util/PostMeta.tsx +++ b/src/view/com/util/PostMeta.tsx @@ -1,37 +1,74 @@ import React from 'react' -import {Platform, StyleSheet, View} from 'react-native' +import {StyleSheet, View} from 'react-native' import {Text} from './text/Text' import {ago} from 'lib/strings/time' import {usePalette} from 'lib/hooks/usePalette' +import {useStores} from 'state/index' +import {observer} from 'mobx-react-lite' +import FollowButton from '../profile/FollowButton' interface PostMetaOpts { authorHandle: string authorDisplayName: string | undefined timestamp: string + did: string + declarationCid: string + showFollowBtn?: boolean } -export function PostMeta(opts: PostMetaOpts) { +export const PostMeta = observer(function (opts: PostMetaOpts) { const pal = usePalette('default') let displayName = opts.authorDisplayName || opts.authorHandle let handle = opts.authorHandle + const store = useStores() + const isMe = opts.did === store.me.did - // HACK - // Android simply cannot handle the truncation case we need - // so we have to do it manually here - // -prf - if (Platform.OS === 'android') { - if (displayName.length + handle.length > 26) { - if (displayName.length > 26) { - displayName = displayName.slice(0, 23) + '...' - } else { - handle = handle.slice(0, 23 - displayName.length) + '...' - if (handle.endsWith('....')) { - handle = handle.slice(0, -4) + '...' - } - } - } + // NOTE we capture `isFollowing` via a memo so that follows + // don't change this UI immediately, but rather upon future + // renders + const isFollowing = React.useMemo( + () => store.me.follows.isFollowing(opts.did), + [opts.did, store.me.follows], + ) + + if (opts.showFollowBtn && !isMe && !isFollowing) { + // two-liner with follow button + return ( + <View style={[styles.metaTwoLine]}> + <View> + <Text + type="lg-bold" + style={[pal.text]} + numberOfLines={1} + lineHeight={1.2}> + {displayName}{' '} + <Text + type="md" + style={[styles.metaItem, pal.textLight]} + lineHeight={1.2}> + · {ago(opts.timestamp)} + </Text> + </Text> + <Text + type="md" + style={[styles.metaItem, pal.textLight]} + lineHeight={1.2}> + {handle ? ( + <Text type="md" style={[pal.textLight]}> + @{handle} + </Text> + ) : undefined} + </Text> + </View> + + <View> + <FollowButton did={opts.did} declarationCid={opts.declarationCid} /> + </View> + </View> + ) } + // one-liner return ( <View style={styles.meta}> <View style={[styles.metaItem, styles.maxWidth]}> @@ -53,13 +90,18 @@ export function PostMeta(opts: PostMetaOpts) { </Text> </View> ) -} +}) const styles = StyleSheet.create({ meta: { flexDirection: 'row', alignItems: 'baseline', - paddingTop: 0, + paddingBottom: 2, + }, + metaTwoLine: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', paddingBottom: 2, }, metaItem: { diff --git a/src/view/com/util/WelcomeBanner.tsx b/src/view/com/util/WelcomeBanner.tsx new file mode 100644 index 000000000..d52288502 --- /dev/null +++ b/src/view/com/util/WelcomeBanner.tsx @@ -0,0 +1,33 @@ +import React from 'react' +import {StyleSheet, View} from 'react-native' +import {usePalette} from 'lib/hooks/usePalette' +import {Text} from './text/Text' +import {s} from 'lib/styles' + +export function WelcomeBanner() { + const pal = usePalette('default') + return ( + <View + testID="welcomeBanner" + style={[pal.view, styles.container, pal.border]}> + <Text + type="title-lg" + style={[pal.text, s.textCenter, s.bold, s.pb5]} + lineHeight={1.1}> + Welcome to the private beta! + </Text> + <Text type="lg" style={[pal.text, s.textCenter]}> + Here are some recent posts. Follow their creators to build your feed. + </Text> + </View> + ) +} + +const styles = StyleSheet.create({ + container: { + paddingTop: 30, + paddingBottom: 26, + paddingHorizontal: 20, + borderTopWidth: 1, + }, +}) diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index d11a9fb72..5b5699bcc 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -71,8 +71,6 @@ export const Home = observer(function Home({navIdx, visible}: ScreenParams) { store.log.debug('HomeScreen: Updating feed') if (store.me.mainFeed.hasContent) { store.me.mainFeed.update() - } else { - store.me.mainFeed.setup() } return cleanup }, [visible, store, store.me.mainFeed, navIdx, doPoll, wasVisible, scrollToTop, screen]) @@ -97,6 +95,8 @@ export const Home = observer(function Home({navIdx, visible}: ScreenParams) { feed={store.me.mainFeed} scrollElRef={scrollElRef} style={s.hContentRegion} + showWelcomeBanner + showPostFollowBtn onPressTryAgain={onPressTryAgain} onScroll={onMainScroll} headerOffset={HEADER_HEIGHT} diff --git a/src/view/screens/Onboard.tsx b/src/view/screens/Onboard.tsx deleted file mode 100644 index 1485670e7..000000000 --- a/src/view/screens/Onboard.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React, {useEffect} from 'react' -import {StyleSheet, View} from 'react-native' -import {observer} from 'mobx-react-lite' -import {FeatureExplainer} from '../com/onboard/FeatureExplainer' -import {Follows} from '../com/onboard/Follows' -import {OnboardStage, OnboardStageOrder} from 'state/models/onboard' -import {useStores} from 'state/index' - -export const Onboard = observer(() => { - const store = useStores() - - useEffect(() => { - // sanity check - bounce out of onboarding if the stage is wrong somehow - if (!OnboardStageOrder.includes(store.onboard.stage)) { - store.onboard.stop() - } - }, [store.onboard]) - - let Com - if (store.onboard.stage === OnboardStage.Explainers) { - Com = FeatureExplainer - } else if (store.onboard.stage === OnboardStage.Follows) { - Com = Follows - } else { - Com = View - } - - return ( - <View style={styles.container}> - <Com /> - </View> - ) -}) - -const styles = StyleSheet.create({ - container: { - height: '100%', - backgroundColor: '#fff', - }, -}) diff --git a/src/view/shell/mobile/index.tsx b/src/view/shell/mobile/index.tsx index dbfcad0ee..80403a6de 100644 --- a/src/view/shell/mobile/index.tsx +++ b/src/view/shell/mobile/index.tsx @@ -26,7 +26,6 @@ import { import {match, MatchResult} from '../../routes' import {Login} from '../../screens/Login' import {Menu} from './Menu' -import {Onboard} from '../../screens/Onboard' import {HorzSwipe} from '../../com/util/gestures/HorzSwipe' import {ModalsContainer} from '../../com/modals/Modal' import {Lightbox} from '../../com/lightbox/Lightbox' @@ -408,17 +407,6 @@ export const MobileShell: React.FC = observer(() => { </View> ) } - if (store.onboard.isOnboarding) { - return ( - <View testID="onboardOuterView" style={styles.outerContainer}> - <View style={styles.innerContainer}> - <ErrorBoundary> - <Onboard /> - </ErrorBoundary> - </View> - </View> - ) - } const isAtHome = store.nav.tab.current.url === TabPurposeMainPath[TabPurpose.Default] diff --git a/src/view/shell/web/index.tsx b/src/view/shell/web/index.tsx index 76b5ed09f..a76ae8060 100644 --- a/src/view/shell/web/index.tsx +++ b/src/view/shell/web/index.tsx @@ -6,7 +6,6 @@ import {useStores} from 'state/index' import {NavigationModel} from 'state/models/navigation' import {match, MatchResult} from '../../routes' import {DesktopHeader} from './DesktopHeader' -import {Onboard} from '../../screens/Onboard' import {Login} from '../../screens/Login' import {ErrorBoundary} from '../../com/util/ErrorBoundary' import {Lightbox} from '../../com/lightbox/Lightbox' @@ -35,15 +34,6 @@ export const WebShell: React.FC = observer(() => { </View> ) } - if (store.onboard.isOnboarding) { - return ( - <View style={styles.outerContainer}> - <ErrorBoundary> - <Onboard /> - </ErrorBoundary> - </View> - ) - } return ( <View style={[styles.outerContainer, pageBg]}> |