Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client
122
fork

Configure Feed

Select the types of activity you want to include in your feed.

Top/Latest for hashtags (#3625)

* Split HashtagScreen into two components

* Hashtag tabs

* Visual fixes

authored by

dan and committed by
GitHub
d3c0b48d c0ca8915

+177 -72
+139 -59
src/screens/Hashtag.tsx
··· 1 1 import React from 'react' 2 - import {ListRenderItemInfo, Pressable} from 'react-native' 2 + import {ListRenderItemInfo, Pressable, StyleSheet, View} from 'react-native' 3 3 import {PostView} from '@atproto/api/dist/client/types/app/bsky/feed/defs' 4 4 import {msg} from '@lingui/macro' 5 5 import {useLingui} from '@lingui/react' 6 6 import {useFocusEffect} from '@react-navigation/native' 7 7 import {NativeStackScreenProps} from '@react-navigation/native-stack' 8 8 9 + import {usePalette} from '#/lib/hooks/usePalette' 9 10 import {HITSLOP_10} from 'lib/constants' 10 11 import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender' 11 12 import {CommonNavigatorParams} from 'lib/routes/types' ··· 13 14 import {cleanError} from 'lib/strings/errors' 14 15 import {sanitizeHandle} from 'lib/strings/handles' 15 16 import {enforceLen} from 'lib/strings/helpers' 16 - import {isNative} from 'platform/detection' 17 + import {isNative, isWeb} from 'platform/detection' 17 18 import {useSearchPostsQuery} from 'state/queries/search-posts' 18 - import {useSetMinimalShellMode} from 'state/shell' 19 + import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from 'state/shell' 20 + import {Pager} from '#/view/com/pager/Pager' 21 + import {TabBar} from '#/view/com/pager/TabBar' 22 + import {CenteredView} from '#/view/com/util/Views' 19 23 import {Post} from 'view/com/post/Post' 20 24 import {List} from 'view/com/util/List' 21 25 import {ViewHeader} from 'view/com/util/ViewHeader' 22 26 import {ArrowOutOfBox_Stroke2_Corner0_Rounded} from '#/components/icons/ArrowOutOfBox' 23 - import { 24 - ListFooter, 25 - ListHeaderDesktop, 26 - ListMaybePlaceholder, 27 - } from '#/components/Lists' 27 + import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' 28 28 29 29 const renderItem = ({item}: ListRenderItemInfo<PostView>) => { 30 30 return <Post post={item} /> ··· 38 38 route, 39 39 }: NativeStackScreenProps<CommonNavigatorParams, 'Hashtag'>) { 40 40 const {tag, author} = route.params 41 - const setMinimalShellMode = useSetMinimalShellMode() 42 41 const {_} = useLingui() 43 - const initialNumToRender = useInitialNumToRender() 44 - const [isPTR, setIsPTR] = React.useState(false) 42 + const pal = usePalette('default') 45 43 46 44 const fullTag = React.useMemo(() => { 47 45 return `#${decodeURIComponent(tag)}` 48 46 }, [tag]) 49 - 50 - const queryParam = React.useMemo(() => { 51 - if (!author) return fullTag 52 - return `${fullTag} from:${sanitizeHandle(author)}` 53 - }, [fullTag, author]) 54 47 55 48 const headerTitle = React.useMemo(() => { 56 49 return enforceLen(fullTag.toLowerCase(), 24, true, 'middle') ··· 61 54 return sanitizeHandle(author) 62 55 }, [author]) 63 56 57 + const onShare = React.useCallback(() => { 58 + const url = new URL('https://bsky.app') 59 + url.pathname = `/hashtag/${decodeURIComponent(tag)}` 60 + if (author) { 61 + url.searchParams.set('author', author) 62 + } 63 + shareUrl(url.toString()) 64 + }, [tag, author]) 65 + 66 + const [activeTab, setActiveTab] = React.useState(0) 67 + const setMinimalShellMode = useSetMinimalShellMode() 68 + const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled() 69 + 70 + useFocusEffect( 71 + React.useCallback(() => { 72 + setMinimalShellMode(false) 73 + }, [setMinimalShellMode]), 74 + ) 75 + 76 + const onPageSelected = React.useCallback( 77 + (index: number) => { 78 + setMinimalShellMode(false) 79 + setDrawerSwipeDisabled(index > 0) 80 + setActiveTab(index) 81 + }, 82 + [setDrawerSwipeDisabled, setMinimalShellMode], 83 + ) 84 + 85 + const sections = React.useMemo(() => { 86 + return [ 87 + { 88 + title: _(msg`Top`), 89 + component: ( 90 + <HashtagScreenTab 91 + fullTag={fullTag} 92 + author={author} 93 + sort="top" 94 + active={activeTab === 0} 95 + /> 96 + ), 97 + }, 98 + { 99 + title: _(msg`Latest`), 100 + component: ( 101 + <HashtagScreenTab 102 + fullTag={fullTag} 103 + author={author} 104 + sort="latest" 105 + active={activeTab === 1} 106 + /> 107 + ), 108 + }, 109 + ] 110 + }, [_, fullTag, author, activeTab]) 111 + 112 + return ( 113 + <> 114 + <CenteredView sideBorders style={[pal.border, pal.view]}> 115 + <ViewHeader 116 + showOnDesktop 117 + title={headerTitle} 118 + subtitle={author ? _(msg`From @${sanitizedAuthor}`) : undefined} 119 + canGoBack 120 + renderButton={ 121 + isNative 122 + ? () => ( 123 + <Pressable 124 + accessibilityRole="button" 125 + onPress={onShare} 126 + hitSlop={HITSLOP_10}> 127 + <ArrowOutOfBox_Stroke2_Corner0_Rounded 128 + size="lg" 129 + onPress={onShare} 130 + /> 131 + </Pressable> 132 + ) 133 + : undefined 134 + } 135 + /> 136 + </CenteredView> 137 + <Pager 138 + onPageSelected={onPageSelected} 139 + renderTabBar={props => ( 140 + <CenteredView 141 + sideBorders 142 + style={[pal.border, pal.view, styles.tabBarContainer]}> 143 + <TabBar items={sections.map(section => section.title)} {...props} /> 144 + </CenteredView> 145 + )} 146 + initialPage={0}> 147 + {sections.map((section, i) => ( 148 + <View key={i}>{section.component}</View> 149 + ))} 150 + </Pager> 151 + </> 152 + ) 153 + } 154 + 155 + function HashtagScreenTab({ 156 + fullTag, 157 + author, 158 + sort, 159 + active, 160 + }: { 161 + fullTag: string 162 + author: string | undefined 163 + sort: 'top' | 'latest' 164 + active: boolean 165 + }) { 166 + const {_} = useLingui() 167 + const initialNumToRender = useInitialNumToRender() 168 + const [isPTR, setIsPTR] = React.useState(false) 169 + 170 + const queryParam = React.useMemo(() => { 171 + if (!author) return fullTag 172 + return `${fullTag} from:${sanitizeHandle(author)}` 173 + }, [fullTag, author]) 174 + 64 175 const { 65 176 data, 177 + isFetched, 66 178 isFetchingNextPage, 67 179 isLoading, 68 180 isError, ··· 70 182 refetch, 71 183 fetchNextPage, 72 184 hasNextPage, 73 - } = useSearchPostsQuery({query: queryParam}) 185 + } = useSearchPostsQuery({query: queryParam, sort, enabled: active}) 74 186 75 187 const posts = React.useMemo(() => { 76 188 return data?.pages.flatMap(page => page.posts) || [] 77 189 }, [data]) 78 190 79 - useFocusEffect( 80 - React.useCallback(() => { 81 - setMinimalShellMode(false) 82 - }, [setMinimalShellMode]), 83 - ) 84 - 85 - const onShare = React.useCallback(() => { 86 - const url = new URL('https://bsky.app') 87 - url.pathname = `/hashtag/${decodeURIComponent(tag)}` 88 - if (author) { 89 - url.searchParams.set('author', author) 90 - } 91 - shareUrl(url.toString()) 92 - }, [tag, author]) 93 - 94 191 const onRefresh = React.useCallback(async () => { 95 192 setIsPTR(true) 96 193 await refetch() ··· 104 201 105 202 return ( 106 203 <> 107 - <ViewHeader 108 - title={headerTitle} 109 - subtitle={author ? _(msg`From @${sanitizedAuthor}`) : undefined} 110 - canGoBack 111 - renderButton={ 112 - isNative 113 - ? () => ( 114 - <Pressable 115 - accessibilityRole="button" 116 - onPress={onShare} 117 - hitSlop={HITSLOP_10}> 118 - <ArrowOutOfBox_Stroke2_Corner0_Rounded 119 - size="lg" 120 - onPress={onShare} 121 - /> 122 - </Pressable> 123 - ) 124 - : undefined 125 - } 126 - /> 127 204 {posts.length < 1 ? ( 128 205 <ListMaybePlaceholder 129 - isLoading={isLoading} 206 + isLoading={isLoading || !isFetched} 130 207 isError={isError} 131 208 onRetry={refetch} 132 209 emptyType="results" ··· 143 220 onEndReachedThreshold={4} 144 221 // @ts-ignore web only -prf 145 222 desktopFixedHeight 146 - ListHeaderComponent={ 147 - <ListHeaderDesktop 148 - title={headerTitle} 149 - subtitle={author ? _(msg`From @${sanitizedAuthor}`) : undefined} 150 - /> 151 - } 152 223 ListFooterComponent={ 153 224 <ListFooter 154 225 isFetchingNextPage={isFetchingNextPage} ··· 163 234 </> 164 235 ) 165 236 } 237 + 238 + const styles = StyleSheet.create({ 239 + tabBarContainer: { 240 + // @ts-ignore web only 241 + position: isWeb ? 'sticky' : '', 242 + top: 0, 243 + zIndex: 1, 244 + }, 245 + })
+38 -13
src/view/com/util/ViewHeader.tsx
··· 1 1 import React from 'react' 2 2 import {StyleSheet, TouchableOpacity, View} from 'react-native' 3 + import Animated from 'react-native-reanimated' 3 4 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5 + import {msg} from '@lingui/macro' 6 + import {useLingui} from '@lingui/react' 4 7 import {useNavigation} from '@react-navigation/native' 5 - import {CenteredView} from './Views' 6 - import {Text} from './text/Text' 8 + 9 + import {useSetDrawerOpen} from '#/state/shell' 10 + import {useAnalytics} from 'lib/analytics/analytics' 11 + import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' 7 12 import {usePalette} from 'lib/hooks/usePalette' 8 13 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 9 - import {useAnalytics} from 'lib/analytics/analytics' 10 14 import {NavigationProp} from 'lib/routes/types' 11 - import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' 12 - import Animated from 'react-native-reanimated' 13 - import {useSetDrawerOpen} from '#/state/shell' 14 - import {msg} from '@lingui/macro' 15 - import {useLingui} from '@lingui/react' 16 15 import {useTheme} from '#/alf' 16 + import {Text} from './text/Text' 17 + import {CenteredView} from './Views' 17 18 18 19 const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20} 19 20 ··· 62 63 return ( 63 64 <DesktopWebHeader 64 65 title={title} 66 + subtitle={subtitle} 65 67 renderButton={renderButton} 66 68 showBorder={showBorder} 67 69 /> ··· 136 138 137 139 function DesktopWebHeader({ 138 140 title, 141 + subtitle, 139 142 renderButton, 140 143 showBorder = true, 141 144 }: { 142 145 title: string 146 + subtitle?: string 143 147 renderButton?: () => JSX.Element 144 148 showBorder?: boolean 145 149 }) { 146 150 const pal = usePalette('default') 151 + const t = useTheme() 147 152 return ( 148 153 <CenteredView 149 154 style={[ ··· 153 158 { 154 159 borderBottomWidth: showBorder ? 1 : 0, 155 160 }, 161 + {display: 'flex', flexDirection: 'column'}, 156 162 ]}> 157 - <View style={styles.titleContainer} pointerEvents="none"> 158 - <Text type="title-lg" style={[pal.text, styles.title]}> 159 - {title} 160 - </Text> 163 + <View> 164 + <View style={styles.titleContainer} pointerEvents="none"> 165 + <Text type="title-lg" style={[pal.text, styles.title]}> 166 + {title} 167 + </Text> 168 + </View> 169 + {renderButton?.()} 161 170 </View> 162 - {renderButton?.()} 171 + {subtitle ? ( 172 + <View> 173 + <View style={[styles.titleContainer]} pointerEvents="none"> 174 + <Text 175 + style={[ 176 + pal.text, 177 + styles.subtitleDesktop, 178 + t.atoms.text_contrast_medium, 179 + ]}> 180 + {subtitle} 181 + </Text> 182 + </View> 183 + </View> 184 + ) : null} 163 185 </CenteredView> 164 186 ) 165 187 } ··· 235 257 }, 236 258 subtitle: { 237 259 fontSize: 13, 260 + }, 261 + subtitleDesktop: { 262 + fontSize: 15, 238 263 }, 239 264 backBtn: { 240 265 width: 30,