diff options
Diffstat (limited to 'src/view/screens')
-rw-r--r-- | src/view/screens/Contacts.tsx | 6 | ||||
-rw-r--r-- | src/view/screens/Debug.tsx | 15 | ||||
-rw-r--r-- | src/view/screens/Home.tsx | 58 | ||||
-rw-r--r-- | src/view/screens/Log.tsx | 8 | ||||
-rw-r--r-- | src/view/screens/Login.tsx | 47 | ||||
-rw-r--r-- | src/view/screens/Login.web.tsx | 14 | ||||
-rw-r--r-- | src/view/screens/NotFound.tsx | 2 | ||||
-rw-r--r-- | src/view/screens/Notifications.tsx | 81 | ||||
-rw-r--r-- | src/view/screens/Onboard.tsx | 4 | ||||
-rw-r--r-- | src/view/screens/PostDownvotedBy.tsx | 4 | ||||
-rw-r--r-- | src/view/screens/PostRepostedBy.tsx | 4 | ||||
-rw-r--r-- | src/view/screens/PostThread.tsx | 14 | ||||
-rw-r--r-- | src/view/screens/PostUpvotedBy.tsx | 4 | ||||
-rw-r--r-- | src/view/screens/Profile.tsx | 19 | ||||
-rw-r--r-- | src/view/screens/ProfileFollowers.tsx | 4 | ||||
-rw-r--r-- | src/view/screens/ProfileFollows.tsx | 4 | ||||
-rw-r--r-- | src/view/screens/Search.tsx | 232 | ||||
-rw-r--r-- | src/view/screens/Settings.tsx | 40 |
18 files changed, 374 insertions, 186 deletions
diff --git a/src/view/screens/Contacts.tsx b/src/view/screens/Contacts.tsx index cba17f285..21943a10a 100644 --- a/src/view/screens/Contacts.tsx +++ b/src/view/screens/Contacts.tsx @@ -4,10 +4,10 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {ProfileFollows as ProfileFollowsComponent} from '../com/profile/ProfileFollows' import {Selector} from '../com/util/Selector' import {Text} from '../com/util/text/Text' -import {colors} from '../lib/styles' +import {colors} from 'lib/styles' import {ScreenParams} from '../routes' -import {useStores} from '../../state' -import {useAnimatedValue} from '../lib/hooks/useAnimatedValue' +import {useStores} from 'state/index' +import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' export const Contacts = ({navIdx, visible}: ScreenParams) => { const store = useStores() diff --git a/src/view/screens/Debug.tsx b/src/view/screens/Debug.tsx index 0223e631d..09e3dd46a 100644 --- a/src/view/screens/Debug.tsx +++ b/src/view/screens/Debug.tsx @@ -1,11 +1,10 @@ import React from 'react' import {ScrollView, View} from 'react-native' import {ViewHeader} from '../com/util/ViewHeader' -import {ThemeProvider} from '../lib/ThemeContext' -import {PaletteColorName} from '../lib/ThemeContext' -import {usePalette} from '../lib/hooks/usePalette' -import {s} from '../lib/styles' -import {displayNotification} from '../lib/notifee' +import {ThemeProvider, PaletteColorName} from 'lib/ThemeContext' +import {usePalette} from 'lib/hooks/usePalette' +import {s} from 'lib/styles' +import {displayNotification} from 'lib/notifee' import {Text} from '../com/util/text/Text' import {ViewSelector} from '../com/util/ViewSelector' @@ -284,6 +283,9 @@ function TypographyView() { 'xs-heavy' lorem ipsum dolor </Text> + <Text type="title-2xl" style={[pal.text]}> + 'title-2xl' lorem ipsum dolor + </Text> <Text type="title-xl" style={[pal.text]}> 'title-xl' lorem ipsum dolor </Text> @@ -296,6 +298,9 @@ function TypographyView() { <Text type="button" style={[pal.text]}> Button </Text> + <Text type="button-lg" style={[pal.text]}> + Button-lg + </Text> </View> ) } diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index 4222c7513..de7e61ba4 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -1,23 +1,24 @@ import React, {useEffect} from 'react' -import {View} from 'react-native' +import {FlatList, View} from 'react-native' import {observer} from 'mobx-react-lite' import useAppState from 'react-native-appstate-hook' import {ViewHeader} from '../com/util/ViewHeader' import {Feed} from '../com/posts/Feed' import {FAB} from '../com/util/FAB' import {LoadLatestBtn} from '../com/util/LoadLatestBtn' -import {useStores} from '../../state' +import {useStores} from 'state/index' import {ScreenParams} from '../routes' -import {s} from '../lib/styles' -import {useOnMainScroll} from '../lib/hooks/useOnMainScroll' +import {s} from 'lib/styles' +import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' +import {useAnalytics} from 'lib/analytics' -export const Home = observer(function Home({ - navIdx, - visible, - scrollElRef, -}: ScreenParams) { +const HEADER_HEIGHT = 42 + +export const Home = observer(function Home({navIdx, visible}: ScreenParams) { const store = useStores() const onMainScroll = useOnMainScroll(store) + const {screen, track} = useAnalytics() + const scrollElRef = React.useRef<FlatList>(null) const [wasVisible, setWasVisible] = React.useState<boolean>(false) const {appState} = useAppState({ onForeground: () => doPoll(true), @@ -31,22 +32,31 @@ export const Home = observer(function Home({ if (store.me.mainFeed.isLoading) { return } - store.log.debug('Polling home feed') - store.me.mainFeed.checkForLatest().catch(e => { - store.log.error('Failed to poll feed', e) - }) + store.log.debug('HomeScreen: Polling for new posts') + store.me.mainFeed.checkForLatest() }, [appState, visible, store], ) + const scrollToTop = React.useCallback(() => { + // NOTE: the feed is offset by the height of the collapsing header, + // so we scroll to the negative of that height -prf + scrollElRef.current?.scrollToOffset({offset: -HEADER_HEIGHT}) + }, [scrollElRef]) + useEffect(() => { + const softResetSub = store.onScreenSoftReset(scrollToTop) const feedCleanup = store.me.mainFeed.registerListeners() - const pollInterval = setInterval(() => doPoll(), 15e3) + const pollInterval = setInterval(doPoll, 15e3) const cleanup = () => { clearInterval(pollInterval) + softResetSub.remove() feedCleanup() } + // guard to only continue when transitioning from !visible -> visible + // TODO is this 100% needed? depends on if useEffect() is getting refired + // for reasons other than `visible` changing -prf if (!visible) { setWasVisible(false) return cleanup @@ -55,17 +65,20 @@ export const Home = observer(function Home({ } setWasVisible(true) + // just became visible + screen('Feed') store.nav.setTitle(navIdx, 'Home') - store.log.debug('Updating home feed') + store.log.debug('HomeScreen: Updating feed') if (store.me.mainFeed.hasContent) { store.me.mainFeed.update() } else { store.me.mainFeed.setup() } return cleanup - }, [visible, store, navIdx, doPoll, wasVisible]) + }, [visible, store, store.me.mainFeed, navIdx, doPoll, wasVisible, scrollToTop, screen]) const onPressCompose = (imagesOpen?: boolean) => { + track('Home:ComposeButtonPressed') store.shell.openComposer({imagesOpen}) } const onPressTryAgain = () => { @@ -73,26 +86,31 @@ export const Home = observer(function Home({ } const onPressLoadLatest = () => { store.me.mainFeed.refresh() - scrollElRef?.current?.scrollToOffset({offset: 0}) + scrollToTop() } return ( <View style={s.h100pct}> - <ViewHeader title="Bluesky" subtitle="Private Beta" canGoBack={false} /> <Feed testID="homeFeed" key="default" feed={store.me.mainFeed} scrollElRef={scrollElRef} style={s.h100pct} - onPressCompose={onPressCompose} onPressTryAgain={onPressTryAgain} + onPressCompose={onPressCompose} onScroll={onMainScroll} + headerOffset={HEADER_HEIGHT} /> + <ViewHeader title="Bluesky" canGoBack={false} hideOnScroll /> {store.me.mainFeed.hasNewLatest && !store.me.mainFeed.isRefreshing && ( <LoadLatestBtn onPress={onPressLoadLatest} /> )} - <FAB icon="pen-nib" onPress={() => onPressCompose(false)} /> + <FAB + testID="composeFAB" + icon="plus" + onPress={() => onPressCompose(false)} + /> </View> ) }) diff --git a/src/view/screens/Log.tsx b/src/view/screens/Log.tsx index c3e156dcb..c067d3506 100644 --- a/src/view/screens/Log.tsx +++ b/src/view/screens/Log.tsx @@ -3,13 +3,13 @@ import {StyleSheet, TouchableOpacity, View} from 'react-native' import {observer} from 'mobx-react-lite' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {ScrollView} from '../com/util/Views' -import {useStores} from '../../state' +import {useStores} from 'state/index' import {ScreenParams} from '../routes' -import {s} from '../lib/styles' +import {s} from 'lib/styles' import {ViewHeader} from '../com/util/ViewHeader' import {Text} from '../com/util/text/Text' -import {usePalette} from '../lib/hooks/usePalette' -import {ago} from '../../lib/strings' +import {usePalette} from 'lib/hooks/usePalette' +import {ago} from 'lib/strings/time' export const Log = observer(function Log({navIdx, visible}: ScreenParams) { const pal = usePalette('default') diff --git a/src/view/screens/Login.tsx b/src/view/screens/Login.tsx index 81a2c9e6b..50b2a34c0 100644 --- a/src/view/screens/Login.tsx +++ b/src/view/screens/Login.tsx @@ -1,19 +1,16 @@ -import React, {useState} from 'react' -import { - Image, - SafeAreaView, - StyleSheet, - TouchableOpacity, - View, -} from 'react-native' +import React, {useEffect, useState} from 'react' +import {SafeAreaView, StyleSheet, TouchableOpacity, View} from 'react-native' +import Image, {Source as ImageSource} from 'view/com/util/images/Image' import {observer} from 'mobx-react-lite' import {Signin} from '../com/login/Signin' import {CreateAccount} from '../com/login/CreateAccount' import {Text} from '../com/util/text/Text' import {ErrorBoundary} from '../com/util/ErrorBoundary' -import {colors} from '../lib/styles' -import {usePalette} from '../lib/hooks/usePalette' -import {CLOUD_SPLASH} from '../lib/assets' +import {colors} from 'lib/styles' +import {usePalette} from 'lib/hooks/usePalette' +import {useStores} from 'state/index' +import {CLOUD_SPLASH} from 'lib/assets' +import {useAnalytics} from 'lib/analytics' enum ScreenState { S_SigninOrCreateAccount, @@ -28,6 +25,12 @@ const SigninOrCreateAccount = ({ onPressSignin: () => void onPressCreateAccount: () => void }) => { + const {screen} = useAnalytics() + + useEffect(() => { + screen('Login') + }, [screen]) + const pal = usePalette('default') return ( <> @@ -57,22 +60,28 @@ const SigninOrCreateAccount = ({ export const Login = observer(() => { const pal = usePalette('default') + const store = useStores() const [screenState, setScreenState] = useState<ScreenState>( ScreenState.S_SigninOrCreateAccount, ) - if (screenState === ScreenState.S_SigninOrCreateAccount) { + if ( + store.session.isResumingSession || + screenState === ScreenState.S_SigninOrCreateAccount + ) { return ( <View style={styles.container}> - <Image source={CLOUD_SPLASH} style={styles.bgImg} /> + <Image source={CLOUD_SPLASH as ImageSource} style={styles.bgImg} /> <SafeAreaView testID="noSessionView" style={styles.container}> <ErrorBoundary> - <SigninOrCreateAccount - onPressSignin={() => setScreenState(ScreenState.S_Signin)} - onPressCreateAccount={() => - setScreenState(ScreenState.S_CreateAccount) - } - /> + {!store.session.isResumingSession && ( + <SigninOrCreateAccount + onPressSignin={() => setScreenState(ScreenState.S_Signin)} + onPressCreateAccount={() => + setScreenState(ScreenState.S_CreateAccount) + } + /> + )} </ErrorBoundary> </SafeAreaView> </View> diff --git a/src/view/screens/Login.web.tsx b/src/view/screens/Login.web.tsx index 77149090c..90effc5d6 100644 --- a/src/view/screens/Login.web.tsx +++ b/src/view/screens/Login.web.tsx @@ -1,20 +1,13 @@ import React, {useState} from 'react' -import { - Image, - SafeAreaView, - StyleSheet, - TouchableOpacity, - View, -} from 'react-native' +import {SafeAreaView, StyleSheet, TouchableOpacity, View} from 'react-native' import {observer} from 'mobx-react-lite' import {CenteredView} from '../com/util/Views' import {Signin} from '../com/login/Signin' import {CreateAccount} from '../com/login/CreateAccount' import {Text} from '../com/util/text/Text' import {ErrorBoundary} from '../com/util/ErrorBoundary' -import {colors} from '../lib/styles' -import {usePalette} from '../lib/hooks/usePalette' -import {CLOUD_SPLASH} from '../lib/assets' +import {colors} from 'lib/styles' +import {usePalette} from 'lib/hooks/usePalette' enum ScreenState { S_SigninOrCreateAccount, @@ -125,6 +118,7 @@ const styles = StyleSheet.create({ width: '100%', height: '100%', }, + hero: {}, heroText: { backgroundColor: colors.white, paddingTop: 10, diff --git a/src/view/screens/NotFound.tsx b/src/view/screens/NotFound.tsx index c5c5ff002..77bbdd2aa 100644 --- a/src/view/screens/NotFound.tsx +++ b/src/view/screens/NotFound.tsx @@ -2,7 +2,7 @@ import React from 'react' import {Button, StyleSheet, View} from 'react-native' import {ViewHeader} from '../com/util/ViewHeader' import {Text} from '../com/util/text/Text' -import {useStores} from '../../state' +import {useStores} from 'state/index' export const NotFound = () => { const stores = useStores() diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx index 9b5dc5970..548b0d564 100644 --- a/src/view/screens/Notifications.tsx +++ b/src/view/screens/Notifications.tsx @@ -1,35 +1,79 @@ import React, {useEffect} from 'react' -import {View} from 'react-native' +import {FlatList, View} from 'react-native' +import useAppState from 'react-native-appstate-hook' import {ViewHeader} from '../com/util/ViewHeader' import {Feed} from '../com/notifications/Feed' -import {useStores} from '../../state' +import {useStores} from 'state/index' import {ScreenParams} from '../routes' -import {useOnMainScroll} from '../lib/hooks/useOnMainScroll' -import {s} from '../lib/styles' +import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' +import {s} from 'lib/styles' +import {useAnalytics} from 'lib/analytics' + +const NOTIFICATIONS_POLL_INTERVAL = 15e3 export const Notifications = ({navIdx, visible}: ScreenParams) => { const store = useStores() const onMainScroll = useOnMainScroll(store) + const scrollElRef = React.useRef<FlatList>(null) + const {screen} = useAnalytics() + const {appState} = useAppState({ + onForeground: () => doPoll(true), + }) + // event handlers + // = + const onPressTryAgain = () => { + store.me.notifications.refresh() + } + const scrollToTop = React.useCallback(() => { + scrollElRef.current?.scrollToOffset({offset: 0}) + }, [scrollElRef]) + + // periodic polling + // = + const doPoll = React.useCallback( + async (isForegrounding = false) => { + if (isForegrounding) { + // app is foregrounding, refresh optimistically + store.log.debug('NotificationsScreen: Refreshing on app foreground') + await Promise.all([ + store.me.notifications.loadUnreadCount(), + store.me.notifications.refresh(), + ]) + } else if (appState === 'active') { + // periodic poll, refresh if there are new notifs + store.log.debug('NotificationsScreen: Polling for new notifications') + const didChange = await store.me.notifications.loadUnreadCount() + if (didChange) { + store.log.debug('NotificationsScreen: Loading new notifications') + await store.me.notifications.loadLatest() + } + } + }, + [appState, store], + ) + useEffect(() => { + const pollInterval = setInterval(doPoll, NOTIFICATIONS_POLL_INTERVAL) + return () => clearInterval(pollInterval) + }, [doPoll]) + + // on-visible setup + // = useEffect(() => { if (!visible) { return } - store.log.debug('Updating notifications feed') - store.me.notifications - .update() - .catch(e => { - store.log.error('Error while updating notifications feed', e) - }) - .then(() => { - store.me.notifications.updateReadState() - }) + store.log.debug('NotificationsScreen: Updating feed') + const softResetSub = store.onScreenSoftReset(scrollToTop) + store.me.notifications.update().then(() => { + store.me.notifications.markAllRead() + }) + screen('Notifications') store.nav.setTitle(navIdx, 'Notifications') - }, [visible, store, navIdx]) - - const onPressTryAgain = () => { - store.me.notifications.refresh() - } + return () => { + softResetSub.remove() + } + }, [visible, store, navIdx, screen, scrollToTop]) return ( <View style={s.h100pct}> @@ -38,6 +82,7 @@ export const Notifications = ({navIdx, visible}: ScreenParams) => { view={store.me.notifications} onPressTryAgain={onPressTryAgain} onScroll={onMainScroll} + scrollElRef={scrollElRef} /> </View> ) diff --git a/src/view/screens/Onboard.tsx b/src/view/screens/Onboard.tsx index e31b42adc..1485670e7 100644 --- a/src/view/screens/Onboard.tsx +++ b/src/view/screens/Onboard.tsx @@ -3,8 +3,8 @@ 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' +import {OnboardStage, OnboardStageOrder} from 'state/models/onboard' +import {useStores} from 'state/index' export const Onboard = observer(() => { const store = useStores() diff --git a/src/view/screens/PostDownvotedBy.tsx b/src/view/screens/PostDownvotedBy.tsx index 1401868d4..570482598 100644 --- a/src/view/screens/PostDownvotedBy.tsx +++ b/src/view/screens/PostDownvotedBy.tsx @@ -3,8 +3,8 @@ import {View} from 'react-native' import {ViewHeader} from '../com/util/ViewHeader' import {PostVotedBy as PostLikedByComponent} from '../com/post-thread/PostVotedBy' import {ScreenParams} from '../routes' -import {useStores} from '../../state' -import {makeRecordUri} from '../../lib/strings' +import {useStores} from 'state/index' +import {makeRecordUri} from 'lib/strings/url-helpers' export const PostDownvotedBy = ({navIdx, visible, params}: ScreenParams) => { const store = useStores() diff --git a/src/view/screens/PostRepostedBy.tsx b/src/view/screens/PostRepostedBy.tsx index bf4d6ec91..4be4b4b42 100644 --- a/src/view/screens/PostRepostedBy.tsx +++ b/src/view/screens/PostRepostedBy.tsx @@ -3,8 +3,8 @@ import {View} from 'react-native' import {ViewHeader} from '../com/util/ViewHeader' import {PostRepostedBy as PostRepostedByComponent} from '../com/post-thread/PostRepostedBy' import {ScreenParams} from '../routes' -import {useStores} from '../../state' -import {makeRecordUri} from '../../lib/strings' +import {useStores} from 'state/index' +import {makeRecordUri} from 'lib/strings/url-helpers' export const PostRepostedBy = ({navIdx, visible, params}: ScreenParams) => { const store = useStores() diff --git a/src/view/screens/PostThread.tsx b/src/view/screens/PostThread.tsx index febaddc09..4b799468d 100644 --- a/src/view/screens/PostThread.tsx +++ b/src/view/screens/PostThread.tsx @@ -1,17 +1,16 @@ -import React, {useEffect, useMemo, useState} from 'react' +import React, {useEffect, useMemo} from 'react' import {View} from 'react-native' -import {makeRecordUri} from '../../lib/strings' +import {makeRecordUri} from 'lib/strings/url-helpers' import {ViewHeader} from '../com/util/ViewHeader' import {PostThread as PostThreadComponent} from '../com/post-thread/PostThread' -import {PostThreadViewModel} from '../../state/models/post-thread-view' +import {PostThreadViewModel} from 'state/models/post-thread-view' import {ScreenParams} from '../routes' -import {useStores} from '../../state' -import {s} from '../lib/styles' +import {useStores} from 'state/index' +import {s} from 'lib/styles' export const PostThread = ({navIdx, visible, params}: ScreenParams) => { const store = useStores() const {name, rkey} = params - const [viewSubtitle, setViewSubtitle] = useState<string>(`by ${name}`) const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) const view = useMemo<PostThreadViewModel>( () => new PostThreadViewModel(store, {uri}), @@ -24,7 +23,6 @@ export const PostThread = ({navIdx, visible, params}: ScreenParams) => { const setTitle = () => { const author = view.thread?.post.author const niceName = author?.handle || name - setViewSubtitle(`by ${niceName}`) store.nav.setTitle(navIdx, `Post by ${niceName}`) } if (!visible) { @@ -52,7 +50,7 @@ export const PostThread = ({navIdx, visible, params}: ScreenParams) => { return ( <View style={s.h100pct}> - <ViewHeader title="Post" subtitle={viewSubtitle} /> + <ViewHeader title="Post" /> <View style={s.h100pct}> <PostThreadComponent uri={uri} view={view} /> </View> diff --git a/src/view/screens/PostUpvotedBy.tsx b/src/view/screens/PostUpvotedBy.tsx index 4bba222ae..4d6ad4114 100644 --- a/src/view/screens/PostUpvotedBy.tsx +++ b/src/view/screens/PostUpvotedBy.tsx @@ -3,8 +3,8 @@ import {View} from 'react-native' import {ViewHeader} from '../com/util/ViewHeader' import {PostVotedBy as PostLikedByComponent} from '../com/post-thread/PostVotedBy' import {ScreenParams} from '../routes' -import {useStores} from '../../state' -import {makeRecordUri} from '../../lib/strings' +import {useStores} from 'state/index' +import {makeRecordUri} from 'lib/strings/url-helpers' export const PostUpvotedBy = ({navIdx, visible, params}: ScreenParams) => { const store = useStores() diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index 5c6616985..03d973b96 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -4,8 +4,8 @@ import {observer} from 'mobx-react-lite' import {ViewSelector} from '../com/util/ViewSelector' import {CenteredView} from '../com/util/Views' import {ScreenParams} from '../routes' -import {ProfileUiModel, Sections} from '../../state/models/profile-ui' -import {useStores} from '../../state' +import {ProfileUiModel, Sections} from 'state/models/profile-ui' +import {useStores} from 'state/index' import {ProfileHeader} from '../com/profile/ProfileHeader' import {FeedItem} from '../com/posts/FeedItem' import {PostFeedLoadingPlaceholder} from '../com/util/LoadingPlaceholder' @@ -14,8 +14,9 @@ import {ErrorMessage} from '../com/util/error/ErrorMessage' import {EmptyState} from '../com/util/EmptyState' import {Text} from '../com/util/text/Text' import {FAB} from '../com/util/FAB' -import {s, colors} from '../lib/styles' -import {useOnMainScroll} from '../lib/hooks/useOnMainScroll' +import {s, colors} from 'lib/styles' +import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' +import {useAnalytics} from 'lib/analytics' const LOADING_ITEM = {_reactKey: '__loading__'} const END_ITEM = {_reactKey: '__end__'} @@ -23,6 +24,12 @@ const EMPTY_ITEM = {_reactKey: '__empty__'} export const Profile = observer(({navIdx, visible, params}: ScreenParams) => { const store = useStores() + const {screen} = useAnalytics() + + useEffect(() => { + screen('Profile') + }, [screen]) + const onMainScroll = useOnMainScroll(store) const [hasSetup, setHasSetup] = useState<boolean>(false) const uiState = React.useMemo( @@ -128,7 +135,7 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => { } if (!uiState.feed.hasMore) { items = items.concat([END_ITEM]) - } else { + } else if (uiState.feed.isLoading) { Footer = LoadingMoreFooter } renderItem = (item: any) => { @@ -184,7 +191,7 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => { ) : ( <CenteredView>{renderHeader()}</CenteredView> )} - <FAB icon="pen-nib" onPress={onPressCompose} /> + <FAB icon="plus" onPress={onPressCompose} /> </View> ) }) diff --git a/src/view/screens/ProfileFollowers.tsx b/src/view/screens/ProfileFollowers.tsx index f7520549e..9f1a9c741 100644 --- a/src/view/screens/ProfileFollowers.tsx +++ b/src/view/screens/ProfileFollowers.tsx @@ -3,7 +3,7 @@ import {View} from 'react-native' import {ViewHeader} from '../com/util/ViewHeader' import {ProfileFollowers as ProfileFollowersComponent} from '../com/profile/ProfileFollowers' import {ScreenParams} from '../routes' -import {useStores} from '../../state' +import {useStores} from 'state/index' export const ProfileFollowers = ({navIdx, visible, params}: ScreenParams) => { const store = useStores() @@ -18,7 +18,7 @@ export const ProfileFollowers = ({navIdx, visible, params}: ScreenParams) => { return ( <View> - <ViewHeader title="Followers" subtitle={`of ${name}`} /> + <ViewHeader title="Followers" /> <ProfileFollowersComponent name={name} /> </View> ) diff --git a/src/view/screens/ProfileFollows.tsx b/src/view/screens/ProfileFollows.tsx index 65e4004e9..1cdb5bccf 100644 --- a/src/view/screens/ProfileFollows.tsx +++ b/src/view/screens/ProfileFollows.tsx @@ -3,7 +3,7 @@ import {View} from 'react-native' import {ViewHeader} from '../com/util/ViewHeader' import {ProfileFollows as ProfileFollowsComponent} from '../com/profile/ProfileFollows' import {ScreenParams} from '../routes' -import {useStores} from '../../state' +import {useStores} from 'state/index' export const ProfileFollows = ({navIdx, visible, params}: ScreenParams) => { const store = useStores() @@ -18,7 +18,7 @@ export const ProfileFollows = ({navIdx, visible, params}: ScreenParams) => { return ( <View> - <ViewHeader title="Followed" subtitle={`by ${name}`} /> + <ViewHeader title="Following" /> <ProfileFollowsComponent name={name} /> </View> ) diff --git a/src/view/screens/Search.tsx b/src/view/screens/Search.tsx index 2a1caab89..2e176d98f 100644 --- a/src/view/screens/Search.tsx +++ b/src/view/screens/Search.tsx @@ -1,41 +1,73 @@ -import React, {useEffect, useState, useMemo, useRef} from 'react' +import React from 'react' import { Keyboard, StyleSheet, TextInput, TouchableOpacity, + TouchableWithoutFeedback, View, } from 'react-native' -import {ViewHeader} from '../com/util/ViewHeader' -import {CenteredView, ScrollView} from '../com/util/Views' -import {SuggestedFollows} from '../com/discover/SuggestedFollows' +import {ScrollView} from '../com/util/Views' +import {observer} from 'mobx-react-lite' import {UserAvatar} from '../com/util/UserAvatar' import {Text} from '../com/util/text/Text' import {ScreenParams} from '../routes' -import {useStores} from '../../state' -import {UserAutocompleteViewModel} from '../../state/models/user-autocomplete-view' -import {s} from '../lib/styles' -import {MagnifyingGlassIcon} from '../lib/icons' -import {usePalette} from '../lib/hooks/usePalette' +import {useStores} from 'state/index' +import {UserAutocompleteViewModel} from 'state/models/user-autocomplete-view' +import {s} from 'lib/styles' +import {MagnifyingGlassIcon} from 'lib/icons' +import {WhoToFollow} from '../com/discover/WhoToFollow' +import {SuggestedPosts} from '../com/discover/SuggestedPosts' +import {ProfileCard} from '../com/profile/ProfileCard' +import {usePalette} from 'lib/hooks/usePalette' +import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' +import {useAnalytics} from 'lib/analytics' -export const Search = ({navIdx, visible, params}: ScreenParams) => { +const MENU_HITSLOP = {left: 10, top: 10, right: 30, bottom: 10} +const FIVE_MIN = 5 * 60 * 1e3 + +export const Search = observer(({navIdx, visible, params}: ScreenParams) => { const pal = usePalette('default') const store = useStores() - const textInput = useRef<TextInput>(null) - const [query, setQuery] = useState<string>('') - const autocompleteView = useMemo<UserAutocompleteViewModel>( + const {track} = useAnalytics() + const scrollElRef = React.useRef<ScrollView>(null) + const onMainScroll = useOnMainScroll(store) + const textInput = React.useRef<TextInput>(null) + const [lastRenderTime, setRenderTime] = React.useState<number>(Date.now()) // used to trigger reloads + const [isInputFocused, setIsInputFocused] = React.useState<boolean>(false) + const [query, setQuery] = React.useState<string>('') + const autocompleteView = React.useMemo<UserAutocompleteViewModel>( () => new UserAutocompleteViewModel(store), [store], ) const {name} = params - useEffect(() => { + const onSoftReset = () => { + scrollElRef.current?.scrollTo({x: 0, y: 0}) + } + + React.useEffect(() => { + const softResetSub = store.onScreenSoftReset(onSoftReset) + const cleanup = () => { + softResetSub.remove() + } + if (visible) { + const now = Date.now() + if (now - lastRenderTime > FIVE_MIN) { + setRenderTime(Date.now()) // trigger reload of suggestions + } store.shell.setMinimalShellMode(false) autocompleteView.setup() store.nav.setTitle(navIdx, 'Search') } - }, [store, visible, name, navIdx, autocompleteView]) + return cleanup + }, [store, visible, name, navIdx, autocompleteView, lastRenderTime]) + + const onPressMenu = () => { + track('ViewHeader:MenuButtonClicked') + store.shell.setMainMenuOpen(true) + } const onChangeQuery = (text: string) => { setQuery(text) @@ -46,87 +78,139 @@ export const Search = ({navIdx, visible, params}: ScreenParams) => { autocompleteView.setActive(false) } } - const onSelect = (handle: string) => { - textInput.current?.blur() - store.nav.navigate(`/profile/${handle}`) + const onPressCancelSearch = () => { + setQuery('') + autocompleteView.setActive(false) } return ( - <View style={[pal.view, styles.container]}> - <ViewHeader title="Search" /> - <CenteredView style={[pal.view, pal.border, styles.inputContainer]}> - <MagnifyingGlassIcon style={[pal.text, styles.inputIcon]} /> - <TextInput - testID="searchTextInput" - ref={textInput} - placeholder="Type your query here..." - placeholderTextColor={pal.colors.textLight} - selectTextOnFocus - returnKeyType="search" - style={[pal.text, styles.input]} - onChangeText={onChangeQuery} - /> - </CenteredView> - <View style={styles.outputContainer}> - {query ? ( - <ScrollView testID="searchScrollView" onScroll={Keyboard.dismiss}> - {autocompleteView.searchRes.map((item, i) => ( - <TouchableOpacity - key={i} - style={[pal.view, pal.border, styles.searchResult]} - onPress={() => onSelect(item.handle)}> - <UserAvatar - handle={item.handle} - displayName={item.displayName} - avatar={item.avatar} - size={36} - /> - <View style={[s.ml10]}> - <Text type="title-sm" style={pal.text}> - {item.displayName || item.handle} - </Text> - <Text style={pal.textLight}>@{item.handle}</Text> - </View> + <TouchableWithoutFeedback onPress={Keyboard.dismiss}> + <ScrollView + ref={scrollElRef} + testID="searchScrollView" + style={[pal.view, styles.container]} + onScroll={onMainScroll} + scrollEventThrottle={100}> + <View style={[pal.view, pal.border, styles.header]}> + <TouchableOpacity + testID="viewHeaderBackOrMenuBtn" + onPress={onPressMenu} + hitSlop={MENU_HITSLOP} + style={styles.headerMenuBtn}> + <UserAvatar + size={30} + handle={store.me.handle} + displayName={store.me.displayName} + avatar={store.me.avatar} + /> + </TouchableOpacity> + <View + style={[ + {backgroundColor: pal.colors.backgroundLight}, + styles.headerSearchContainer, + ]}> + <MagnifyingGlassIcon + style={[pal.icon, styles.headerSearchIcon]} + size={21} + /> + <TextInput + testID="searchTextInput" + ref={textInput} + placeholder="Search" + placeholderTextColor={pal.colors.textLight} + selectTextOnFocus + returnKeyType="search" + value={query} + style={[pal.text, styles.headerSearchInput]} + onFocus={() => setIsInputFocused(true)} + onBlur={() => setIsInputFocused(false)} + onChangeText={onChangeQuery} + /> + </View> + {query ? ( + <View style={styles.headerCancelBtn}> + <TouchableOpacity onPress={onPressCancelSearch}> + <Text>Cancel</Text> </TouchableOpacity> + </View> + ) : undefined} + </View> + {query && autocompleteView.searchRes.length ? ( + <> + {autocompleteView.searchRes.map(item => ( + <ProfileCard + key={item.did} + handle={item.handle} + displayName={item.displayName} + avatar={item.avatar} + /> ))} + </> + ) : query && !autocompleteView.searchRes.length ? ( + <View> + <Text style={[pal.textLight, styles.searchPrompt]}> + No results found for {autocompleteView.prefix} + </Text> + </View> + ) : isInputFocused ? ( + <View> + <Text style={[pal.textLight, styles.searchPrompt]}> + Search for users on the network + </Text> + </View> + ) : ( + <ScrollView onScroll={Keyboard.dismiss}> + <WhoToFollow key={`wtf-${lastRenderTime}`} /> + <SuggestedPosts key={`sp-${lastRenderTime}`} /> <View style={s.footerSpacer} /> </ScrollView> - ) : ( - <SuggestedFollows asLinks /> )} - </View> - </View> + <View style={s.footerSpacer} /> + </ScrollView> + </TouchableWithoutFeedback> ) -} +}) const styles = StyleSheet.create({ container: { flex: 1, }, - inputContainer: { + header: { flexDirection: 'row', - paddingVertical: 16, - paddingHorizontal: 16, - borderTopWidth: 1, + alignItems: 'center', + paddingHorizontal: 12, + paddingTop: 4, + marginBottom: 14, }, - inputIcon: { - marginRight: 10, - alignSelf: 'center', + headerMenuBtn: { + width: 40, + height: 30, + marginLeft: 6, }, - input: { + headerSearchContainer: { flex: 1, - fontSize: 16, + flexDirection: 'row', + alignItems: 'center', + borderRadius: 30, + paddingHorizontal: 12, + paddingVertical: 8, }, - - outputContainer: { + headerSearchIcon: { + marginRight: 6, + alignSelf: 'center', + }, + headerSearchInput: { flex: 1, + fontSize: 17, + }, + headerCancelBtn: { + width: 60, + paddingLeft: 10, }, - searchResult: { - flexDirection: 'row', - borderTopWidth: 1, - paddingVertical: 12, - paddingHorizontal: 16, + searchPrompt: { + textAlign: 'center', + paddingTop: 10, }, }) diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx index c2953b59d..94f5acd93 100644 --- a/src/view/screens/Settings.tsx +++ b/src/view/screens/Settings.tsx @@ -7,17 +7,20 @@ import { } from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {observer} from 'mobx-react-lite' -import {useStores} from '../../state' +import * as AppInfo from 'lib/app-info' +import {useStores} from 'state/index' import {ScreenParams} from '../routes' -import {s} from '../lib/styles' +import {s} from 'lib/styles' import {ScrollView} from '../com/util/Views' import {ViewHeader} from '../com/util/ViewHeader' import {Link} from '../com/util/Link' import {Text} from '../com/util/text/Text' import * as Toast from '../com/util/Toast' import {UserAvatar} from '../com/util/UserAvatar' -import {usePalette} from '../lib/hooks/usePalette' -import {AccountData} from '../../state/models/session' +import {usePalette} from 'lib/hooks/usePalette' +import {AccountData} from 'state/models/session' +import {useAnalytics} from 'lib/analytics' +import {DeleteAccountModal} from 'state/models/shell-ui' export const Settings = observer(function Settings({ navIdx, @@ -25,9 +28,14 @@ export const Settings = observer(function Settings({ }: ScreenParams) { const pal = usePalette('default') const store = useStores() + const {screen, track} = useAnalytics() const [isSwitching, setIsSwitching] = React.useState(false) useEffect(() => { + screen('Settings') + }, [screen]) + + useEffect(() => { if (!visible) { return } @@ -36,22 +44,30 @@ export const Settings = observer(function Settings({ }, [visible, store, navIdx]) const onPressSwitchAccount = async (acct: AccountData) => { + track('Settings:SwitchAccountButtonClicked') setIsSwitching(true) if (await store.session.resumeSession(acct)) { setIsSwitching(false) + store.nav.tab.fixedTabReset() Toast.show(`Signed in as ${acct.displayName || acct.handle}`) return } setIsSwitching(false) Toast.show('Sorry! We need you to enter your password.') + store.nav.tab.fixedTabReset() store.session.clear() } const onPressAddAccount = () => { + track('Settings:AddAccountButtonClicked') store.session.clear() } const onPressSignout = () => { + track('Settings:SignOutButtonClicked') store.session.logout() } + const onPressDeleteAccount = () => { + store.shell.openModal(new DeleteAccountModal()) + } return ( <View style={[s.h100pct]} testID="settingsScreen"> @@ -143,22 +159,34 @@ export const Settings = observer(function Settings({ </Text> </View> </TouchableOpacity> + <View style={styles.spacer} /> <Text type="sm-medium" style={[s.mb5]}> + Danger zone + </Text> + <TouchableOpacity + style={[pal.view, s.p10, s.mb10]} + onPress={onPressDeleteAccount}> + <Text style={pal.textLight}>Delete my account</Text> + </TouchableOpacity> + <Text type="sm-medium" style={[s.mt10, s.mb5]}> Developer tools </Text> <Link style={[pal.view, s.p10, s.mb2]} href="/sys/log" title="System log"> - <Text style={pal.link}>System log</Text> + <Text style={pal.textLight}>System log</Text> </Link> <Link style={[pal.view, s.p10, s.mb2]} href="/sys/debug" title="Debug tools"> - <Text style={pal.link}>Storybook</Text> + <Text style={pal.textLight}>Storybook</Text> </Link> + <Text type="sm" style={[s.mt10, pal.textLight]}> + Build version {AppInfo.appVersion} ({AppInfo.buildVersion}) + </Text> <View style={s.footerSpacer} /> </View> </ScrollView> |