Merge branch 'main' into web-layout

This commit is contained in:
Dan Abramov
2023-12-19 02:54:41 +00:00
55 changed files with 1596 additions and 1159 deletions
+44 -3
View File
@@ -3,8 +3,9 @@ import {View, Pressable} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
import {useNavigation} from '@react-navigation/native'
import {isIOS} from 'platform/detection'
import {isIOS, isNative} from 'platform/detection'
import {Login} from 'view/com/auth/login/Login'
import {CreateAccount} from 'view/com/auth/create/CreateAccount'
import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
@@ -18,6 +19,9 @@ import {
useLoggedOutView,
useLoggedOutViewControls,
} from '#/state/shell/logged-out'
import {useSession} from '#/state/session'
import {Text} from '#/view/com/util/text/Text'
import {NavigationProp} from 'lib/routes/types'
enum ScreenState {
S_LoginOrCreateAccount,
@@ -26,6 +30,7 @@ enum ScreenState {
}
export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
const {hasSession} = useSession()
const {_} = useLingui()
const pal = usePalette('default')
const setMinimalShellMode = useSetMinimalShellMode()
@@ -40,6 +45,8 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
)
const {isMobile} = useWebMediaQueries()
const {clearRequestedAccount} = useLoggedOutViewControls()
const navigation = useNavigation<NavigationProp>()
const isFirstScreen = screenState === ScreenState.S_LoginOrCreateAccount
React.useEffect(() => {
screen('Login')
@@ -53,6 +60,10 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
clearRequestedAccount()
}, [clearRequestedAccount, onDismiss])
const onPressSearch = React.useCallback(() => {
navigation.navigate(`SearchTab`)
}, [navigation])
return (
<View
testID="noSessionView"
@@ -65,7 +76,7 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
},
]}>
<ErrorBoundary>
{onDismiss && (
{onDismiss ? (
<Pressable
accessibilityHint={_(msg`Go back`)}
accessibilityLabel={_(msg`Go back`)}
@@ -88,7 +99,37 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
}}
/>
</Pressable>
)}
) : isNative && !hasSession && isFirstScreen ? (
<Pressable
accessibilityHint={_(msg`Search for users`)}
accessibilityLabel={_(msg`Search for users`)}
accessibilityRole="button"
style={{
flexDirection: 'row',
alignItems: 'center',
gap: 4,
position: 'absolute',
top: 20,
right: 20,
paddingHorizontal: 16,
paddingVertical: 8,
zIndex: 100,
backgroundColor: pal.btn.backgroundColor,
borderRadius: 100,
}}
onPress={onPressSearch}>
<Text type="lg-bold" style={[pal.text]}>
Search{' '}
</Text>
<FontAwesomeIcon
icon="search"
size={16}
style={{
color: String(pal.text.color),
}}
/>
</Pressable>
) : null}
{screenState === ScreenState.S_LoginOrCreateAccount ? (
<SplashScreen
+2 -2
View File
@@ -27,11 +27,11 @@ export const SplashScreen = ({
<Logo width={92} fill="sky" />
<View style={{paddingTop: 40, paddingBottom: 6}}>
<Logotype width={161} />
<Logotype width={161} fill={pal.text.color} />
</View>
<Text type="lg-medium" style={[pal.textLight]}>
<Trans>What's next?</Trans>
<Trans>What's up?</Trans>
</Text>
</View>
<View testID="signinOrCreateAccount" style={styles.btns}>
+1 -1
View File
@@ -63,7 +63,7 @@ export const SplashScreen = ({
<Logo width={92} fill="sky" />
<View style={{paddingTop: 40, paddingBottom: 20}}>
<Logotype width={161} />
<Logotype width={161} fill={pal.text.color} />
</View>
<View testID="signinOrCreateAccount" style={styles.btns}>
+1 -5
View File
@@ -30,7 +30,7 @@ import {Step2} from './Step2'
import {Step3} from './Step3'
export function CreateAccount({onPressBack}: {onPressBack: () => void}) {
const {track, screen} = useAnalytics()
const {screen} = useAnalytics()
const pal = usePalette('default')
const {_} = useLingui()
const [uiState, uiDispatch] = useCreateAccount()
@@ -93,21 +93,17 @@ export function CreateAccount({onPressBack}: {onPressBack: () => void}) {
uiDispatch,
_,
})
track('Create Account')
setBirthDate({birthDate: uiState.birthDate})
if (IS_PROD(uiState.serviceUrl)) {
setSavedFeeds(DEFAULT_PROD_FEEDS)
}
} catch {
// dont need to handle here
} finally {
track('Try Create Account')
}
}
}, [
uiState,
uiDispatch,
track,
onboardingDispatch,
createAccount,
setBirthDate,
+6
View File
@@ -62,6 +62,7 @@ import {useProfileQuery} from '#/state/queries/profile'
import {useComposerControls} from '#/state/shell/composer'
import {emitPostCreated} from '#/state/events'
import {ThreadgateSetting} from '#/state/queries/threadgate'
import {logger} from '#/logger'
type Props = ComposerOpts
export const ComposePost = observer(function ComposePost({
@@ -228,6 +229,11 @@ export const ComposePost = observer(function ComposePost({
})
).uri
} catch (e: any) {
logger.error(e, {
message: `Composer: create post failed`,
hasImages: gallery.size > 0,
})
if (extLink) {
setExtLink({
...extLink,
@@ -215,7 +215,13 @@ export const TextInput = forwardRef(function TextInputImpl(
autoFocus={true}
allowFontScaling
multiline
style={[pal.text, styles.textInput, styles.textInputFormatting]}
numberOfLines={4}
style={[
pal.text,
styles.textInput,
styles.textInputFormatting,
{textAlignVertical: 'top'},
]}
{...props}>
{textDecorated}
</PasteInput>
+1 -1
View File
@@ -29,7 +29,7 @@ import {truncateAndInvalidate} from '#/state/queries/util'
import {TabState, getTabState, getRootNavigation} from '#/lib/routes/helpers'
import {isNative} from '#/platform/detection'
const POLL_FREQ = 30e3 // 30sec
const POLL_FREQ = 60e3 // 60sec
export function FeedPage({
testID,
+3 -3
View File
@@ -62,17 +62,17 @@ export function Component(props: ReportComponentProps) {
<Text
type="2xl-bold"
style={[pal.text, s.textCenter, {paddingBottom: 8}]}>
<Trans>Appeal Decision</Trans>
<Trans>Appeal Content Warning</Trans>
</Text>
<ScrollView>
<View style={[pal.btn, styles.detailsInputContainer]}>
<TextInput
accessibilityLabel={_(msg`Text input field`)}
accessibilityHint={_(
msg`Please tell us why you think this decision was incorrect.`,
msg`Please tell us why you think this content warning was incorrectly applied!`,
)}
placeholder={_(
msg`Please tell us why you think this decision was incorrect.`,
msg`Please tell us why you think this content warning was incorrectly applied!`,
)}
placeholderTextColor={pal.textLight.color}
value={details}
+1 -1
View File
@@ -82,7 +82,7 @@ export function ModalsContainer() {
useEffect(() => {
if (isModalActive) {
bottomSheetRef.current?.expand()
bottomSheetRef.current?.snapToIndex(0)
} else {
bottomSheetRef.current?.close()
}
+6 -8
View File
@@ -42,7 +42,6 @@ import {useComposerControls} from '#/state/shell/composer'
import {useModerationOpts} from '#/state/queries/preferences'
import {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow'
import {ThreadPost} from '#/state/queries/post-thread'
import {LabelInfo} from '../util/moderation/LabelInfo'
import {useSession} from '#/state/session'
import {WhoCanReply} from '../threadgate/WhoCanReply'
@@ -187,6 +186,9 @@ let PostThreadItemLoaded = ({
return makeProfileLink(post.author, 'post', urip.rkey, 'reposted-by')
}, [post.uri, post.author])
const repostsTitle = 'Reposts of this post'
const isSelfLabeledPost =
moderation.decisions.post.cause?.type === 'label' &&
moderation.decisions.post.cause.label.src === currentAccount?.did
const translatorUrl = getTranslatorLink(
record?.text || '',
@@ -332,6 +334,9 @@ let PostThreadItemLoaded = ({
postCid={post.cid}
postUri={post.uri}
record={record}
showAppealLabelItem={
post.author.did === currentAccount?.did && !isSelfLabeledPost
}
style={{
paddingVertical: 6,
paddingHorizontal: 10,
@@ -351,13 +356,6 @@ let PostThreadItemLoaded = ({
includeMute
style={styles.alert}
/>
{post.author.did === currentAccount?.did ? (
<LabelInfo
details={{uri: post.uri, cid: post.cid}}
labels={post.labels}
style={{marginBottom: 8}}
/>
) : null}
{richText?.text ? (
<View
style={[
+22 -4
View File
@@ -27,12 +27,16 @@ import {
import {isWeb} from '#/platform/detection'
import {listenPostCreated} from '#/state/events'
import {useSession} from '#/state/session'
import {STALE} from '#/state/queries'
const LOADING_ITEM = {_reactKey: '__loading__'}
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
const ERROR_ITEM = {_reactKey: '__error__'}
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
const REFRESH_AFTER = STALE.HOURS.ONE
const CHECK_LATEST_AFTER = STALE.SECONDS.THIRTY
let Feed = ({
feed,
feedParams,
@@ -74,6 +78,7 @@ let Feed = ({
const {currentAccount} = useSession()
const [isPTRing, setIsPTRing] = React.useState(false)
const checkForNewRef = React.useRef<(() => void) | null>(null)
const lastFetchRef = React.useRef<number>(Date.now())
const opts = React.useMemo(
() => ({enabled, ignoreFilterFor}),
@@ -91,6 +96,9 @@ let Feed = ({
fetchNextPage,
} = usePostFeedQuery(feed, feedParams, opts)
const isEmpty = !isFetching && !data?.pages[0]?.slices.length
if (data?.pages[0]) {
lastFetchRef.current = data?.pages[0].fetchedAt
}
const checkForNew = React.useCallback(async () => {
if (!data?.pages[0] || isFetching || !onHasNew || !enabled) {
@@ -130,11 +138,21 @@ let Feed = ({
checkForNewRef.current = checkForNew
}, [checkForNew])
React.useEffect(() => {
if (enabled && checkForNewRef.current) {
// check for new on enable (aka on focus)
checkForNewRef.current()
if (enabled) {
const timeSinceFirstLoad = Date.now() - lastFetchRef.current
if (timeSinceFirstLoad > REFRESH_AFTER) {
// do a full refresh
scrollElRef?.current?.scrollToOffset({offset: 0, animated: false})
queryClient.resetQueries({queryKey: RQKEY(feed)})
} else if (
timeSinceFirstLoad > CHECK_LATEST_AFTER &&
checkForNewRef.current
) {
// check for new on enable (aka on focus)
checkForNewRef.current()
}
}
}, [enabled])
}, [enabled, feed, queryClient, scrollElRef])
React.useEffect(() => {
let cleanup1: () => void | undefined, cleanup2: () => void | undefined
const subscription = AppState.addEventListener('change', nextAppState => {
@@ -31,6 +31,7 @@ let PostDropdownBtn = ({
postUri,
record,
style,
showAppealLabelItem,
}: {
testID: string
postAuthor: AppBskyActorDefs.ProfileViewBasic
@@ -38,6 +39,7 @@ let PostDropdownBtn = ({
postUri: string
record: AppBskyFeedPost.Record
style?: StyleProp<ViewStyle>
showAppealLabelItem?: boolean
}): React.ReactNode => {
const {hasSession, currentAccount} = useSession()
const theme = useTheme()
@@ -198,6 +200,23 @@ let PostDropdownBtn = ({
web: ['far', 'trash-can'],
},
},
showAppealLabelItem && {
label: 'separator',
},
showAppealLabelItem && {
label: _(msg`Appeal content warning`),
onPress() {
openModal({name: 'appeal-label', uri: postUri, cid: postCid})
},
testID: 'postDropdownAppealBtn',
icon: {
ios: {
name: 'exclamationmark.triangle',
},
android: 'ic_menu_report_image',
web: 'circle-exclamation',
},
},
].filter(Boolean) as NativeDropdownItem[]
return (
+2 -1
View File
@@ -43,7 +43,8 @@ export function LabelInfo({
]}>
<Text type="sm" style={pal.text}>
<Trans>
This {'did' in details ? 'account' : 'post'} has been labeled.
A content warning has been applied to this{' '}
{'did' in details ? 'account' : 'post'}.
</Trans>{' '}
</Text>
<Pressable
+3 -6
View File
@@ -10,7 +10,7 @@ import {AppBskyFeedDefs, AppBskyFeedPost} from '@atproto/api'
import {Text} from '../text/Text'
import {PostDropdownBtn} from '../forms/PostDropdownBtn'
import {HeartIcon, HeartIconSolid, CommentBottomArrow} from 'lib/icons'
import {s, colors} from 'lib/styles'
import {s} from 'lib/styles'
import {pluralize} from 'lib/strings/helpers'
import {useTheme} from 'lib/ThemeContext'
import {RepostButton} from './RepostButton'
@@ -180,7 +180,7 @@ let PostCtrls = ({
accessibilityHint=""
hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
{post.viewer?.like ? (
<HeartIconSolid style={styles.ctrlIconLiked} size={big ? 22 : 16} />
<HeartIconSolid style={s.likeColor} size={big ? 22 : 16} />
) : (
<HeartIcon
style={[defaultCtrlColor, big ? styles.mt1 : undefined]}
@@ -193,7 +193,7 @@ let PostCtrls = ({
testID="likeCount"
style={
post.viewer?.like
? [s.bold, s.red3, s.f15, s.ml5]
? [s.bold, s.likeColor, s.f15, s.ml5]
: [defaultCtrlColor, s.f15, s.ml5]
}>
{post.likeCount}
@@ -233,9 +233,6 @@ const styles = StyleSheet.create({
paddingLeft: 5,
paddingRight: 5,
},
ctrlIconLiked: {
color: colors.like,
},
mt1: {
marginTop: 1,
},
+3 -2
View File
@@ -1,7 +1,7 @@
import React from 'react'
import Svg, {Path, SvgProps, PathProps} from 'react-native-svg'
import {colors} from '#/lib/styles'
import {usePalette} from '#/lib/hooks/usePalette'
const ratio = 17 / 64
@@ -9,6 +9,7 @@ export function Logotype({
fill,
...rest
}: {fill?: PathProps['fill']} & SvgProps) {
const pal = usePalette('default')
// @ts-ignore it's fiiiiine
const size = parseInt(rest.width || 32)
@@ -20,7 +21,7 @@ export function Logotype({
width={size}
height={Number(size) * ratio}>
<Path
fill={fill || colors.black}
fill={fill || pal.text.color}
d="M8.478 6.252c1.503.538 2.3 1.78 2.3 3.172 0 2.356-1.576 3.785-4.6 3.785H0V0h5.974c2.875 0 4.267 1.466 4.267 3.413 0 1.3-.594 2.245-1.763 2.839Zm-2.69-4.193H2.504v3.45h3.284c1.28 0 1.967-.667 1.967-1.78 0-1.02-.705-1.67-1.967-1.67Zm-3.284 9.072h3.544c1.41 0 2.17-.65 2.17-1.818 0-1.224-.723-1.837-2.17-1.837H2.504v3.655ZM14.251 13.209h-2.337V0h2.337v13.209ZM22.001 8.998V3.636h2.338v9.573h-2.263v-1.392c-.724 1.076-1.726 1.614-3.006 1.614-2.022 0-3.34-1.224-3.34-3.45V3.636h2.338v5.955c0 1.206.594 1.818 1.8 1.818 1.132 0 2.133-.835 2.133-2.411ZM34.979 8.59v.556h-7.161c.167 1.651 1.076 2.467 2.486 2.467 1.076 0 1.8-.463 2.189-1.372h2.244c-.5 1.947-2.17 3.19-4.452 3.19-1.428 0-2.579-.463-3.45-1.372-.872-.91-1.318-2.115-1.318-3.637 0-1.502.427-2.708 1.299-3.636.872-.909 2.004-1.372 3.432-1.372 1.447 0 2.597.482 3.45 1.428.854.946 1.28 2.208 1.28 3.747Zm-4.75-3.358c-1.28 0-2.17.742-2.393 2.281h4.805c-.204-1.391-1.057-2.281-2.411-2.281ZM40.16 13.469c-2.783 0-4.249-1.095-4.379-3.303h2.282c.13 1.188.724 1.633 2.134 1.633 1.261 0 1.892-.39 1.892-1.15 0-.687-.445-1.02-1.874-1.262l-1.094-.185c-2.097-.353-3.136-1.318-3.136-2.894 0-1.8 1.429-2.894 3.97-2.894 2.728 0 4.138 1.075 4.23 3.246h-2.207c-.056-1.169-.742-1.577-2.023-1.577-1.113 0-1.67.371-1.67 1.113 0 .668.483.965 1.596 1.169l1.206.186c2.32.426 3.32 1.28 3.32 2.912 0 1.93-1.557 3.006-4.247 3.006ZM54.667 13.209h-2.671l-2.783-4.453-1.447 1.447v3.006h-2.3V0h2.3v7.606l3.896-3.97h2.783l-3.618 3.618 3.84 5.955ZM60.772 6.048l.78-2.412H64l-3.692 10.352c-.39 1.057-.872 1.818-1.484 2.245-.612.426-1.484.63-2.634.63-.39 0-.724-.018-1.02-.055V14.97h.89c1.057 0 1.577-.65 1.577-1.54 0-.445-.149-1.094-.446-1.929l-2.746-7.866h2.487l.779 2.393c.575 1.8 1.076 3.58 1.521 5.343.408-1.521.928-3.302 1.54-5.324Z"
/>
</Svg>
+9 -4
View File
@@ -12,6 +12,7 @@ import {FeedPage} from 'view/com/feeds/FeedPage'
import {HomeLoggedOutCTA} from '../com/auth/HomeLoggedOutCTA'
import {useSetMinimalShellMode, useSetDrawerSwipeDisabled} from '#/state/shell'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {usePinnedFeedsInfos, FeedSourceInfo} from '#/state/queries/feed'
import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
import {emitSoftReset} from '#/state/events'
import {useSession} from '#/state/session'
@@ -21,6 +22,7 @@ import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
export function HomeScreen(props: Props) {
const {data: preferences} = usePreferencesQuery()
const {feeds: pinnedFeeds} = usePinnedFeedsInfos()
const {isDesktop} = useWebMediaQueries()
const [initialPage, setInitialPage] = React.useState<string | undefined>(
undefined,
@@ -39,11 +41,12 @@ export function HomeScreen(props: Props) {
loadLastActivePage()
}, [])
if (preferences && initialPage !== undefined) {
if (preferences && pinnedFeeds && initialPage !== undefined) {
return (
<HomeScreenReady
{...props}
preferences={preferences}
pinnedFeeds={pinnedFeeds}
initialPage={isDesktop ? 'Following' : initialPage}
/>
)
@@ -58,9 +61,11 @@ export function HomeScreen(props: Props) {
function HomeScreenReady({
preferences,
pinnedFeeds,
initialPage,
}: Props & {
preferences: UsePreferencesQueryResponse
pinnedFeeds: FeedSourceInfo[]
initialPage: string
}) {
const {hasSession} = useSession()
@@ -82,9 +87,9 @@ function HomeScreenReady({
}, [preferences.feeds.pinned, selectedPage])
const customFeeds = React.useMemo(() => {
const pinned = preferences.feeds.pinned
const pinned = pinnedFeeds
const feeds: FeedDescriptor[] = []
for (const uri of pinned) {
for (const {uri} of pinned) {
if (uri.includes('app.bsky.feed.generator')) {
feeds.push(`feedgen|${uri}`)
} else if (uri.includes('app.bsky.graph.list')) {
@@ -92,7 +97,7 @@ function HomeScreenReady({
}
}
return feeds
}, [preferences.feeds.pinned])
}, [pinnedFeeds])
const homeFeedParams = React.useMemo<FeedParams>(() => {
return {
+3 -1
View File
@@ -24,11 +24,13 @@ import {useResolveUriQuery} from '#/state/queries/resolve-uri'
import {ErrorMessage} from '../com/util/error/ErrorMessage'
import {CenteredView} from '../com/util/Views'
import {useComposerControls} from '#/state/shell/composer'
import {useSession} from '#/state/session'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostThread'>
export function PostThreadScreen({route}: Props) {
const queryClient = useQueryClient()
const {_} = useLingui()
const {hasSession} = useSession()
const {fabMinimalShellTransform} = useMinimalShellMode()
const setMinimalShellMode = useSetMinimalShellMode()
const {openComposer} = useComposerControls()
@@ -89,7 +91,7 @@ export function PostThreadScreen({route}: Props) {
/>
)}
</View>
{isMobile && canReply && (
{isMobile && canReply && hasSession && (
<Animated.View
style={[
styles.prompt,
+19 -72
View File
@@ -1,14 +1,14 @@
import React, {useMemo, useCallback} from 'react'
import {Dimensions, StyleSheet, View, ActivityIndicator} from 'react-native'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {useNavigation} from '@react-navigation/native'
import {useIsFocused, useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
import {usePalette} from 'lib/hooks/usePalette'
import {HeartIcon, HeartIconSolid} from 'lib/icons'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {CommonNavigatorParams} from 'lib/routes/types'
import {makeRecordUri} from 'lib/strings/url-helpers'
import {colors, s} from 'lib/styles'
import {s} from 'lib/styles'
import {FeedDescriptor} from '#/state/queries/post-feed'
import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader'
@@ -42,11 +42,7 @@ import {logger} from '#/logger'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {
useFeedSourceInfoQuery,
FeedSourceFeedInfo,
useIsFeedPublicQuery,
} from '#/state/queries/feed'
import {useFeedSourceInfoQuery, FeedSourceFeedInfo} from '#/state/queries/feed'
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
import {
UsePreferencesQueryResponse,
@@ -132,10 +128,8 @@ export function ProfileFeedScreen(props: Props) {
function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) {
const {data: preferences} = usePreferencesQuery()
const {data: info} = useFeedSourceInfoQuery({uri: feedUri})
const {isLoading: isPublicStatusLoading, data: isPublicResponse} =
useIsFeedPublicQuery({uri: feedUri})
if (!preferences || !info || isPublicStatusLoading) {
if (!preferences || !info) {
return (
<CenteredView>
<View style={s.p20}>
@@ -149,7 +143,6 @@ function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) {
<ProfileFeedScreenInner
preferences={preferences}
feedInfo={info as FeedSourceFeedInfo}
isPublicResponse={isPublicResponse}
/>
)
}
@@ -157,11 +150,9 @@ function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) {
export function ProfileFeedScreenInner({
preferences,
feedInfo,
isPublicResponse,
}: {
preferences: UsePreferencesQueryResponse
feedInfo: FeedSourceFeedInfo
isPublicResponse: ReturnType<typeof useIsFeedPublicQuery>['data']
}) {
const {_} = useLingui()
const pal = usePalette('default')
@@ -170,6 +161,7 @@ export function ProfileFeedScreenInner({
const {openComposer} = useComposerControls()
const {track} = useAnalytics()
const feedSectionRef = React.useRef<SectionRef>(null)
const isScreenFocused = useIsFocused()
const {
mutateAsync: saveFeed,
@@ -205,6 +197,9 @@ export function ProfileFeedScreenInner({
useSetTitle(feedInfo?.displayName)
// event handlers
//
const onToggleSaved = React.useCallback(async () => {
try {
Haptics.default()
@@ -398,21 +393,15 @@ export function ProfileFeedScreenInner({
isHeaderReady={true}
renderHeader={renderHeader}
onCurrentPageSelected={onCurrentPageSelected}>
{({headerHeight, scrollElRef, isFocused}) =>
isPublicResponse?.isPublic ? (
<FeedSection
ref={feedSectionRef}
feed={`feedgen|${feedInfo.uri}`}
headerHeight={headerHeight}
scrollElRef={scrollElRef as ListRef}
isFocused={isFocused}
/>
) : (
<CenteredView sideBorders style={[{paddingTop: headerHeight}]}>
<NonPublicFeedMessage rawError={isPublicResponse?.error} />
</CenteredView>
)
}
{({headerHeight, scrollElRef, isFocused}) => (
<FeedSection
ref={feedSectionRef}
feed={`feedgen|${feedInfo.uri}`}
headerHeight={headerHeight}
scrollElRef={scrollElRef as ListRef}
isFocused={isScreenFocused && isFocused}
/>
)}
{({headerHeight, scrollElRef}) => (
<AboutSection
feedOwnerDid={feedInfo.creatorDid}
@@ -446,45 +435,6 @@ export function ProfileFeedScreenInner({
)
}
function NonPublicFeedMessage({rawError}: {rawError?: Error}) {
const pal = usePalette('default')
return (
<View
style={[
pal.border,
{
padding: 18,
borderTopWidth: 1,
minHeight: Dimensions.get('window').height * 1.5,
},
]}>
<View
style={[
pal.viewLight,
{
padding: 12,
borderRadius: 8,
gap: 12,
},
]}>
<Text style={[pal.text]}>
<Trans>
Looks like this feed is only available to users with a Bluesky
account. Please sign up or sign in to view this feed!
</Trans>
</Text>
{rawError?.message && (
<Text style={pal.textLight}>
<Trans>Message from server</Trans>: {rawError.message}
</Text>
)}
</View>
</View>
)
}
interface FeedSectionProps {
feed: FeedDescriptor
headerHeight: number
@@ -519,7 +469,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
<Feed
enabled={isFocused}
feed={feed}
pollInterval={30e3}
pollInterval={60e3}
scrollElRef={scrollElRef}
onHasNew={setHasNew}
onScrolledDownChange={setIsScrolledDown}
@@ -630,7 +580,7 @@ function AboutSection({
onPress={onToggleLiked}
style={{paddingHorizontal: 10}}>
{isLiked ? (
<HeartIconSolid size={19} style={styles.liked} />
<HeartIconSolid size={19} style={s.likeColor} />
) : (
<HeartIcon strokeWidth={3} size={19} style={pal.textLight} />
)}
@@ -673,9 +623,6 @@ const styles = StyleSheet.create({
borderRadius: 50,
marginLeft: 6,
},
liked: {
color: colors.red3,
},
notFoundContainer: {
margin: 10,
paddingHorizontal: 18,
+4 -3
View File
@@ -1,6 +1,6 @@
import React, {useCallback, useMemo} from 'react'
import {ActivityIndicator, Pressable, StyleSheet, View} from 'react-native'
import {useFocusEffect} from '@react-navigation/native'
import {useFocusEffect, useIsFocused} from '@react-navigation/native'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {useNavigation} from '@react-navigation/native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
@@ -115,6 +115,7 @@ function ProfileListScreenLoaded({
const aboutSectionRef = React.useRef<SectionRef>(null)
const {openModal} = useModalControls()
const isCurateList = list.purpose === 'app.bsky.graph.defs#curatelist'
const isScreenFocused = useIsFocused()
useSetTitle(list.name)
@@ -165,7 +166,7 @@ function ProfileListScreenLoaded({
feed={`list|${uri}`}
scrollElRef={scrollElRef as ListRef}
headerHeight={headerHeight}
isFocused={isFocused}
isFocused={isScreenFocused && isFocused}
/>
)}
{({headerHeight, scrollElRef}) => (
@@ -623,7 +624,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
testID="listFeed"
enabled={isFocused}
feed={feed}
pollInterval={30e3}
pollInterval={60e3}
scrollElRef={scrollElRef}
onHasNew={setHasNew}
onScrolledDownChange={setIsScrolledDown}
+11 -11
View File
@@ -221,18 +221,16 @@ let DrawerContent = ({}: {}): React.ReactNode => {
<NavSignupCard />
)}
{hasSession && <InviteCodes />}
{hasSession && <View style={{height: 10}} />}
<SearchMenuItem isActive={isAtSearch} onPress={onPressSearch} />
<HomeMenuItem isActive={isAtHome} onPress={onPressHome} />
{hasSession && (
<NotificationsMenuItem
isActive={isAtNotifications}
onPress={onPressNotifications}
/>
)}
{hasSession && (
{hasSession ? (
<>
<InviteCodes />
<View style={{height: 10}} />
<SearchMenuItem isActive={isAtSearch} onPress={onPressSearch} />
<HomeMenuItem isActive={isAtHome} onPress={onPressHome} />
<NotificationsMenuItem
isActive={isAtNotifications}
onPress={onPressNotifications}
/>
<FeedsMenuItem isActive={isAtFeeds} onPress={onPressMyFeeds} />
<ListsMenuItem onPress={onPressLists} />
<ModerationMenuItem onPress={onPressModeration} />
@@ -242,6 +240,8 @@ let DrawerContent = ({}: {}): React.ReactNode => {
/>
<SettingsMenuItem onPress={onPressSettings} />
</>
) : (
<SearchMenuItem isActive={isAtSearch} onPress={onPressSearch} />
)}
<View style={styles.smallSpacer} />
+4 -4
View File
@@ -5,11 +5,11 @@ import {useLingui} from '@lingui/react'
import {s} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {DefaultAvatar} from '#/view/com/util/UserAvatar'
import {Text} from '#/view/com/util/text/Text'
import {Button} from '#/view/com/util/forms/Button'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useCloseAllActiveElements} from '#/state/util'
import {Logo} from '#/view/icons/Logo'
let NavSignupCard = ({}: {}): React.ReactNode => {
const {_} = useLingui()
@@ -35,10 +35,10 @@ let NavSignupCard = ({}: {}): React.ReactNode => {
paddingTop: 6,
marginBottom: 24,
}}>
<DefaultAvatar type="user" size={48} />
<Logo width={48} />
<View style={{paddingTop: 12}}>
<Text type="md" style={[pal.text, s.bold]}>
<View style={{paddingTop: 18}}>
<Text type="md-bold" style={[pal.text]}>
<Trans>Sign up or sign in to join the conversation</Trans>
</Text>
</View>
+108 -47
View File
@@ -23,13 +23,19 @@ import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
import {useNavigationTabState} from 'lib/hooks/useNavigationTabState'
import {UserAvatar} from 'view/com/util/UserAvatar'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
import {msg, Trans} from '@lingui/macro'
import {useModalControls} from '#/state/modals'
import {useShellLayout} from '#/state/shell/shell-layout'
import {useUnreadNotifications} from '#/state/queries/notifications/unread'
import {emitSoftReset} from '#/state/events'
import {useSession} from '#/state/session'
import {useProfileQuery} from '#/state/queries/profile'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useCloseAllActiveElements} from '#/state/util'
import {Button} from '#/view/com/util/forms/Button'
import {s} from 'lib/styles'
import {Logo} from '#/view/icons/Logo'
import {Logotype} from '#/view/icons/Logotype'
type TabOptions = 'Home' | 'Search' | 'Notifications' | 'MyProfile' | 'Feeds'
@@ -46,6 +52,19 @@ export function BottomBar({navigation}: BottomTabBarProps) {
const numUnreadNotifications = useUnreadNotifications()
const {footerMinimalShellTransform} = useMinimalShellMode()
const {data: profile} = useProfileQuery({did: currentAccount?.did})
const {requestSwitchToAccount} = useLoggedOutViewControls()
const closeAllActiveElements = useCloseAllActiveElements()
const showSignIn = React.useCallback(() => {
closeAllActiveElements()
requestSwitchToAccount({requestedAccount: 'none'})
}, [requestSwitchToAccount, closeAllActiveElements])
const showCreateAccount = React.useCallback(() => {
closeAllActiveElements()
requestSwitchToAccount({requestedAccount: 'new'})
// setShowLoggedOut(true)
}, [requestSwitchToAccount, closeAllActiveElements])
const onPressTab = React.useCallback(
(tab: TabOptions) => {
@@ -94,53 +113,52 @@ export function BottomBar({navigation}: BottomTabBarProps) {
onLayout={e => {
footerHeight.value = e.nativeEvent.layout.height
}}>
<Btn
testID="bottomBarHomeBtn"
icon={
isAtHome ? (
<HomeIconSolid
strokeWidth={4}
size={24}
style={[styles.ctrlIcon, pal.text, styles.homeIcon]}
/>
) : (
<HomeIcon
strokeWidth={4}
size={24}
style={[styles.ctrlIcon, pal.text, styles.homeIcon]}
/>
)
}
onPress={onPressHome}
accessibilityRole="tab"
accessibilityLabel={_(msg`Home`)}
accessibilityHint=""
/>
<Btn
testID="bottomBarSearchBtn"
icon={
isAtSearch ? (
<MagnifyingGlassIcon2Solid
size={25}
style={[styles.ctrlIcon, pal.text, styles.searchIcon]}
strokeWidth={1.8}
/>
) : (
<MagnifyingGlassIcon2
size={25}
style={[styles.ctrlIcon, pal.text, styles.searchIcon]}
strokeWidth={1.8}
/>
)
}
onPress={onPressSearch}
accessibilityRole="search"
accessibilityLabel={_(msg`Search`)}
accessibilityHint=""
/>
{hasSession && (
{hasSession ? (
<>
<Btn
testID="bottomBarHomeBtn"
icon={
isAtHome ? (
<HomeIconSolid
strokeWidth={4}
size={24}
style={[styles.ctrlIcon, pal.text, styles.homeIcon]}
/>
) : (
<HomeIcon
strokeWidth={4}
size={24}
style={[styles.ctrlIcon, pal.text, styles.homeIcon]}
/>
)
}
onPress={onPressHome}
accessibilityRole="tab"
accessibilityLabel={_(msg`Home`)}
accessibilityHint=""
/>
<Btn
testID="bottomBarSearchBtn"
icon={
isAtSearch ? (
<MagnifyingGlassIcon2Solid
size={25}
style={[styles.ctrlIcon, pal.text, styles.searchIcon]}
strokeWidth={1.8}
/>
) : (
<MagnifyingGlassIcon2
size={25}
style={[styles.ctrlIcon, pal.text, styles.searchIcon]}
strokeWidth={1.8}
/>
)
}
onPress={onPressSearch}
accessibilityRole="search"
accessibilityLabel={_(msg`Search`)}
accessibilityHint=""
/>
<Btn
testID="bottomBarFeedsBtn"
icon={
@@ -230,6 +248,49 @@ export function BottomBar({navigation}: BottomTabBarProps) {
accessibilityHint=""
/>
</>
) : (
<>
<View
style={{
width: '100%',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingTop: 14,
paddingBottom: 2,
paddingLeft: 14,
paddingRight: 6,
gap: 8,
}}>
<View style={{flexDirection: 'row', alignItems: 'center', gap: 8}}>
<Logo width={28} />
<View style={{paddingTop: 4}}>
<Logotype width={80} fill={pal.text.color} />
</View>
</View>
<View style={{flexDirection: 'row', alignItems: 'center', gap: 4}}>
<Button
onPress={showCreateAccount}
accessibilityHint={_(msg`Sign up`)}
accessibilityLabel={_(msg`Sign up`)}>
<Text type="md" style={[{color: 'white'}, s.bold]}>
<Trans>Sign up</Trans>
</Text>
</Button>
<Button
type="default"
onPress={showSignIn}
accessibilityHint={_(msg`Sign in`)}
accessibilityLabel={_(msg`Sign in`)}>
<Text type="md" style={[pal.text, s.bold]}>
<Trans>Sign in</Trans>
</Text>
</Button>
</View>
</View>
</>
)}
</Animated.View>
)
+128 -57
View File
@@ -3,6 +3,9 @@ import {usePalette} from 'lib/hooks/usePalette'
import {useNavigationState} from '@react-navigation/native'
import Animated from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {getCurrentRoute, isTab} from 'lib/routes/helpers'
import {styles} from './BottomBarStyles'
import {clamp} from 'lib/numbers'
@@ -22,12 +25,33 @@ import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
import {makeProfileLink} from 'lib/routes/links'
import {CommonNavigatorParams} from 'lib/routes/types'
import {useSession} from '#/state/session'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useCloseAllActiveElements} from '#/state/util'
import {Button} from '#/view/com/util/forms/Button'
import {Text} from '#/view/com/util/text/Text'
import {s} from 'lib/styles'
import {Logo} from '#/view/icons/Logo'
import {Logotype} from '#/view/icons/Logotype'
export function BottomBarWeb() {
const {_} = useLingui()
const {hasSession, currentAccount} = useSession()
const pal = usePalette('default')
const safeAreaInsets = useSafeAreaInsets()
const {footerMinimalShellTransform} = useMinimalShellMode()
const {requestSwitchToAccount} = useLoggedOutViewControls()
const closeAllActiveElements = useCloseAllActiveElements()
const showSignIn = React.useCallback(() => {
closeAllActiveElements()
requestSwitchToAccount({requestedAccount: 'none'})
}, [requestSwitchToAccount, closeAllActiveElements])
const showCreateAccount = React.useCallback(() => {
closeAllActiveElements()
requestSwitchToAccount({requestedAccount: 'new'})
// setShowLoggedOut(true)
}, [requestSwitchToAccount, closeAllActiveElements])
return (
<Animated.View
@@ -39,79 +63,126 @@ export function BottomBarWeb() {
{paddingBottom: clamp(safeAreaInsets.bottom, 15, 30)},
footerMinimalShellTransform,
]}>
<NavItem routeName="Home" href="/">
{({isActive}) => {
const Icon = isActive ? HomeIconSolid : HomeIcon
return (
<Icon
strokeWidth={4}
size={24}
style={[styles.ctrlIcon, pal.text, styles.homeIcon]}
/>
)
}}
</NavItem>
<NavItem routeName="Search" href="/search">
{({isActive}) => {
const Icon = isActive
? MagnifyingGlassIcon2Solid
: MagnifyingGlassIcon2
return (
<Icon
size={25}
style={[styles.ctrlIcon, pal.text, styles.searchIcon]}
strokeWidth={1.8}
/>
)
}}
</NavItem>
{hasSession && (
{hasSession ? (
<>
<NavItem routeName="Feeds" href="/feeds">
<NavItem routeName="Home" href="/">
{({isActive}) => {
return (
<HashtagIcon
size={22}
style={[styles.ctrlIcon, pal.text, styles.feedsIcon]}
strokeWidth={isActive ? 4 : 2.5}
/>
)
}}
</NavItem>
<NavItem routeName="Notifications" href="/notifications">
{({isActive}) => {
const Icon = isActive ? BellIconSolid : BellIcon
const Icon = isActive ? HomeIconSolid : HomeIcon
return (
<Icon
strokeWidth={4}
size={24}
strokeWidth={1.9}
style={[styles.ctrlIcon, pal.text, styles.bellIcon]}
style={[styles.ctrlIcon, pal.text, styles.homeIcon]}
/>
)
}}
</NavItem>
<NavItem
routeName="Profile"
href={
currentAccount
? makeProfileLink({
did: currentAccount.did,
handle: currentAccount.handle,
})
: '/'
}>
<NavItem routeName="Search" href="/search">
{({isActive}) => {
const Icon = isActive ? UserIconSolid : UserIcon
const Icon = isActive
? MagnifyingGlassIcon2Solid
: MagnifyingGlassIcon2
return (
<Icon
size={28}
strokeWidth={1.5}
style={[styles.ctrlIcon, pal.text, styles.profileIcon]}
size={25}
style={[styles.ctrlIcon, pal.text, styles.searchIcon]}
strokeWidth={1.8}
/>
)
}}
</NavItem>
{hasSession && (
<>
<NavItem routeName="Feeds" href="/feeds">
{({isActive}) => {
return (
<HashtagIcon
size={22}
style={[styles.ctrlIcon, pal.text, styles.feedsIcon]}
strokeWidth={isActive ? 4 : 2.5}
/>
)
}}
</NavItem>
<NavItem routeName="Notifications" href="/notifications">
{({isActive}) => {
const Icon = isActive ? BellIconSolid : BellIcon
return (
<Icon
size={24}
strokeWidth={1.9}
style={[styles.ctrlIcon, pal.text, styles.bellIcon]}
/>
)
}}
</NavItem>
<NavItem
routeName="Profile"
href={
currentAccount
? makeProfileLink({
did: currentAccount.did,
handle: currentAccount.handle,
})
: '/'
}>
{({isActive}) => {
const Icon = isActive ? UserIconSolid : UserIcon
return (
<Icon
size={28}
strokeWidth={1.5}
style={[styles.ctrlIcon, pal.text, styles.profileIcon]}
/>
)
}}
</NavItem>
</>
)}
</>
) : (
<>
<View
style={{
width: '100%',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingTop: 14,
paddingBottom: 2,
paddingLeft: 14,
paddingRight: 6,
gap: 8,
}}>
<View style={{flexDirection: 'row', alignItems: 'center', gap: 12}}>
<Logo width={32} />
<View style={{paddingTop: 4}}>
<Logotype width={80} fill={pal.text.color} />
</View>
</View>
<View style={{flexDirection: 'row', alignItems: 'center', gap: 8}}>
<Button
onPress={showCreateAccount}
accessibilityHint={_(msg`Sign up`)}
accessibilityLabel={_(msg`Sign up`)}>
<Text type="md" style={[{color: 'white'}, s.bold]}>
<Trans>Sign up</Trans>
</Text>
</Button>
<Button
type="default"
onPress={showSignIn}
accessibilityHint={_(msg`Sign in`)}
accessibilityLabel={_(msg`Sign in`)}>
<Text type="md" style={[pal.text, s.bold]}>
<Trans>Sign in</Trans>
</Text>
</Button>
</View>
</View>
</>
)}
</Animated.View>
+37 -34
View File
@@ -266,6 +266,10 @@ export function DesktopLeftNav() {
const {isDesktop, isTablet} = useWebMediaQueries()
const numUnread = useUnreadNotifications()
if (!hasSession && !isDesktop) {
return null
}
return (
<View
style={[
@@ -282,40 +286,40 @@ export function DesktopLeftNav() {
</View>
) : null}
<BackBtn />
<NavItem
href="/"
icon={<HomeIcon size={isDesktop ? 24 : 28} style={pal.text} />}
iconFilled={
<HomeIconSolid
strokeWidth={4}
size={isDesktop ? 24 : 28}
style={pal.text}
/>
}
label={_(msg`Home`)}
/>
<NavItem
href="/search"
icon={
<MagnifyingGlassIcon2
strokeWidth={2}
size={isDesktop ? 24 : 26}
style={pal.text}
/>
}
iconFilled={
<MagnifyingGlassIcon2Solid
strokeWidth={2}
size={isDesktop ? 24 : 26}
style={pal.text}
/>
}
label={_(msg`Search`)}
/>
{hasSession && (
<>
<BackBtn />
<NavItem
href="/"
icon={<HomeIcon size={isDesktop ? 24 : 28} style={pal.text} />}
iconFilled={
<HomeIconSolid
strokeWidth={4}
size={isDesktop ? 24 : 28}
style={pal.text}
/>
}
label={_(msg`Home`)}
/>
<NavItem
href="/search"
icon={
<MagnifyingGlassIcon2
strokeWidth={2}
size={isDesktop ? 24 : 26}
style={pal.text}
/>
}
iconFilled={
<MagnifyingGlassIcon2Solid
strokeWidth={2}
size={isDesktop ? 24 : 26}
style={pal.text}
/>
}
label={_(msg`Search`)}
/>
<NavItem
href="/feeds"
icon={
@@ -405,7 +409,7 @@ export function DesktopLeftNav() {
style={pal.text}
/>
}
label="Profile"
label={_(msg`Profile`)}
/>
<NavItem
href="/settings"
@@ -512,7 +516,6 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
width: 140,
borderRadius: 24,
paddingTop: 10,
paddingBottom: 12, // visually aligns the text vertically inside the button
+1 -1
View File
@@ -52,7 +52,7 @@ export function DesktopRightNav() {
</Text>
</View>
) : undefined}
<View style={[s.flexRow]}>
<View style={[{flexWrap: 'wrap'}, s.flexRow]}>
{hasSession && (
<>
<TextLink