diff options
-rw-r--r-- | LICENSE | 2 | ||||
-rw-r--r-- | eas.json | 7 | ||||
-rw-r--r-- | package.json | 4 | ||||
-rw-r--r-- | src/App.native.tsx | 26 | ||||
-rw-r--r-- | src/App.web.tsx | 28 | ||||
-rw-r--r-- | src/Navigation.tsx | 5 | ||||
-rw-r--r-- | src/lib/ThemeContext.tsx | 54 | ||||
-rw-r--r-- | src/lib/api/feed-manip.ts | 5 | ||||
-rw-r--r-- | src/lib/constants.ts | 107 | ||||
-rw-r--r-- | src/lib/react-query.ts | 3 | ||||
-rw-r--r-- | src/view/com/auth/onboarding/RecommendedFeeds.tsx | 70 | ||||
-rw-r--r-- | src/view/com/auth/onboarding/RecommendedFeedsItem.tsx | 11 | ||||
-rw-r--r-- | src/view/shell/Drawer.tsx | 9 | ||||
-rw-r--r-- | src/view/shell/bottom-bar/BottomBarWeb.tsx | 28 | ||||
-rw-r--r-- | yarn.lock | 15 |
15 files changed, 185 insertions, 189 deletions
diff --git a/LICENSE b/LICENSE index 801636644..d6da98bd5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2023 Bluesky PBLLC +Copyright 2023 Bluesky PBC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/eas.json b/eas.json index 240bc017d..69e5c94d6 100644 --- a/eas.json +++ b/eas.json @@ -33,6 +33,13 @@ "resourceClass": "m-large" }, "channel": "production" + }, + "dev-android-apk": { + "developmentClient": true, + "android": { + "buildType": "apk", + "gradleCommand": ":app:assembleRelease" + } } }, "submit": { diff --git a/package.json b/package.json index f16330bed..1cf80eda4 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "e2e:mock-server": "ts-node __e2e__/mock-server.ts", "e2e:metro": "RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios", "e2e:build": "detox build -c ios.sim.debug", - "e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all" + "e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all", + "build:apk": "eas build -p android --profile dev-android-apk" }, "dependencies": { "@atproto/api": "^0.6.12", @@ -52,6 +53,7 @@ "@segment/analytics-react-native": "^2.10.1", "@segment/sovran-react-native": "^0.4.5", "@sentry/react-native": "5.5.0", + "@tanstack/react-query": "^4.33.0", "@tiptap/core": "^2.0.0-beta.220", "@tiptap/extension-document": "^2.0.0-beta.220", "@tiptap/extension-hard-break": "^2.0.3", diff --git a/src/App.native.tsx b/src/App.native.tsx index 09782a875..d43155bf3 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -16,6 +16,8 @@ import * as notifications from 'lib/notifications/notifications' import * as analytics from 'lib/analytics/analytics' import * as Toast from './view/com/util/Toast' import {handleLink} from './Navigation' +import {QueryClientProvider} from '@tanstack/react-query' +import {queryClient} from 'lib/react-query' SplashScreen.preventAutoHideAsync() @@ -51,17 +53,19 @@ const App = observer(function AppImpl() { return null } return ( - <ThemeProvider theme={rootStore.shell.colorMode}> - <RootSiblingParent> - <analytics.Provider> - <RootStoreProvider value={rootStore}> - <GestureHandlerRootView style={s.h100pct}> - <Shell /> - </GestureHandlerRootView> - </RootStoreProvider> - </analytics.Provider> - </RootSiblingParent> - </ThemeProvider> + <QueryClientProvider client={queryClient}> + <ThemeProvider theme={rootStore.shell.colorMode}> + <RootSiblingParent> + <analytics.Provider> + <RootStoreProvider value={rootStore}> + <GestureHandlerRootView style={s.h100pct}> + <Shell /> + </GestureHandlerRootView> + </RootStoreProvider> + </analytics.Provider> + </RootSiblingParent> + </ThemeProvider> + </QueryClientProvider> ) }) diff --git a/src/App.web.tsx b/src/App.web.tsx index 41a7189d3..a9123cc58 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -9,6 +9,8 @@ import {Shell} from './view/shell/index' import {ToastContainer} from './view/com/util/Toast.web' import {ThemeProvider} from 'lib/ThemeContext' import {observer} from 'mobx-react-lite' +import {QueryClientProvider} from '@tanstack/react-query' +import {queryClient} from 'lib/react-query' const App = observer(function AppImpl() { const [rootStore, setRootStore] = useState<RootStoreModel | undefined>( @@ -30,18 +32,20 @@ const App = observer(function AppImpl() { } return ( - <ThemeProvider theme={rootStore.shell.colorMode}> - <RootSiblingParent> - <analytics.Provider> - <RootStoreProvider value={rootStore}> - <SafeAreaProvider> - <Shell /> - </SafeAreaProvider> - <ToastContainer /> - </RootStoreProvider> - </analytics.Provider> - </RootSiblingParent> - </ThemeProvider> + <QueryClientProvider client={queryClient}> + <ThemeProvider theme={rootStore.shell.colorMode}> + <RootSiblingParent> + <analytics.Provider> + <RootStoreProvider value={rootStore}> + <SafeAreaProvider> + <Shell /> + </SafeAreaProvider> + <ToastContainer /> + </RootStoreProvider> + </analytics.Provider> + </RootSiblingParent> + </ThemeProvider> + </QueryClientProvider> ) }) diff --git a/src/Navigation.tsx b/src/Navigation.tsx index dac70dfc7..c16ff3a8c 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -348,7 +348,6 @@ const MyProfileTabNavigator = observer(function MyProfileTabNavigatorImpl() { component={ProfileScreen} initialParams={{ name: store.me.did, - hideBackButton: true, }} /> {commonScreens(MyProfileTab as typeof HomeTab)} @@ -362,7 +361,9 @@ const MyProfileTabNavigator = observer(function MyProfileTabNavigatorImpl() { */ const FlatNavigator = observer(function FlatNavigatorImpl() { const pal = usePalette('default') - const unreadCountLabel = useStores().me.notifications.unreadCountLabel + const store = useStores() + const unreadCountLabel = store.me.notifications.unreadCountLabel + const title = (page: string) => bskyTitle(page, unreadCountLabel) return ( <Flat.Navigator diff --git a/src/lib/ThemeContext.tsx b/src/lib/ThemeContext.tsx index fe25dde54..483c50c42 100644 --- a/src/lib/ThemeContext.tsx +++ b/src/lib/ThemeContext.tsx @@ -1,8 +1,9 @@ +import {isWeb} from 'platform/detection' import React, {ReactNode, createContext, useContext} from 'react' import { AppState, TextStyle, - useColorScheme, + useColorScheme as useColorScheme_BUGGY, ViewStyle, ColorSchemeName, } from 'react-native' @@ -92,33 +93,44 @@ export const ThemeContext = createContext<Theme>(defaultTheme) export const useTheme = () => useContext(ThemeContext) -export const ThemeProvider: React.FC<ThemeProviderProps> = ({ - theme, - children, -}) => { - const colorSchemeFromRN = useColorScheme() - const [nativeColorScheme, setNativeColorScheme] = - React.useState<ColorSchemeName>(colorSchemeFromRN) +function getTheme(theme: ColorSchemeName) { + return theme === 'dark' ? darkTheme : defaultTheme +} + +/** + * With RN iOS, we can only "trust" the color scheme reported while the app is + * active. This is a workaround until the bug is fixed upstream. + * + * @see https://github.com/bluesky-social/social-app/pull/1417#issuecomment-1719868504 + * @see https://github.com/facebook/react-native/pull/39439 + */ +function useColorScheme_FIXED() { + const colorScheme = useColorScheme_BUGGY() + const [currentColorScheme, setCurrentColorScheme] = + React.useState<ColorSchemeName>(colorScheme) React.useEffect(() => { + // we don't need to be updating state on web + if (isWeb) return const subscription = AppState.addEventListener('change', state => { const isActive = state === 'active' - if (!isActive) return - - setNativeColorScheme(colorSchemeFromRN) + setCurrentColorScheme(colorScheme) }) return () => subscription.remove() - }, [colorSchemeFromRN]) + }, [colorScheme]) + + return isWeb ? colorScheme : currentColorScheme +} - const value = - theme === 'system' - ? nativeColorScheme === 'dark' - ? darkTheme - : defaultTheme - : theme === 'dark' - ? darkTheme - : defaultTheme +export const ThemeProvider: React.FC<ThemeProviderProps> = ({ + theme, + children, +}) => { + const colorScheme = useColorScheme_FIXED() + const themeValue = getTheme(theme === 'system' ? colorScheme : theme) - return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider> + return ( + <ThemeContext.Provider value={themeValue}>{children}</ThemeContext.Provider> + ) } diff --git a/src/lib/api/feed-manip.ts b/src/lib/api/feed-manip.ts index 60b0f2641..149859ea9 100644 --- a/src/lib/api/feed-manip.ts +++ b/src/lib/api/feed-manip.ts @@ -281,7 +281,10 @@ export class FeedTuner { function getSelfReplyUri(item: FeedViewPost): string | undefined { if (item.reply) { - if (AppBskyFeedDefs.isPostView(item.reply.parent)) { + if ( + AppBskyFeedDefs.isPostView(item.reply.parent) && + !AppBskyFeedDefs.isReasonRepost(item.reason) // don't thread reposted self-replies + ) { return item.reply.parent.author.did === item.post.author.did ? item.reply.parent.uri : undefined diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 94551e6ef..001cdf8c3 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -148,110 +148,3 @@ export const HITSLOP_10 = createHitslop(10) export const HITSLOP_20 = createHitslop(20) export const HITSLOP_30 = createHitslop(30) export const BACK_HITSLOP = HITSLOP_30 - -export const RECOMMENDED_FEEDS = [ - { - did: 'did:plc:hsqwcidfez66lwm3gxhfv5in', - rkey: 'aaaf2pqeodmpy', - }, - { - did: 'did:plc:gekdk2nd47gkk3utfz2xf7cn', - rkey: 'aaap4tbjcfe5y', - }, - { - did: 'did:plc:5rw2on4i56btlcajojaxwcat', - rkey: 'aaao6g552b33o', - }, - { - did: 'did:plc:jfhpnnst6flqway4eaeqzj2a', - rkey: 'for-science', - }, - { - did: 'did:plc:7q4nnnxawajbfaq7to5dpbsy', - rkey: 'bsky-news', - }, - { - did: 'did:plc:jcoy7v3a2t4rcfdh6i4kza25', - rkey: 'astro', - }, - { - did: 'did:plc:tenurhgjptubkk5zf5qhi3og', - rkey: 'h-nba', - }, - { - did: 'did:plc:vpkhqolt662uhesyj6nxm7ys', - rkey: 'devfeed', - }, - { - did: 'did:plc:cndfx4udwgvpjaakvxvh7wm5', - rkey: 'flipboard-tech', - }, - { - did: 'did:plc:w4xbfzo7kqfes5zb7r6qv3rw', - rkey: 'blacksky', - }, - { - did: 'did:plc:lptjvw6ut224kwrj7ub3sqbe', - rkey: 'aaaotfjzjplna', - }, - { - did: 'did:plc:gkvpokm7ec5j5yxls6xk4e3z', - rkey: 'formula-one', - }, - { - did: 'did:plc:q6gjnaw2blty4crticxkmujt', - rkey: 'positivifeed', - }, - { - did: 'did:plc:l72uci4styb4jucsgcrrj5ap', - rkey: 'aaao5dzfm36u4', - }, - { - did: 'did:plc:k3jkadxv5kkjgs6boyon7m6n', - rkey: 'aaaavlyvqzst2', - }, - { - did: 'did:plc:nkahctfdi6bxk72umytfwghw', - rkey: 'aaado2uvfsc6w', - }, - { - did: 'did:plc:epihigio3d7un7u3gpqiy5gv', - rkey: 'aaaekwsc7zsvs', - }, - { - did: 'did:plc:qiknc4t5rq7yngvz7g4aezq7', - rkey: 'aaaejxlobe474', - }, - { - did: 'did:plc:mlq4aycufcuolr7ax6sezpc4', - rkey: 'aaaoudweck6uy', - }, - { - did: 'did:plc:rcez5hcvq3vzlu5x7xrjyccg', - rkey: 'aaadzjxbcddzi', - }, - { - did: 'did:plc:lnxbuzaenlwjrncx6sc4cfdr', - rkey: 'aaab2vesjtszc', - }, - { - did: 'did:plc:x3cya3wkt4n6u4ihmvpsc5if', - rkey: 'aaacynbxwimok', - }, - { - did: 'did:plc:abv47bjgzjgoh3yrygwoi36x', - rkey: 'aaagt6amuur5e', - }, - { - did: 'did:plc:ffkgesg3jsv2j7aagkzrtcvt', - rkey: 'aaacjerk7gwek', - }, - { - did: 'did:plc:geoqe3qls5mwezckxxsewys2', - rkey: 'aaai43yetqshu', - }, - { - did: 'did:plc:2wqomm3tjqbgktbrfwgvrw34', - rkey: 'authors', - }, -] diff --git a/src/lib/react-query.ts b/src/lib/react-query.ts new file mode 100644 index 000000000..2a8f1d759 --- /dev/null +++ b/src/lib/react-query.ts @@ -0,0 +1,3 @@ +import {QueryClient} from '@tanstack/react-query' + +export const queryClient = new QueryClient() diff --git a/src/view/com/auth/onboarding/RecommendedFeeds.tsx b/src/view/com/auth/onboarding/RecommendedFeeds.tsx index 99cdcafd0..8e29a5895 100644 --- a/src/view/com/auth/onboarding/RecommendedFeeds.tsx +++ b/src/view/com/auth/onboarding/RecommendedFeeds.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {FlatList, StyleSheet, View} from 'react-native' +import {ActivityIndicator, FlatList, StyleSheet, View} from 'react-native' import {observer} from 'mobx-react-lite' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {TabletOrDesktop, Mobile} from 'view/com/util/layouts/Breakpoints' @@ -10,7 +10,10 @@ import {Button} from 'view/com/util/forms/Button' import {RecommendedFeedsItem} from './RecommendedFeedsItem' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {usePalette} from 'lib/hooks/usePalette' -import {RECOMMENDED_FEEDS} from 'lib/constants' +import {useQuery} from '@tanstack/react-query' +import {useStores} from 'state/index' +import {CustomFeedModel} from 'state/models/feeds/custom-feed' +import {ErrorMessage} from 'view/com/util/error/ErrorMessage' type Props = { next: () => void @@ -18,8 +21,31 @@ type Props = { export const RecommendedFeeds = observer(function RecommendedFeedsImpl({ next, }: Props) { + const store = useStores() const pal = usePalette('default') const {isTabletOrMobile} = useWebMediaQueries() + const {isLoading, data: recommendedFeeds} = useQuery({ + staleTime: Infinity, // fixed list rn, never refetch + queryKey: ['onboarding', 'recommended_feeds'], + async queryFn() { + try { + const { + data: {feeds}, + success, + } = await store.agent.app.bsky.feed.getSuggestedFeeds() + + if (!success) return + + return (feeds.length ? feeds : []).map(feed => { + return new CustomFeedModel(store, feed) + }) + } catch (e) { + return + } + }, + }) + + const hasFeeds = recommendedFeeds && recommendedFeeds.length const title = ( <> @@ -86,12 +112,20 @@ export const RecommendedFeeds = observer(function RecommendedFeedsImpl({ horizontal titleStyle={isTabletOrMobile ? undefined : {minWidth: 470}} contentStyle={{paddingHorizontal: 0}}> - <FlatList - data={RECOMMENDED_FEEDS} - renderItem={({item}) => <RecommendedFeedsItem {...item} />} - keyExtractor={item => item.did + item.rkey} - style={{flex: 1}} - /> + {hasFeeds ? ( + <FlatList + data={recommendedFeeds} + renderItem={({item}) => <RecommendedFeedsItem item={item} />} + keyExtractor={item => item.uri} + style={{flex: 1}} + /> + ) : isLoading ? ( + <View> + <ActivityIndicator size="large" /> + </View> + ) : ( + <ErrorMessage message="Failed to load recommended feeds" /> + )} </TitleColumnLayout> </TabletOrDesktop> <Mobile> @@ -106,12 +140,20 @@ export const RecommendedFeeds = observer(function RecommendedFeedsImpl({ pinned feeds. </Text> - <FlatList - data={RECOMMENDED_FEEDS} - renderItem={({item}) => <RecommendedFeedsItem {...item} />} - keyExtractor={item => item.did + item.rkey} - style={{flex: 1}} - /> + {hasFeeds ? ( + <FlatList + data={recommendedFeeds} + renderItem={({item}) => <RecommendedFeedsItem item={item} />} + keyExtractor={item => item.uri} + style={{flex: 1}} + /> + ) : isLoading ? ( + <View> + <ActivityIndicator size="large" /> + </View> + ) : ( + <ErrorMessage message="Failed to load recommended feeds" /> + )} <Button onPress={next} diff --git a/src/view/com/auth/onboarding/RecommendedFeedsItem.tsx b/src/view/com/auth/onboarding/RecommendedFeedsItem.tsx index e5d12273a..d130dc138 100644 --- a/src/view/com/auth/onboarding/RecommendedFeedsItem.tsx +++ b/src/view/com/auth/onboarding/RecommendedFeedsItem.tsx @@ -8,22 +8,17 @@ import {UserAvatar} from 'view/com/util/UserAvatar' import * as Toast from 'view/com/util/Toast' import {HeartIcon} from 'lib/icons' import {usePalette} from 'lib/hooks/usePalette' -import {useCustomFeed} from 'lib/hooks/useCustomFeed' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {makeRecordUri} from 'lib/strings/url-helpers' import {sanitizeHandle} from 'lib/strings/handles' +import {CustomFeedModel} from 'state/models/feeds/custom-feed' export const RecommendedFeedsItem = observer(function RecommendedFeedsItemImpl({ - did, - rkey, + item, }: { - did: string - rkey: string + item: CustomFeedModel }) { const {isMobile} = useWebMediaQueries() const pal = usePalette('default') - const uri = makeRecordUri(did, 'app.bsky.feed.generator', rkey) - const item = useCustomFeed(uri) if (!item) return null const onToggle = async () => { if (item.isSaved) { diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx index 3379d0501..67092938e 100644 --- a/src/view/shell/Drawer.tsx +++ b/src/view/shell/Drawer.tsx @@ -64,8 +64,13 @@ export const DrawerContent = observer(function DrawerContentImpl() { const state = navigation.getState() store.shell.closeDrawer() if (isWeb) { - // @ts-ignore must be Home, Search, Notifications, or MyProfile - navigation.navigate(tab) + // hack because we have flat navigator for web and MyProfile does not exist on the web navigator -ansh + if (tab === 'MyProfile') { + navigation.navigate('Profile', {name: store.me.handle}) + } else { + // @ts-ignore must be Home, Search, Notifications, or MyProfile + navigation.navigate(tab) + } } else { const tabState = getTabState(state, tab) if (tabState === TabState.InsideAtRoot) { diff --git a/src/view/shell/bottom-bar/BottomBarWeb.tsx b/src/view/shell/bottom-bar/BottomBarWeb.tsx index ee575c217..af70d3364 100644 --- a/src/view/shell/bottom-bar/BottomBarWeb.tsx +++ b/src/view/shell/bottom-bar/BottomBarWeb.tsx @@ -18,10 +18,12 @@ import { SatelliteDishIcon, SatelliteDishIconSolid, UserIcon, + UserIconSolid, } from 'lib/icons' import {Link} from 'view/com/util/Link' import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' import {makeProfileLink} from 'lib/routes/links' +import {CommonNavigatorParams} from 'lib/routes/types' export const BottomBarWeb = observer(function BottomBarWebImpl() { const store = useStores() @@ -89,13 +91,16 @@ export const BottomBarWeb = observer(function BottomBarWebImpl() { }} </NavItem> <NavItem routeName="Profile" href={makeProfileLink(store.me)}> - {() => ( - <UserIcon - size={28} - strokeWidth={1.5} - style={[styles.ctrlIcon, pal.text, styles.profileIcon]} - /> - )} + {({isActive}) => { + const Icon = isActive ? UserIconSolid : UserIcon + return ( + <Icon + size={28} + strokeWidth={1.5} + style={[styles.ctrlIcon, pal.text, styles.profileIcon]} + /> + ) + }} </NavItem> </Animated.View> ) @@ -107,7 +112,14 @@ const NavItem: React.FC<{ routeName: string }> = ({children, href, routeName}) => { const currentRoute = useNavigationState(getCurrentRoute) - const isActive = isTab(currentRoute.name, routeName) + const store = useStores() + const isActive = + currentRoute.name === 'Profile' + ? isTab(currentRoute.name, routeName) && + (currentRoute.params as CommonNavigatorParams['Profile']).name === + store.me.handle + : isTab(currentRoute.name, routeName) + return ( <Link href={href} style={styles.ctrl}> {children({isActive})} diff --git a/yarn.lock b/yarn.lock index 5875b12c7..59297e6cb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6005,6 +6005,19 @@ "@svgr/plugin-svgo" "^5.5.0" loader-utils "^2.0.0" +"@tanstack/query-core@4.33.0": + version "4.33.0" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.33.0.tgz#7756da9a75a424e521622b1d84eb55b7a2b33715" + integrity sha512-qYu73ptvnzRh6se2nyBIDHGBQvPY1XXl3yR769B7B6mIDD7s+EZhdlWHQ67JI6UOTFRaI7wupnTnwJ3gE0Mr/g== + +"@tanstack/react-query@^4.33.0": + version "4.33.0" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.33.0.tgz#e927b0343a6ecaa948fee59e9ca98fe561062638" + integrity sha512-97nGbmDK0/m0B86BdiXzx3EW9RcDYKpnyL2+WwyuLHEgpfThYAnXFaMMmnTDuAO4bQJXEhflumIEUfKmP7ESGA== + dependencies: + "@tanstack/query-core" "4.33.0" + use-sync-external-store "^1.2.0" + "@testing-library/jest-native@^5.4.1": version "5.4.2" resolved "https://registry.yarnpkg.com/@testing-library/jest-native/-/jest-native-5.4.2.tgz#6b0c987cc57f8d900763e763025d00d26ccbc85f" @@ -19293,7 +19306,7 @@ use-sidecar@^1.1.2: detect-node-es "^1.1.0" tslib "^2.0.0" -use-sync-external-store@^1.0.0: +use-sync-external-store@^1.0.0, use-sync-external-store@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== |