Merge branch 'main' into hailey/search-improvements

This commit is contained in:
Dan Abramov
2024-04-26 16:25:34 +01:00
118 changed files with 10075 additions and 9585 deletions
-51
View File
@@ -1,51 +0,0 @@
import React from 'react'
import {SafeAreaView, Platform} from 'react-native'
import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
import {s} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {Welcome} from './onboarding/Welcome'
import {RecommendedFeeds} from './onboarding/RecommendedFeeds'
import {RecommendedFollows} from './onboarding/RecommendedFollows'
import {useSetMinimalShellMode} from '#/state/shell/minimal-mode'
import {useOnboardingState, useOnboardingDispatch} from '#/state/shell'
export function Onboarding() {
const pal = usePalette('default')
const setMinimalShellMode = useSetMinimalShellMode()
const onboardingState = useOnboardingState()
const onboardingDispatch = useOnboardingDispatch()
React.useEffect(() => {
setMinimalShellMode(true)
}, [setMinimalShellMode])
const next = () => onboardingDispatch({type: 'next'})
const skip = () => onboardingDispatch({type: 'skip'})
return (
<SafeAreaView
testID="onboardingView"
style={[
s.hContentRegion,
pal.view,
// @ts-ignore web only -esb
Platform.select({
web: {
height: '100vh',
},
}),
]}>
<ErrorBoundary>
{onboardingState.step === 'Welcome' && (
<Welcome skip={skip} next={next} />
)}
{onboardingState.step === 'RecommendedFeeds' && (
<RecommendedFeeds next={next} />
)}
{onboardingState.step === 'RecommendedFollows' && (
<RecommendedFollows next={next} />
)}
</ErrorBoundary>
</SafeAreaView>
)
}
@@ -1,211 +0,0 @@
import React from 'react'
import {ActivityIndicator, FlatList, StyleSheet, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useSuggestedFeedsQuery} from '#/state/queries/suggested-feeds'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
import {Button} from 'view/com/util/forms/Button'
import {Mobile, TabletOrDesktop} from 'view/com/util/layouts/Breakpoints'
import {TitleColumnLayout} from 'view/com/util/layouts/TitleColumnLayout'
import {Text} from 'view/com/util/text/Text'
import {ViewHeader} from 'view/com/util/ViewHeader'
import {RecommendedFeedsItem} from './RecommendedFeedsItem'
type Props = {
next: () => void
}
export function RecommendedFeeds({next}: Props) {
const pal = usePalette('default')
const {_} = useLingui()
const {isTabletOrMobile} = useWebMediaQueries()
const {isLoading, data} = useSuggestedFeedsQuery()
const hasFeeds = data && data.pages[0].feeds.length
const title = (
<>
<Trans>
<Text
style={[
pal.textLight,
tdStyles.title1,
isTabletOrMobile && tdStyles.title1Small,
]}>
Choose your
</Text>
<Text
style={[
pal.link,
tdStyles.title2,
isTabletOrMobile && tdStyles.title2Small,
]}>
Recommended
</Text>
<Text
style={[
pal.link,
tdStyles.title2,
isTabletOrMobile && tdStyles.title2Small,
]}>
Feeds
</Text>
</Trans>
<Text type="2xl-medium" style={[pal.textLight, tdStyles.description]}>
<Trans>
Feeds are created by users to curate content. Choose some feeds that
you find interesting.
</Trans>
</Text>
<View
style={{
flexDirection: 'row',
justifyContent: 'flex-end',
marginTop: 20,
}}>
<Button onPress={next} testID="continueBtn">
<View
style={{
flexDirection: 'row',
alignItems: 'center',
paddingLeft: 2,
gap: 6,
}}>
<Text
type="2xl-medium"
style={{color: '#fff', position: 'relative', top: -1}}>
<Trans>Next</Trans>
</Text>
<FontAwesomeIcon icon="angle-right" color="#fff" size={14} />
</View>
</Button>
</View>
</>
)
return (
<>
<TabletOrDesktop>
<TitleColumnLayout
testID="recommendedFeedsOnboarding"
title={title}
horizontal
titleStyle={isTabletOrMobile ? undefined : {minWidth: 470}}
contentStyle={{paddingHorizontal: 0}}>
{hasFeeds ? (
<FlatList
data={data.pages[0].feeds}
renderItem={({item}) => <RecommendedFeedsItem item={item} />}
keyExtractor={item => item.uri}
style={{flex: 1}}
/>
) : isLoading ? (
<View>
<ActivityIndicator size="large" />
</View>
) : (
<ErrorMessage message={_(msg`Failed to load recommended feeds`)} />
)}
</TitleColumnLayout>
</TabletOrDesktop>
<Mobile>
<View style={[mStyles.container]} testID="recommendedFeedsOnboarding">
<ViewHeader
title={_(msg`Recommended Feeds`)}
showBackButton={false}
showOnDesktop
/>
<Text type="lg-medium" style={[pal.text, mStyles.header]}>
<Trans>
Check out some recommended feeds. Tap + to add them to your list
of pinned feeds.
</Trans>
</Text>
{hasFeeds ? (
<FlatList
data={data.pages[0].feeds}
renderItem={({item}) => <RecommendedFeedsItem item={item} />}
keyExtractor={item => item.uri}
style={{flex: 1}}
showsVerticalScrollIndicator={false}
/>
) : isLoading ? (
<View style={{flex: 1}}>
<ActivityIndicator size="large" />
</View>
) : (
<View style={{flex: 1}}>
<ErrorMessage
message={_(msg`Failed to load recommended feeds`)}
/>
</View>
)}
<Button
onPress={next}
label={_(msg`Continue`)}
testID="continueBtn"
style={mStyles.button}
labelStyle={mStyles.buttonText}
/>
</View>
</Mobile>
</>
)
}
const tdStyles = StyleSheet.create({
container: {
flex: 1,
marginHorizontal: 16,
justifyContent: 'space-between',
},
title1: {
fontSize: 36,
fontWeight: '800',
textAlign: 'right',
},
title1Small: {
fontSize: 24,
},
title2: {
fontSize: 58,
fontWeight: '800',
textAlign: 'right',
},
title2Small: {
fontSize: 36,
},
description: {
maxWidth: 400,
marginTop: 10,
marginLeft: 'auto',
textAlign: 'right',
},
})
const mStyles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'space-between',
},
header: {
marginBottom: 16,
marginHorizontal: 16,
},
button: {
marginBottom: 16,
marginHorizontal: 16,
marginTop: 16,
alignItems: 'center',
},
buttonText: {
textAlign: 'center',
fontSize: 18,
paddingVertical: 4,
},
})
@@ -1,172 +0,0 @@
import React from 'react'
import {View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {AppBskyFeedDefs, RichText as BskRichText} from '@atproto/api'
import {Text} from 'view/com/util/text/Text'
import {RichText} from 'view/com/util/text/RichText'
import {Button} from 'view/com/util/forms/Button'
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 {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {sanitizeHandle} from 'lib/strings/handles'
import {
usePreferencesQuery,
usePinFeedMutation,
useRemoveFeedMutation,
} from '#/state/queries/preferences'
import {logger} from '#/logger'
import {useAnalytics} from '#/lib/analytics/analytics'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
export function RecommendedFeedsItem({
item,
}: {
item: AppBskyFeedDefs.GeneratorView
}) {
const {isMobile} = useWebMediaQueries()
const pal = usePalette('default')
const {_} = useLingui()
const {data: preferences} = usePreferencesQuery()
const {
mutateAsync: pinFeed,
variables: pinnedFeed,
reset: resetPinFeed,
} = usePinFeedMutation()
const {
mutateAsync: removeFeed,
variables: removedFeed,
reset: resetRemoveFeed,
} = useRemoveFeedMutation()
const {track} = useAnalytics()
if (!item || !preferences) return null
const isPinned =
!removedFeed?.uri &&
(pinnedFeed?.uri || preferences.feeds.saved.includes(item.uri))
const onToggle = async () => {
if (isPinned) {
try {
await removeFeed({uri: item.uri})
resetRemoveFeed()
} catch (e) {
Toast.show(_(msg`There was an issue contacting your server`))
logger.error('Failed to unsave feed', {message: e})
}
} else {
try {
await pinFeed({uri: item.uri})
resetPinFeed()
track('Onboarding:CustomFeedAdded')
} catch (e) {
Toast.show(_(msg`There was an issue contacting your server`))
logger.error('Failed to pin feed', {message: e})
}
}
}
return (
<View testID={`feed-${item.displayName}`}>
<View
style={[
pal.border,
{
flex: isMobile ? 1 : undefined,
flexDirection: 'row',
gap: 18,
maxWidth: isMobile ? undefined : 670,
borderRightWidth: isMobile ? undefined : 1,
paddingHorizontal: 24,
paddingVertical: isMobile ? 12 : 24,
borderTopWidth: 1,
},
]}>
<View style={{marginTop: 2}}>
<UserAvatar type="algo" size={42} avatar={item.avatar} />
</View>
<View style={{flex: isMobile ? 1 : undefined}}>
<Text
type="2xl-bold"
numberOfLines={1}
style={[pal.text, {fontSize: 19}]}>
{item.displayName}
</Text>
<Text style={[pal.textLight, {marginBottom: 8}]} numberOfLines={1}>
<Trans>by {sanitizeHandle(item.creator.handle, '@')}</Trans>
</Text>
{item.description ? (
<RichText
type="xl"
style={[
pal.text,
{
flex: isMobile ? 1 : undefined,
maxWidth: 550,
marginBottom: 18,
},
]}
richText={new BskRichText({text: item.description || ''})}
numberOfLines={6}
/>
) : null}
<View style={{flexDirection: 'row', alignItems: 'center', gap: 12}}>
<Button
type="inverted"
style={{paddingVertical: 6}}
onPress={onToggle}>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
paddingRight: 2,
gap: 6,
}}>
{isPinned ? (
<>
<FontAwesomeIcon
icon="check"
size={16}
color={pal.colors.textInverted}
/>
<Text type="lg-medium" style={pal.textInverted}>
<Trans>Added</Trans>
</Text>
</>
) : (
<>
<FontAwesomeIcon
icon="plus"
size={16}
color={pal.colors.textInverted}
/>
<Text type="lg-medium" style={pal.textInverted}>
<Trans>Add</Trans>
</Text>
</>
)}
</View>
</Button>
<View style={{flexDirection: 'row', gap: 4}}>
<HeartIcon
size={16}
strokeWidth={2.5}
style={[pal.textLight, {position: 'relative', top: 2}]}
/>
<Text type="lg-medium" style={[pal.text, pal.textLight]}>
{item.likeCount || 0}
</Text>
</View>
</View>
</View>
</View>
</View>
)
}
@@ -1,272 +0,0 @@
import React from 'react'
import {ActivityIndicator, FlatList, StyleSheet, View} from 'react-native'
import {AppBskyActorDefs, moderateProfile} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {logger} from '#/logger'
import {useModerationOpts} from '#/state/queries/preferences'
import {useSuggestedFollowsQuery} from '#/state/queries/suggested-follows'
import {useGetSuggestedFollowersByActor} from '#/state/queries/suggested-follows'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {Button} from 'view/com/util/forms/Button'
import {Mobile, TabletOrDesktop} from 'view/com/util/layouts/Breakpoints'
import {TitleColumnLayout} from 'view/com/util/layouts/TitleColumnLayout'
import {Text} from 'view/com/util/text/Text'
import {ViewHeader} from 'view/com/util/ViewHeader'
import {RecommendedFollowsItem} from './RecommendedFollowsItem'
type Props = {
next: () => void
}
export function RecommendedFollows({next}: Props) {
const pal = usePalette('default')
const {_} = useLingui()
const {isTabletOrMobile} = useWebMediaQueries()
const {data: suggestedFollows} = useSuggestedFollowsQuery()
const getSuggestedFollowsByActor = useGetSuggestedFollowersByActor()
const [additionalSuggestions, setAdditionalSuggestions] = React.useState<{
[did: string]: AppBskyActorDefs.ProfileView[]
}>({})
const existingDids = React.useRef<string[]>([])
const moderationOpts = useModerationOpts()
const title = (
<>
<Trans>
<Text
style={[
pal.textLight,
tdStyles.title1,
isTabletOrMobile && tdStyles.title1Small,
]}>
Follow some
</Text>
<Text
style={[
pal.link,
tdStyles.title2,
isTabletOrMobile && tdStyles.title2Small,
]}>
Recommended
</Text>
<Text
style={[
pal.link,
tdStyles.title2,
isTabletOrMobile && tdStyles.title2Small,
]}>
Users
</Text>
</Trans>
<Text type="2xl-medium" style={[pal.textLight, tdStyles.description]}>
<Trans>
Follow some users to get started. We can recommend you more users
based on who you find interesting.
</Trans>
</Text>
<View
style={{
flexDirection: 'row',
justifyContent: 'flex-end',
marginTop: 20,
}}>
<Button onPress={next} testID="continueBtn">
<View
style={{
flexDirection: 'row',
alignItems: 'center',
paddingLeft: 2,
gap: 6,
}}>
<Text
type="2xl-medium"
style={{color: '#fff', position: 'relative', top: -1}}>
<Trans context="action">Done</Trans>
</Text>
<FontAwesomeIcon icon="angle-right" color="#fff" size={14} />
</View>
</Button>
</View>
</>
)
const suggestions = React.useMemo(() => {
if (!suggestedFollows) return []
const additional = Object.entries(additionalSuggestions)
const items = suggestedFollows.pages.flatMap(page => page.actors)
outer: while (additional.length) {
const additionalAccount = additional.shift()
if (!additionalAccount) break
const [followedUser, relatedAccounts] = additionalAccount
for (let i = 0; i < items.length; i++) {
if (items[i].did === followedUser) {
items.splice(i + 1, 0, ...relatedAccounts)
continue outer
}
}
}
existingDids.current = items.map(i => i.did)
return items
}, [suggestedFollows, additionalSuggestions])
const onFollowStateChange = React.useCallback(
async ({following, did}: {following: boolean; did: string}) => {
if (following) {
try {
const {suggestions: results} = await getSuggestedFollowsByActor(did)
if (results.length) {
const deduped = results.filter(
r => !existingDids.current.find(did => did === r.did),
)
setAdditionalSuggestions(s => ({
...s,
[did]: deduped.slice(0, 3),
}))
}
} catch (e) {
logger.error('RecommendedFollows: failed to get suggestions', {
message: e,
})
}
}
// not handling the unfollow case
},
[existingDids, getSuggestedFollowsByActor, setAdditionalSuggestions],
)
return (
<>
<TabletOrDesktop>
<TitleColumnLayout
testID="recommendedFollowsOnboarding"
title={title}
horizontal
titleStyle={isTabletOrMobile ? undefined : {minWidth: 470}}
contentStyle={{paddingHorizontal: 0}}>
{!suggestedFollows || !moderationOpts ? (
<ActivityIndicator size="large" />
) : (
<FlatList
data={suggestions}
renderItem={({item}) => (
<RecommendedFollowsItem
profile={item}
onFollowStateChange={onFollowStateChange}
moderation={moderateProfile(item, moderationOpts)}
/>
)}
keyExtractor={item => item.did}
style={{flex: 1}}
/>
)}
</TitleColumnLayout>
</TabletOrDesktop>
<Mobile>
<View style={[mStyles.container]} testID="recommendedFollowsOnboarding">
<View>
<ViewHeader
title={_(msg`Recommended Users`)}
showBackButton={false}
showOnDesktop
/>
<Text type="lg-medium" style={[pal.text, mStyles.header]}>
<Trans>
Check out some recommended users. Follow them to see similar
users.
</Trans>
</Text>
</View>
{!suggestedFollows || !moderationOpts ? (
<ActivityIndicator size="large" />
) : (
<FlatList
data={suggestions}
renderItem={({item}) => (
<RecommendedFollowsItem
profile={item}
onFollowStateChange={onFollowStateChange}
moderation={moderateProfile(item, moderationOpts)}
/>
)}
keyExtractor={item => item.did}
style={{flex: 1}}
showsVerticalScrollIndicator={false}
/>
)}
<Button
onPress={next}
label={_(msg`Continue`)}
testID="continueBtn"
style={mStyles.button}
labelStyle={mStyles.buttonText}
/>
</View>
</Mobile>
</>
)
}
const tdStyles = StyleSheet.create({
container: {
flex: 1,
marginHorizontal: 16,
justifyContent: 'space-between',
},
title1: {
fontSize: 36,
fontWeight: '800',
textAlign: 'right',
},
title1Small: {
fontSize: 24,
},
title2: {
fontSize: 58,
fontWeight: '800',
textAlign: 'right',
},
title2Small: {
fontSize: 36,
},
description: {
maxWidth: 400,
marginTop: 10,
marginLeft: 'auto',
textAlign: 'right',
},
})
const mStyles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'space-between',
},
header: {
marginBottom: 16,
marginHorizontal: 16,
},
button: {
marginBottom: 16,
marginHorizontal: 16,
marginTop: 16,
alignItems: 'center',
},
buttonText: {
textAlign: 'center',
fontSize: 18,
paddingVertical: 4,
},
})
@@ -1,202 +0,0 @@
import React from 'react'
import {View, StyleSheet, ActivityIndicator} from 'react-native'
import {ModerationDecision, AppBskyActorDefs} from '@atproto/api'
import {Button} from '#/view/com/util/forms/Button'
import {usePalette} from 'lib/hooks/usePalette'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles'
import {s} from 'lib/styles'
import {UserAvatar} from 'view/com/util/UserAvatar'
import {Text} from 'view/com/util/text/Text'
import Animated, {FadeInRight} from 'react-native-reanimated'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {useAnalytics} from 'lib/analytics/analytics'
import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro'
import {Shadow, useProfileShadow} from '#/state/cache/profile-shadow'
import {useProfileFollowMutationQueue} from '#/state/queries/profile'
import {logger} from '#/logger'
type Props = {
profile: AppBskyActorDefs.ProfileViewBasic
moderation: ModerationDecision
onFollowStateChange: (props: {
did: string
following: boolean
}) => Promise<void>
}
export function RecommendedFollowsItem({
profile,
moderation,
onFollowStateChange,
}: React.PropsWithChildren<Props>) {
const pal = usePalette('default')
const {isMobile} = useWebMediaQueries()
const shadowedProfile = useProfileShadow(profile)
return (
<Animated.View
entering={FadeInRight}
style={[
styles.cardContainer,
pal.view,
pal.border,
{
maxWidth: isMobile ? undefined : 670,
borderRightWidth: isMobile ? undefined : 1,
},
]}>
<ProfileCard
key={profile.did}
profile={shadowedProfile}
onFollowStateChange={onFollowStateChange}
moderation={moderation}
/>
</Animated.View>
)
}
function ProfileCard({
profile,
onFollowStateChange,
moderation,
}: {
profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
moderation: ModerationDecision
onFollowStateChange: (props: {
did: string
following: boolean
}) => Promise<void>
}) {
const {track} = useAnalytics()
const pal = usePalette('default')
const {_} = useLingui()
const [addingMoreSuggestions, setAddingMoreSuggestions] =
React.useState(false)
const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
profile,
'RecommendedFollowsItem',
)
const onToggleFollow = React.useCallback(async () => {
try {
if (profile.viewer?.following) {
await queueUnfollow()
} else {
setAddingMoreSuggestions(true)
await queueFollow()
await onFollowStateChange({did: profile.did, following: true})
setAddingMoreSuggestions(false)
track('Onboarding:SuggestedFollowFollowed')
}
} catch (e: any) {
if (e?.name !== 'AbortError') {
logger.error('RecommendedFollows: failed to toggle following', {
message: e,
})
}
} finally {
setAddingMoreSuggestions(false)
}
}, [
profile,
queueFollow,
queueUnfollow,
setAddingMoreSuggestions,
track,
onFollowStateChange,
])
return (
<View style={styles.card}>
<View style={styles.layout}>
<View style={styles.layoutAvi}>
<UserAvatar
size={40}
avatar={profile.avatar}
moderation={moderation.ui('avatar')}
/>
</View>
<View style={styles.layoutContent}>
<Text
type="2xl-bold"
style={[s.bold, pal.text]}
numberOfLines={1}
lineHeight={1.2}>
{sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle),
moderation.ui('displayName'),
)}
</Text>
<Text type="xl" style={[pal.textLight]} numberOfLines={1}>
{sanitizeHandle(profile.handle, '@')}
</Text>
</View>
<Button
type={profile.viewer?.following ? 'default' : 'inverted'}
labelStyle={styles.followButton}
onPress={onToggleFollow}
label={profile.viewer?.following ? _(msg`Unfollow`) : _(msg`Follow`)}
/>
</View>
{profile.description ? (
<View style={styles.details}>
<Text type="lg" style={pal.text} numberOfLines={4}>
{profile.description as string}
</Text>
</View>
) : undefined}
{addingMoreSuggestions ? (
<View style={styles.addingMoreContainer}>
<ActivityIndicator size="small" color={pal.colors.text} />
<Text style={[pal.text]}>
<Trans>Finding similar accounts...</Trans>
</Text>
</View>
) : null}
</View>
)
}
const styles = StyleSheet.create({
cardContainer: {
borderTopWidth: 1,
},
card: {
paddingHorizontal: 10,
},
layout: {
flexDirection: 'row',
alignItems: 'center',
},
layoutAvi: {
width: 54,
paddingLeft: 4,
paddingTop: 8,
paddingBottom: 10,
},
layoutContent: {
flex: 1,
paddingRight: 10,
paddingTop: 10,
paddingBottom: 10,
},
details: {
paddingLeft: 54,
paddingRight: 10,
paddingBottom: 10,
},
addingMoreContainer: {
flexDirection: 'row',
alignItems: 'center',
paddingLeft: 54,
paddingTop: 4,
paddingBottom: 12,
gap: 4,
},
followButton: {
fontSize: 16,
},
})
-10
View File
@@ -1,10 +0,0 @@
import 'react'
import {withBreakpoints} from 'view/com/util/layouts/withBreakpoints'
import {WelcomeDesktop} from './WelcomeDesktop'
import {WelcomeMobile} from './WelcomeMobile'
export const Welcome = withBreakpoints(
WelcomeMobile,
WelcomeDesktop,
WelcomeDesktop,
)
@@ -1,126 +0,0 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import {useMediaQuery} from 'react-responsive'
import {Text} from 'view/com/util/text/Text'
import {s} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {TitleColumnLayout} from 'view/com/util/layouts/TitleColumnLayout'
import {Button} from 'view/com/util/forms/Button'
import {Trans} from '@lingui/macro'
type Props = {
next: () => void
skip: () => void
}
export function WelcomeDesktop({next}: Props) {
const pal = usePalette('default')
const horizontal = useMediaQuery({minWidth: 1300})
const title = (
<Trans>
<Text
style={[
pal.textLight,
{
fontSize: 36,
fontWeight: '800',
textAlign: horizontal ? 'right' : 'left',
},
]}>
Welcome to
</Text>
<Text
style={[
pal.link,
{
fontSize: 72,
fontWeight: '800',
textAlign: horizontal ? 'right' : 'left',
},
]}>
Bluesky
</Text>
</Trans>
)
return (
<TitleColumnLayout
testID="welcomeOnboarding"
title={title}
horizontal={horizontal}
titleStyle={horizontal ? {paddingBottom: 160} : undefined}>
<View style={[styles.row]}>
<FontAwesomeIcon icon={'globe'} size={36} color={pal.colors.link} />
<View style={[styles.rowText]}>
<Text type="xl-bold" style={[pal.text]}>
<Trans>Bluesky is public.</Trans>
</Text>
<Text type="xl" style={[pal.text, s.pt2]}>
<Trans>
Your posts, likes, and blocks are public. Mutes are private.
</Trans>
</Text>
</View>
</View>
<View style={[styles.row]}>
<FontAwesomeIcon icon={'at'} size={36} color={pal.colors.link} />
<View style={[styles.rowText]}>
<Text type="xl-bold" style={[pal.text]}>
<Trans>Bluesky is open.</Trans>
</Text>
<Text type="xl" style={[pal.text, s.pt2]}>
<Trans>Never lose access to your followers and data.</Trans>
</Text>
</View>
</View>
<View style={[styles.row]}>
<FontAwesomeIcon icon={'gear'} size={36} color={pal.colors.link} />
<View style={[styles.rowText]}>
<Text type="xl-bold" style={[pal.text]}>
<Trans>Bluesky is flexible.</Trans>
</Text>
<Text type="xl" style={[pal.text, s.pt2]}>
<Trans>
Choose the algorithms that power your experience with custom
feeds.
</Trans>
</Text>
</View>
</View>
<View style={styles.spacer} />
<View style={{flexDirection: 'row'}}>
<Button onPress={next} testID="continueBtn">
<View
style={{
flexDirection: 'row',
alignItems: 'center',
paddingLeft: 2,
gap: 6,
}}>
<Text
type="2xl-medium"
style={{color: '#fff', position: 'relative', top: -1}}>
<Trans context="action">Next</Trans>
</Text>
<FontAwesomeIcon icon="angle-right" color="#fff" size={14} />
</View>
</Button>
</View>
</TitleColumnLayout>
)
}
const styles = StyleSheet.create({
row: {
flexDirection: 'row',
columnGap: 20,
alignItems: 'center',
marginVertical: 20,
},
rowText: {
flex: 1,
},
spacer: {
height: 20,
},
})
@@ -1,136 +0,0 @@
import React from 'react'
import {Pressable, StyleSheet, View} from 'react-native'
import {Text} from 'view/com/util/text/Text'
import {s} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Button} from 'view/com/util/forms/Button'
import {ViewHeader} from 'view/com/util/ViewHeader'
import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro'
type Props = {
next: () => void
skip: () => void
}
export function WelcomeMobile({next, skip}: Props) {
const pal = usePalette('default')
const {_} = useLingui()
return (
<View style={[styles.container]} testID="welcomeOnboarding">
<ViewHeader
showOnDesktop
showBorder={false}
showBackButton={false}
title=""
renderButton={() => {
return (
<Pressable
accessibilityRole="button"
style={[s.flexRow, s.alignCenter]}
onPress={skip}>
<Text style={[pal.link]}>
<Trans>Skip</Trans>
</Text>
<FontAwesomeIcon
icon={'chevron-right'}
size={14}
color={pal.colors.link}
/>
</Pressable>
)
}}
/>
<View>
<Text style={[pal.text, styles.title]}>
<Trans>
Welcome to{' '}
<Text style={[pal.text, pal.link, styles.title]}>Bluesky</Text>
</Trans>
</Text>
<View style={styles.spacer} />
<View style={[styles.row]}>
<FontAwesomeIcon icon={'globe'} size={36} color={pal.colors.link} />
<View style={[styles.rowText]}>
<Text type="lg-bold" style={[pal.text]}>
<Trans>Bluesky is public.</Trans>
</Text>
<Text type="lg-thin" style={[pal.text, s.pt2]}>
<Trans>
Your posts, likes, and blocks are public. Mutes are private.
</Trans>
</Text>
</View>
</View>
<View style={[styles.row]}>
<FontAwesomeIcon icon={'at'} size={36} color={pal.colors.link} />
<View style={[styles.rowText]}>
<Text type="lg-bold" style={[pal.text]}>
<Trans>Bluesky is open.</Trans>
</Text>
<Text type="lg-thin" style={[pal.text, s.pt2]}>
<Trans>Never lose access to your followers and data.</Trans>
</Text>
</View>
</View>
<View style={[styles.row]}>
<FontAwesomeIcon icon={'gear'} size={36} color={pal.colors.link} />
<View style={[styles.rowText]}>
<Text type="lg-bold" style={[pal.text]}>
<Trans>Bluesky is flexible.</Trans>
</Text>
<Text type="lg-thin" style={[pal.text, s.pt2]}>
<Trans>
Choose the algorithms that power your experience with custom
feeds.
</Trans>
</Text>
</View>
</View>
</View>
<Button
onPress={next}
label={_(msg`Continue`)}
testID="continueBtn"
style={[styles.buttonContainer]}
labelStyle={styles.buttonText}
/>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
marginBottom: 60,
marginHorizontal: 16,
justifyContent: 'space-between',
},
title: {
fontSize: 42,
fontWeight: '800',
},
row: {
flexDirection: 'row',
columnGap: 20,
alignItems: 'center',
marginVertical: 20,
},
rowText: {
flex: 1,
},
spacer: {
height: 20,
},
buttonContainer: {
alignItems: 'center',
},
buttonText: {
textAlign: 'center',
fontSize: 18,
marginVertical: 4,
},
})
+10 -4
View File
@@ -32,7 +32,7 @@ import {
import {useProfileQuery} from '#/state/queries/profile'
import {Gif} from '#/state/queries/tenor'
import {ThreadgateSetting} from '#/state/queries/threadgate'
import {getAgent, useSession} from '#/state/session'
import {useAgent, useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell/composer'
import {useAnalytics} from 'lib/analytics/analytics'
import * as apilib from 'lib/api/index'
@@ -53,7 +53,7 @@ import {atoms as a} from '#/alf'
import {Button} from '#/components/Button'
import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji'
import * as Prompt from '#/components/Prompt'
import {QuoteEmbed} from '../util/post-embeds/QuoteEmbed'
import {QuoteEmbed, QuoteX} from '../util/post-embeds/QuoteEmbed'
import {Text} from '../util/text/Text'
import * as Toast from '../util/Toast'
import {UserAvatar} from '../util/UserAvatar'
@@ -83,6 +83,7 @@ export const ComposePost = observer(function ComposePost({
imageUris: initImageUris,
}: Props) {
const {currentAccount} = useSession()
const {getAgent} = useAgent()
const {data: currentProfile} = useProfileQuery({did: currentAccount!.did})
const {isModalActive} = useModals()
const {closeComposer} = useComposerControls()
@@ -483,8 +484,13 @@ export const ComposePost = observer(function ComposePost({
/>
)}
{quote ? (
<View style={[s.mt5, isWeb && s.mb10, {pointerEvents: 'none'}]}>
<QuoteEmbed quote={quote} />
<View style={[s.mt5, isWeb && s.mb10]}>
<View style={{pointerEvents: 'none'}}>
<QuoteEmbed quote={quote} />
</View>
{quote.uri !== initQuote?.uri && (
<QuoteX onRemove={() => setQuote(undefined)} />
)}
</View>
) : undefined}
</ScrollView>
+16 -20
View File
@@ -28,8 +28,8 @@ import {getMentionAt, insertMentionAt} from 'lib/strings/mention-manip'
import {useTheme} from 'lib/ThemeContext'
import {isIOS} from 'platform/detection'
import {
addLinkCardIfNecessary,
findIndexInText,
LinkFacetMatch,
suggestLinkCardUri,
} from 'view/com/composer/text-input/text-input-util'
import {Text} from 'view/com/util/text/Text'
import {Autocomplete} from './mobile/Autocomplete'
@@ -73,7 +73,6 @@ export const TextInput = forwardRef(function TextInputImpl(
const theme = useTheme()
const [autocompletePrefix, setAutocompletePrefix] = useState('')
const prevLength = React.useRef(richtext.length)
const prevAddedLinks = useRef(new Set<string>())
React.useImperativeHandle(ref, () => ({
focus: () => textInput.current?.focus(),
@@ -83,6 +82,8 @@ export const TextInput = forwardRef(function TextInputImpl(
getCursorPosition: () => undefined, // Not implemented on native
}))
const pastSuggestedUris = useRef(new Set<string>())
const prevDetectedUris = useRef(new Map<string, LinkFacetMatch>())
const onChangeText = useCallback(
(newText: string) => {
/*
@@ -112,6 +113,7 @@ export const TextInput = forwardRef(function TextInputImpl(
setAutocompletePrefix('')
}
const nextDetectedUris = new Map<string, LinkFacetMatch>()
if (newRt.facets) {
for (const facet of newRt.facets) {
for (const feature of facet.features) {
@@ -130,32 +132,26 @@ export const TextInput = forwardRef(function TextInputImpl(
onPhotoPasted(res.path)
}
} else {
const cursorLocation = textInputSelection.current.end
addLinkCardIfNecessary({
uri: feature.uri,
newText,
cursorLocation,
mayBePaste,
onNewLink,
prevAddedLinks: prevAddedLinks.current,
})
nextDetectedUris.set(feature.uri, {facet, rt: newRt})
}
}
}
}
}
for (const uri of prevAddedLinks.current.keys()) {
if (findIndexInText(uri, newText) === -1) {
prevAddedLinks.current.delete(uri)
}
const suggestedUri = suggestLinkCardUri(
mayBePaste,
nextDetectedUris,
prevDetectedUris.current,
pastSuggestedUris.current,
)
prevDetectedUris.current = nextDetectedUris
if (suggestedUri) {
onNewLink(suggestedUri)
}
prevLength.current = newText.length
}, 1)
},
[setRichText, autocompletePrefix, onPhotoPasted, prevAddedLinks, onNewLink],
[setRichText, autocompletePrefix, onPhotoPasted, onNewLink],
)
const onPaste = useCallback(
@@ -19,8 +19,8 @@ import {useActorAutocompleteFn} from '#/state/queries/actor-autocomplete'
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
import {blobToDataUri, isUriImage} from 'lib/media/util'
import {
addLinkCardIfNecessary,
findIndexInText,
LinkFacetMatch,
suggestLinkCardUri,
} from 'view/com/composer/text-input/text-input-util'
import {Portal} from '#/components/Portal'
import {Text} from '../../util/text/Text'
@@ -61,9 +61,6 @@ export const TextInput = React.forwardRef(function TextInputImpl(
ref,
) {
const autocomplete = useActorAutocompleteFn()
const prevLength = React.useRef(0)
const prevAddedLinks = useRef(new Set<string>())
const pal = usePalette('default')
const modeClass = useColorSchemeStyle('ProseMirror-light', 'ProseMirror-dark')
@@ -144,6 +141,8 @@ export const TextInput = React.forwardRef(function TextInputImpl(
}
}, [setIsDropping])
const pastSuggestedUris = useRef(new Set<string>())
const prevDetectedUris = useRef(new Map<string, LinkFacetMatch>())
const editor = useEditor(
{
extensions,
@@ -185,42 +184,34 @@ export const TextInput = React.forwardRef(function TextInputImpl(
},
onUpdate({editor: editorProp}) {
const json = editorProp.getJSON()
const newText = editorJsonToText(json).trimEnd()
const mayBePaste = newText.length > prevLength.current + 1
const newText = editorJsonToText(json)
const isPaste = window.event?.type === 'paste'
const newRt = new RichText({text: newText})
newRt.detectFacetsWithoutResolution()
setRichText(newRt)
const nextDetectedUris = new Map<string, LinkFacetMatch>()
if (newRt.facets) {
for (const facet of newRt.facets) {
for (const feature of facet.features) {
if (AppBskyRichtextFacet.isLink(feature)) {
// The TipTap editor shows the position as being one character ahead, as if the start index is 1.
// Subtracting 1 from the pos gives us the same behavior as the native impl.
let cursorLocation = editor?.state.selection.$anchor.pos ?? 1
cursorLocation -= 1
addLinkCardIfNecessary({
uri: feature.uri,
newText,
cursorLocation,
mayBePaste,
onNewLink,
prevAddedLinks: prevAddedLinks.current,
})
nextDetectedUris.set(feature.uri, {facet, rt: newRt})
}
}
}
}
for (const uri of prevAddedLinks.current.keys()) {
if (findIndexInText(uri, newText) === -1) {
prevAddedLinks.current.delete(uri)
}
const suggestedUri = suggestLinkCardUri(
isPaste,
nextDetectedUris,
prevDetectedUris.current,
pastSuggestedUris.current,
)
prevDetectedUris.current = nextDetectedUris
if (suggestedUri) {
onNewLink(suggestedUri)
}
prevLength.current = newText.length
},
},
[modeClass],
@@ -277,15 +268,29 @@ export const TextInput = React.forwardRef(function TextInputImpl(
)
})
function editorJsonToText(json: JSONContent): string {
function editorJsonToText(
json: JSONContent,
isLastDocumentChild: boolean = false,
): string {
let text = ''
if (json.type === 'doc' || json.type === 'paragraph') {
if (json.type === 'doc') {
if (json.content?.length) {
for (const node of json.content) {
for (let i = 0; i < json.content.length; i++) {
const node = json.content[i]
const isLastNode = i === json.content.length - 1
text += editorJsonToText(node, isLastNode)
}
}
} else if (json.type === 'paragraph') {
if (json.content?.length) {
for (let i = 0; i < json.content.length; i++) {
const node = json.content[i]
text += editorJsonToText(node)
}
}
text += '\n'
if (!isLastDocumentChild) {
text += '\n'
}
} else if (json.type === 'hardBreak') {
text += '\n'
} else if (json.type === 'text') {
@@ -1,41 +1,85 @@
export function addLinkCardIfNecessary({
uri,
newText,
cursorLocation,
mayBePaste,
onNewLink,
prevAddedLinks,
}: {
uri: string
newText: string
cursorLocation: number
mayBePaste: boolean
onNewLink: (uri: string) => void
prevAddedLinks: Set<string>
}) {
// It would be cool if we could just use facet.index.byteEnd, but you know... *upside down smiley*
const lastCharacterPosition = findIndexInText(uri, newText) + uri.length
import {AppBskyRichtextFacet, RichText} from '@atproto/api'
// If the text being added is not from a paste, then we should only check if the cursor is one
// position ahead of the last character. However, if it is a paste we need to check both if it's
// the same position _or_ one position ahead. That is because iOS will add a space after a paste if
// pasting into the middle of a sentence!
const cursorLocationIsOkay =
cursorLocation === lastCharacterPosition + 1 || mayBePaste
export type LinkFacetMatch = {
rt: RichText
facet: AppBskyRichtextFacet.Main
}
// Checking previouslyAddedLinks keeps a card from getting added over and over i.e.
// Link card added -> Remove link card -> Press back space -> Press space -> Link card added -> and so on
// We use the isValidUrl regex below because we don't want to add embeds only if the url is valid, i.e.
// http://facebook is a valid url, but that doesn't mean we want to embed it. We should only embed if
// the url is a valid url _and_ domain. new URL() won't work for this check.
const shouldCheck =
cursorLocationIsOkay && !prevAddedLinks.has(uri) && isValidUrlAndDomain(uri)
if (shouldCheck) {
onNewLink(uri)
prevAddedLinks.add(uri)
export function suggestLinkCardUri(
mayBePaste: boolean,
nextDetectedUris: Map<string, LinkFacetMatch>,
prevDetectedUris: Map<string, LinkFacetMatch>,
pastSuggestedUris: Set<string>,
): string | undefined {
const suggestedUris = new Set<string>()
for (const [uri, nextMatch] of nextDetectedUris) {
if (!isValidUrlAndDomain(uri)) {
continue
}
if (pastSuggestedUris.has(uri)) {
// Don't suggest already added or already dismissed link cards.
continue
}
if (mayBePaste) {
// Immediately add the pasted link without waiting to type more.
suggestedUris.add(uri)
continue
}
const prevMatch = prevDetectedUris.get(uri)
if (!prevMatch) {
// If the same exact link wasn't already detected during the last keystroke,
// it means you're probably still typing it. Disregard until it stabilizes.
continue
}
const prevTextAfterUri = prevMatch.rt.unicodeText.slice(
prevMatch.facet.index.byteEnd,
)
const nextTextAfterUri = nextMatch.rt.unicodeText.slice(
nextMatch.facet.index.byteEnd,
)
if (prevTextAfterUri === nextTextAfterUri) {
// The text you're editing is before the link, e.g.
// "abc google.com" -> "abcd google.com".
// This is a good time to add the link.
suggestedUris.add(uri)
continue
}
if (/^\s/m.test(nextTextAfterUri)) {
// The link is followed by a space, e.g.
// "google.com" -> "google.com " or
// "google.com." -> "google.com ".
// This is a clear indicator we can linkify it.
suggestedUris.add(uri)
continue
}
if (
/^[)]?[.,:;!?)](\s|$)/m.test(prevTextAfterUri) &&
/^[)]?[.,:;!?)]\s/m.test(nextTextAfterUri)
) {
// The link was *already* being followed by punctuation,
// and now it's followed both by punctuation and a space.
// This means you're typing after punctuation, e.g.
// "google.com." -> "google.com. " or
// "google.com.foo" -> "google.com. foo".
// This means you're not typing the link anymore, so we can linkify it.
suggestedUris.add(uri)
continue
}
}
for (const uri of pastSuggestedUris) {
if (!nextDetectedUris.has(uri)) {
// If a link is no longer detected, it's eligible for suggestions next time.
pastSuggestedUris.delete(uri)
}
}
let suggestedUri: string | undefined
if (suggestedUris.size > 0) {
suggestedUri = Array.from(suggestedUris)[0]
pastSuggestedUris.add(suggestedUri)
}
return suggestedUri
}
// https://stackoverflow.com/questions/8667070/javascript-regular-expression-to-validate-url
@@ -46,14 +90,3 @@ function isValidUrlAndDomain(value: string) {
value,
)
}
export function findIndexInText(term: string, text: string) {
// This should find patterns like:
// HELLO SENTENCE http://google.com/ HELLO
// HELLO SENTENCE http://google.com HELLO
// http://google.com/ HELLO.
// http://google.com/.
const pattern = new RegExp(`\\b(${term})(?![/w])`, 'i')
const match = pattern.exec(text)
return match ? match.index : -1
}
@@ -14,11 +14,11 @@
* the facet-set.
*/
import {Mark} from '@tiptap/core'
import {Plugin, PluginKey} from '@tiptap/pm/state'
import {Node as ProsemirrorNode} from '@tiptap/pm/model'
import {Decoration, DecorationSet} from '@tiptap/pm/view'
import {URL_REGEX} from '@atproto/api'
import {Mark} from '@tiptap/core'
import {Node as ProsemirrorNode} from '@tiptap/pm/model'
import {Plugin, PluginKey} from '@tiptap/pm/state'
import {Decoration, DecorationSet} from '@tiptap/pm/view'
import {isValidDomain} from 'lib/strings/url-helpers'
@@ -91,7 +91,7 @@ function iterateUris(str: string, cb: (from: number, to: number) => void) {
uri = `https://${uri}`
}
let from = str.indexOf(match[2], match.index)
let to = from + match[2].length + 1
let to = from + match[2].length
// strip ending puncuation
if (/[.,;!?]$/.test(uri)) {
uri = uri.slice(0, -1)
@@ -1,12 +1,14 @@
import {useState, useEffect} from 'react'
import {useEffect, useState} from 'react'
import {useAgent} from '#/state/session'
import * as apilib from 'lib/api/index'
import {getLinkMeta} from 'lib/link-meta/link-meta'
import {ComposerOpts} from 'state/shell/composer'
import {getAgent} from '#/state/session'
export function useExternalLinkFetch({}: {
setQuote: (opts: ComposerOpts['quote']) => void
}) {
const {getAgent} = useAgent()
const [extLink, setExtLink] = useState<apilib.ExternalEmbedDraft | undefined>(
undefined,
)
@@ -39,7 +41,7 @@ export function useExternalLinkFetch({}: {
})
}
return cleanup
}, [extLink])
}, [extLink, getAgent])
return {extLink, setExtLink}
}
+13 -11
View File
@@ -1,24 +1,25 @@
import {useState, useEffect} from 'react'
import {ImageModel} from 'state/models/media/image'
import {useEffect, useState} from 'react'
import {logger} from '#/logger'
import {useFetchDid} from '#/state/queries/handle'
import {useGetPost} from '#/state/queries/post'
import {useAgent} from '#/state/session'
import * as apilib from 'lib/api/index'
import {getLinkMeta} from 'lib/link-meta/link-meta'
import {POST_IMG_MAX} from 'lib/constants'
import {
getPostAsQuote,
getFeedAsEmbed,
getListAsEmbed,
getPostAsQuote,
} from 'lib/link-meta/bsky'
import {getLinkMeta} from 'lib/link-meta/link-meta'
import {downloadAndResize} from 'lib/media/manip'
import {
isBskyPostUrl,
isBskyCustomFeedUrl,
isBskyListUrl,
isBskyPostUrl,
} from 'lib/strings/url-helpers'
import {ImageModel} from 'state/models/media/image'
import {ComposerOpts} from 'state/shell/composer'
import {POST_IMG_MAX} from 'lib/constants'
import {logger} from '#/logger'
import {getAgent} from '#/state/session'
import {useGetPost} from '#/state/queries/post'
import {useFetchDid} from '#/state/queries/handle'
export function useExternalLinkFetch({
setQuote,
@@ -30,6 +31,7 @@ export function useExternalLinkFetch({
)
const getPost = useGetPost()
const fetchDid = useFetchDid()
const {getAgent} = useAgent()
useEffect(() => {
let aborted = false
@@ -135,7 +137,7 @@ export function useExternalLinkFetch({
})
}
return cleanup
}, [extLink, setQuote, getPost, fetchDid])
}, [extLink, setQuote, getPost, fetchDid, getAgent])
return {extLink, setExtLink}
}
+13 -11
View File
@@ -1,19 +1,20 @@
import React, {useState} from 'react'
import {ActivityIndicator, SafeAreaView, StyleSheet, View} from 'react-native'
import {ScrollView, TextInput} from './util'
import {Text} from '../util/text/Text'
import {Button} from '../util/forms/Button'
import {ErrorMessage} from '../util/error/ErrorMessage'
import * as Toast from '../util/Toast'
import {s, colors} from 'lib/styles'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {useAgent, useSession, useSessionApi} from '#/state/session'
import {usePalette} from 'lib/hooks/usePalette'
import {isWeb} from 'platform/detection'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {cleanError} from 'lib/strings/errors'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {useSession, useSessionApi, getAgent} from '#/state/session'
import {colors, s} from 'lib/styles'
import {isWeb} from 'platform/detection'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {Button} from '../util/forms/Button'
import {Text} from '../util/text/Text'
import * as Toast from '../util/Toast'
import {ScrollView, TextInput} from './util'
enum Stages {
InputEmail,
@@ -26,6 +27,7 @@ export const snapPoints = ['90%']
export function Component() {
const pal = usePalette('default')
const {currentAccount} = useSession()
const {getAgent} = useAgent()
const {updateCurrentAccount} = useSessionApi()
const {_} = useLingui()
const [stage, setStage] = useState<Stages>(Stages.InputEmail)
+2 -1
View File
@@ -16,8 +16,8 @@ import {useModalControls} from '#/state/modals'
import {useFetchDid, useUpdateHandleMutation} from '#/state/queries/handle'
import {useServiceQuery} from '#/state/queries/service'
import {
getAgent,
SessionAccount,
useAgent,
useSession,
useSessionApi,
} from '#/state/session'
@@ -40,6 +40,7 @@ export type Props = {onChanged: () => void}
export function Component(props: Props) {
const {currentAccount} = useSession()
const {getAgent} = useAgent()
const {
isLoading,
data: serviceInfo,
+15 -13
View File
@@ -6,24 +6,25 @@ import {
TouchableOpacity,
View,
} from 'react-native'
import {ScrollView} from './util'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {TextInput} from './util'
import {Text} from '../util/text/Text'
import {Button} from '../util/forms/Button'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {s, colors} from 'lib/styles'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import * as EmailValidator from 'email-validator'
import {logger} from '#/logger'
import {useModalControls} from '#/state/modals'
import {useAgent, useSession} from '#/state/session'
import {usePalette} from 'lib/hooks/usePalette'
import {isAndroid, isWeb} from 'platform/detection'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {cleanError, isNetworkError} from 'lib/strings/errors'
import {checkAndFormatResetCode} from 'lib/strings/password'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {useSession, getAgent} from '#/state/session'
import * as EmailValidator from 'email-validator'
import {logger} from '#/logger'
import {colors, s} from 'lib/styles'
import {isAndroid, isWeb} from 'platform/detection'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {Button} from '../util/forms/Button'
import {Text} from '../util/text/Text'
import {ScrollView} from './util'
import {TextInput} from './util'
enum Stages {
RequestCode,
@@ -36,6 +37,7 @@ export const snapPoints = isAndroid ? ['90%'] : ['45%']
export function Component() {
const pal = usePalette('default')
const {currentAccount} = useSession()
const {getAgent} = useAgent()
const {_} = useLingui()
const [stage, setStage] = useState<Stages>(Stages.RequestCode)
const [isProcessing, setIsProcessing] = useState<boolean>(false)
+3 -1
View File
@@ -25,7 +25,7 @@ import {
useListCreateMutation,
useListMetadataMutation,
} from '#/state/queries/list'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
@@ -62,6 +62,7 @@ export function Component({
const {_} = useLingui()
const listCreateMutation = useListCreateMutation()
const listMetadataMutation = useListMetadataMutation()
const {getAgent} = useAgent()
const activePurpose = useMemo(() => {
if (list?.purpose) {
@@ -228,6 +229,7 @@ export function Component({
listMetadataMutation,
listCreateMutation,
_,
getAgent,
])
return (
+2 -1
View File
@@ -11,7 +11,7 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {getAgent, useSession, useSessionApi} from '#/state/session'
import {useAgent, useSession, useSessionApi} from '#/state/session'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {cleanError} from 'lib/strings/errors'
@@ -30,6 +30,7 @@ export function Component({}: {}) {
const pal = usePalette('default')
const theme = useTheme()
const {currentAccount} = useSession()
const {getAgent} = useAgent()
const {clearCurrentAccount, removeAccount} = useSessionApi()
const {_} = useLingui()
const {closeModal} = useModalControls()
+2 -1
View File
@@ -13,7 +13,7 @@ import {useLingui} from '@lingui/react'
import {logger} from '#/logger'
import {useModalControls} from '#/state/modals'
import {getAgent, useSession, useSessionApi} from '#/state/session'
import {useAgent, useSession, useSessionApi} from '#/state/session'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {cleanError} from 'lib/strings/errors'
@@ -41,6 +41,7 @@ export function Component({
onSuccess?: () => void
}) {
const pal = usePalette('default')
const {getAgent} = useAgent()
const {currentAccount} = useSession()
const {updateCurrentAccount} = useSessionApi()
const {_} = useLingui()
+15 -10
View File
@@ -1,26 +1,28 @@
import * as React from 'react'
import {
LayoutChangeEvent,
NativeScrollEvent,
ScrollView,
StyleSheet,
View,
NativeScrollEvent,
} from 'react-native'
import Animated, {
useAnimatedStyle,
useSharedValue,
AnimatedRef,
runOnJS,
runOnUI,
scrollTo,
useAnimatedRef,
AnimatedRef,
SharedValue,
useAnimatedRef,
useAnimatedStyle,
useSharedValue,
} from 'react-native-reanimated'
import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager'
import {TabBar} from './TabBar'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {ListMethods} from '../util/List'
import {ScrollProvider} from '#/lib/ScrollContext'
import {isIOS} from 'platform/detection'
import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager'
import {ListMethods} from '../util/List'
import {TabBar} from './TabBar'
export interface PagerWithHeaderChildParams {
headerHeight: number
@@ -236,9 +238,12 @@ let PagerTabBar = ({
const headerRef = React.useRef(null)
return (
<Animated.View
pointerEvents="box-none"
pointerEvents={isIOS ? 'auto' : 'box-none'}
style={[styles.tabBarMobile, headerTransform]}>
<View ref={headerRef} pointerEvents="box-none" collapsable={false}>
<View
ref={headerRef}
pointerEvents={isIOS ? 'auto' : 'box-none'}
collapsable={false}>
{renderHeader?.()}
{
// It wouldn't be enough to place `onLayout` on the parent node because
@@ -38,6 +38,7 @@ function PlaybackControls({
a.inset_0,
a.w_full,
a.h_full,
a.rounded_sm,
{
zIndex: 2,
backgroundColor: !isLoaded
+39 -2
View File
@@ -1,5 +1,11 @@
import React from 'react'
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {
StyleProp,
StyleSheet,
TouchableOpacity,
View,
ViewStyle,
} from 'react-native'
import {
AppBskyEmbedExternal,
AppBskyEmbedImages,
@@ -12,9 +18,13 @@ import {
RichText as RichTextAPI,
} from '@atproto/api'
import {AtUri} from '@atproto/api'
import {Trans} from '@lingui/macro'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import {HITSLOP_20} from '#/lib/constants'
import {s} from '#/lib/styles'
import {useModerationOpts} from '#/state/queries/preferences'
import {RQKEY as RQKEY_URI} from '#/state/queries/resolve-uri'
import {usePalette} from 'lib/hooks/usePalette'
@@ -177,6 +187,33 @@ export function QuoteEmbed({
)
}
export function QuoteX({onRemove}: {onRemove: () => void}) {
const {_} = useLingui()
return (
<TouchableOpacity
style={[
a.absolute,
a.p_xs,
a.rounded_full,
a.align_center,
a.justify_center,
{
top: 16,
right: 10,
backgroundColor: 'rgba(0, 0, 0, 0.75)',
},
]}
onPress={onRemove}
accessibilityRole="button"
accessibilityLabel={_(msg`Remove quote`)}
accessibilityHint={_(msg`Removes quoted post`)}
onAccessibilityEscape={onRemove}
hitSlop={HITSLOP_20}>
<FontAwesomeIcon size={12} icon="xmark" style={s.white} />
</TouchableOpacity>
)
}
function viewRecordToPostView(
viewRecord: AppBskyEmbedRecord.ViewRecord,
): AppBskyFeedDefs.PostView {
+3 -2
View File
@@ -19,7 +19,7 @@ import {resetProfilePostsQueries} from '#/state/queries/post-feed'
import {useModerationOpts} from '#/state/queries/preferences'
import {useProfileQuery} from '#/state/queries/profile'
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {getAgent, useSession} from '#/state/session'
import {useAgent, useSession} from '#/state/session'
import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell'
import {useComposerControls} from '#/state/shell/composer'
import {useAnalytics} from 'lib/analytics/analytics'
@@ -472,6 +472,7 @@ function ProfileScreenLoaded({
}
function useRichText(text: string): [RichTextAPI, boolean] {
const {getAgent} = useAgent()
const [prevText, setPrevText] = React.useState(text)
const [rawRT, setRawRT] = React.useState(() => new RichTextAPI({text}))
const [resolvedRT, setResolvedRT] = React.useState<RichTextAPI | null>(null)
@@ -495,7 +496,7 @@ function useRichText(text: string): [RichTextAPI, boolean] {
return () => {
ignore = true
}
}, [text])
}, [text, getAgent])
const isResolving = resolvedRT === null
return [resolvedRT ?? rawRT, isResolving]
}
+2 -61
View File
@@ -22,7 +22,6 @@ import {HITSLOP_10} from '#/lib/constants'
import {usePalette} from '#/lib/hooks/usePalette'
import {MagnifyingGlassIcon} from '#/lib/icons'
import {NavigationProp} from '#/lib/routes/types'
import {useGate} from '#/lib/statsig/statsig'
import {augmentSearchQuery} from '#/lib/strings/helpers'
import {s} from '#/lib/styles'
import {logger} from '#/logger'
@@ -32,10 +31,7 @@ import {useActorAutocompleteFn} from '#/state/queries/actor-autocomplete'
import {useActorSearch} from '#/state/queries/actor-search'
import {useModerationOpts} from '#/state/queries/preferences'
import {useSearchPostsQuery} from '#/state/queries/search-posts'
import {
useGetSuggestedFollowersByActor,
useSuggestedFollowsQuery,
} from '#/state/queries/suggested-follows'
import {useSuggestedFollowsQuery} from '#/state/queries/suggested-follows'
import {useSession} from '#/state/session'
import {useSetDrawerOpen} from '#/state/shell'
import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell'
@@ -123,56 +119,7 @@ function EmptyState({message, error}: {message: string; error?: string}) {
)
}
function useSuggestedFollowsV1(): [
AppBskyActorDefs.ProfileViewBasic[],
() => void,
] {
const {currentAccount} = useSession()
const [suggestions, setSuggestions] = React.useState<
AppBskyActorDefs.ProfileViewBasic[]
>([])
const getSuggestedFollowsByActor = useGetSuggestedFollowersByActor()
React.useEffect(() => {
async function getSuggestions() {
const friends = await getSuggestedFollowsByActor(
currentAccount!.did,
).then(friendsRes => friendsRes.suggestions)
if (!friends) return // :(
const friendsOfFriends = new Map<
string,
AppBskyActorDefs.ProfileViewBasic
>()
await Promise.all(
friends.slice(0, 4).map(friend =>
getSuggestedFollowsByActor(friend.did).then(foafsRes => {
for (const user of foafsRes.suggestions) {
if (user.associated?.labeler) continue
friendsOfFriends.set(user.did, user)
}
}),
),
)
setSuggestions(Array.from(friendsOfFriends.values()))
}
try {
getSuggestions()
} catch (e) {
logger.error(`SearchScreenSuggestedFollows: failed to get suggestions`, {
message: e,
})
}
}, [currentAccount, setSuggestions, getSuggestedFollowsByActor])
return [suggestions, () => {}]
}
function useSuggestedFollowsV2(): [
function useSuggestedFollows(): [
AppBskyActorDefs.ProfileViewBasic[],
() => void,
] {
@@ -212,12 +159,6 @@ function useSuggestedFollowsV2(): [
function SearchScreenSuggestedFollows() {
const pal = usePalette('default')
const gate = useGate()
const useSuggestedFollows = gate('use_new_suggestions_endpoint')
? // Conditional hook call here is *only* OK because useGate()
// result won't change until a remount.
useSuggestedFollowsV2
: useSuggestedFollowsV1
const [suggestions, onEndReached] = useSuggestedFollows()
return suggestions.length ? (
@@ -5,7 +5,7 @@ import {useLingui} from '@lingui/react'
import {cleanError} from '#/lib/strings/errors'
import {isNative} from '#/platform/detection'
import {getAgent, useSession, useSessionApi} from '#/state/session'
import {useAgent, useSession, useSessionApi} from '#/state/session'
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
@@ -31,6 +31,7 @@ export function DisableEmail2FADialog({
const {gtMobile} = useBreakpoints()
const {currentAccount} = useSession()
const {updateCurrentAccount} = useSessionApi()
const {getAgent} = useAgent()
const [stage, setStage] = useState<Stages>(Stages.Email)
const [confirmationCode, setConfirmationCode] = useState<string>('')
+3 -2
View File
@@ -3,7 +3,7 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {getAgent, useSession, useSessionApi} from '#/state/session'
import {useAgent, useSession, useSessionApi} from '#/state/session'
import {ToggleButton} from 'view/com/util/forms/ToggleButton'
import {useDialogControl} from '#/components/Dialog'
import {DisableEmail2FADialog} from './DisableEmail2FADialog'
@@ -14,6 +14,7 @@ export function Email2FAToggle() {
const {updateCurrentAccount} = useSessionApi()
const {openModal} = useModalControls()
const disableDialogCtrl = useDialogControl()
const {getAgent} = useAgent()
const enableEmailAuthFactor = React.useCallback(async () => {
if (currentAccount?.email) {
@@ -25,7 +26,7 @@ export function Email2FAToggle() {
emailAuthFactor: true,
})
}
}, [currentAccount, updateCurrentAccount])
}, [currentAccount, updateCurrentAccount, getAgent])
const onToggle = React.useCallback(() => {
if (!currentAccount) {
@@ -3,7 +3,7 @@ import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {getAgent, useSession} from '#/state/session'
import {useAgent, useSession} from '#/state/session'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
@@ -19,6 +19,7 @@ export function ExportCarDialog({
const t = useTheme()
const {gtMobile} = useBreakpoints()
const {currentAccount} = useSession()
const {getAgent} = useAgent()
const downloadUrl = React.useMemo(() => {
const agent = getAgent()
@@ -30,7 +31,7 @@ export function ExportCarDialog({
url.pathname = '/xrpc/com.atproto.sync.getRepo'
url.searchParams.set('did', agent.session.did)
return url.toString()
}, [currentAccount])
}, [currentAccount, getAgent])
return (
<Dialog.Outer control={control}>
+43 -43
View File
@@ -493,6 +493,49 @@ export function SettingsScreen({}: Props) {
<Trans>Accessibility</Trans>
</Text>
</TouchableOpacity>
<TouchableOpacity
testID="languageSettingsBtn"
style={[
styles.linkCard,
pal.view,
isSwitchingAccounts && styles.dimmed,
]}
onPress={isSwitchingAccounts ? undefined : onPressLanguageSettings}
accessibilityRole="button"
accessibilityLabel={_(msg`Language settings`)}
accessibilityHint={_(msg`Opens configurable language settings`)}>
<View style={[styles.iconContainer, pal.btn]}>
<FontAwesomeIcon
icon="language"
style={pal.text as FontAwesomeIconStyle}
/>
</View>
<Text type="lg" style={pal.text}>
<Trans>Languages</Trans>
</Text>
</TouchableOpacity>
<TouchableOpacity
testID="moderationBtn"
style={[
styles.linkCard,
pal.view,
isSwitchingAccounts && styles.dimmed,
]}
onPress={
isSwitchingAccounts
? undefined
: () => navigation.navigate('Moderation')
}
accessibilityRole="button"
accessibilityLabel={_(msg`Moderation settings`)}
accessibilityHint={_(msg`Opens moderation settings`)}>
<View style={[styles.iconContainer, pal.btn]}>
<HandIcon style={pal.text} size={18} strokeWidth={6} />
</View>
<Text type="lg" style={pal.text}>
<Trans>Moderation</Trans>
</Text>
</TouchableOpacity>
<TouchableOpacity
testID="preferencesHomeFeedButton"
style={[
@@ -554,49 +597,6 @@ export function SettingsScreen({}: Props) {
<Trans>My Saved Feeds</Trans>
</Text>
</TouchableOpacity>
<TouchableOpacity
testID="languageSettingsBtn"
style={[
styles.linkCard,
pal.view,
isSwitchingAccounts && styles.dimmed,
]}
onPress={isSwitchingAccounts ? undefined : onPressLanguageSettings}
accessibilityRole="button"
accessibilityLabel={_(msg`Language settings`)}
accessibilityHint={_(msg`Opens configurable language settings`)}>
<View style={[styles.iconContainer, pal.btn]}>
<FontAwesomeIcon
icon="language"
style={pal.text as FontAwesomeIconStyle}
/>
</View>
<Text type="lg" style={pal.text}>
<Trans>Languages</Trans>
</Text>
</TouchableOpacity>
<TouchableOpacity
testID="moderationBtn"
style={[
styles.linkCard,
pal.view,
isSwitchingAccounts && styles.dimmed,
]}
onPress={
isSwitchingAccounts
? undefined
: () => navigation.navigate('Moderation')
}
accessibilityRole="button"
accessibilityLabel={_(msg`Moderation settings`)}
accessibilityHint={_(msg`Opens moderation settings`)}>
<View style={[styles.iconContainer, pal.btn]}>
<HandIcon style={pal.text} size={18} strokeWidth={6} />
</View>
<Text type="lg" style={pal.text}>
<Trans>Moderation</Trans>
</Text>
</TouchableOpacity>
<View style={styles.spacer20} />
-27
View File
@@ -30,7 +30,6 @@ import {
BellIcon,
BellIconSolid,
CogIcon,
HandIcon,
HashtagIcon,
HomeIcon,
HomeIconSolid,
@@ -177,12 +176,6 @@ let DrawerContent = ({}: {}): React.ReactNode => {
setDrawerOpen(false)
}, [navigation, track, setDrawerOpen])
const onPressModeration = React.useCallback(() => {
track('Menu:ItemClicked', {url: 'Moderation'})
navigation.navigate('Moderation')
setDrawerOpen(false)
}, [navigation, track, setDrawerOpen])
const onPressSettings = React.useCallback(() => {
track('Menu:ItemClicked', {url: 'Settings'})
navigation.navigate('Settings')
@@ -240,7 +233,6 @@ let DrawerContent = ({}: {}): React.ReactNode => {
/>
<FeedsMenuItem isActive={isAtFeeds} onPress={onPressMyFeeds} />
<ListsMenuItem onPress={onPressLists} />
<ModerationMenuItem onPress={onPressModeration} />
<ProfileMenuItem
isActive={isAtMyProfile}
onPress={onPressProfile}
@@ -507,25 +499,6 @@ let ListsMenuItem = ({onPress}: {onPress: () => void}): React.ReactNode => {
}
ListsMenuItem = React.memo(ListsMenuItem)
let ModerationMenuItem = ({
onPress,
}: {
onPress: () => void
}): React.ReactNode => {
const {_} = useLingui()
const pal = usePalette('default')
return (
<MenuItem
icon={<HandIcon strokeWidth={5} style={pal.text} size={24} />}
label={_(msg`Moderation`)}
accessibilityLabel={_(msg`Moderation`)}
accessibilityHint=""
onPress={onPress}
/>
)
}
ModerationMenuItem = React.memo(ModerationMenuItem)
let ProfileMenuItem = ({
isActive,
onPress,
@@ -1,11 +1,8 @@
import * as React from 'react'
import {View} from 'react-native'
import {PWI_ENABLED, NEW_ONBOARDING_ENABLED} from '#/lib/build-flags'
// Based on @react-navigation/native-stack/src/createNativeStackNavigator.ts
// MIT License
// Copyright (c) 2017 React Navigation Contributors
import {
createNavigatorFactory,
EventArg,
@@ -21,24 +18,24 @@ import type {
NativeStackNavigationEventMap,
NativeStackNavigationOptions,
} from '@react-navigation/native-stack'
import type {NativeStackNavigatorProps} from '@react-navigation/native-stack/src/types'
import {NativeStackView} from '@react-navigation/native-stack'
import type {NativeStackNavigatorProps} from '@react-navigation/native-stack/src/types'
import {BottomBarWeb} from './bottom-bar/BottomBarWeb'
import {DesktopLeftNav} from './desktop/LeftNav'
import {DesktopRightNav} from './desktop/RightNav'
import {PWI_ENABLED} from '#/lib/build-flags'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {useSession} from '#/state/session'
import {useOnboardingState} from '#/state/shell'
import {
useLoggedOutView,
useLoggedOutViewControls,
} from '#/state/shell/logged-out'
import {useSession} from '#/state/session'
import {isWeb} from 'platform/detection'
import {Deactivated} from '#/screens/Deactivated'
import {Onboarding} from '#/screens/Onboarding'
import {LoggedOut} from '../com/auth/LoggedOut'
import {Onboarding} from '../com/auth/Onboarding'
import {Onboarding as NewOnboarding} from '#/screens/Onboarding'
import {BottomBarWeb} from './bottom-bar/BottomBarWeb'
import {DesktopLeftNav} from './desktop/LeftNav'
import {DesktopRightNav} from './desktop/RightNav'
type NativeStackNavigationOptionsWithAuth = NativeStackNavigationOptions & {
requireAuth?: boolean
@@ -112,11 +109,7 @@ function NativeStackNavigator({
return <LoggedOut onDismiss={() => setShowLoggedOut(false)} />
}
if (onboardingState.isActive) {
if (NEW_ONBOARDING_ENABLED) {
return <NewOnboarding />
} else {
return <Onboarding />
}
return <Onboarding />
}
const newDescriptors: typeof descriptors = {}
for (let key in descriptors) {
+48 -66
View File
@@ -1,52 +1,52 @@
import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {PressableWithHover} from 'view/com/util/PressableWithHover'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {
useLinkProps,
useNavigation,
useNavigationState,
} from '@react-navigation/native'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {Text} from 'view/com/util/text/Text'
import {UserAvatar} from 'view/com/util/UserAvatar'
import {Link} from 'view/com/util/Link'
import {LoadingPlaceholder} from 'view/com/util/LoadingPlaceholder'
import {isInvalidHandle} from '#/lib/strings/handles'
import {emitSoftReset} from '#/state/events'
import {useFetchHandle} from '#/state/queries/handle'
import {useUnreadNotifications} from '#/state/queries/notifications/unread'
import {useProfileQuery} from '#/state/queries/profile'
import {useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell/composer'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {s, colors} from 'lib/styles'
import {
HomeIcon,
HomeIconSolid,
MagnifyingGlassIcon2,
MagnifyingGlassIcon2Solid,
BellIcon,
BellIconSolid,
UserIcon,
UserIconSolid,
CogIcon,
CogIconSolid,
ComposeIcon2,
ListIcon,
HashtagIcon,
HandIcon,
HomeIcon,
HomeIconSolid,
ListIcon,
MagnifyingGlassIcon2,
MagnifyingGlassIcon2Solid,
UserIcon,
UserIconSolid,
} from 'lib/icons'
import {getCurrentRoute, isTab, isStateAtTabRoot} from 'lib/routes/helpers'
import {NavigationProp, CommonNavigatorParams} from 'lib/routes/types'
import {router} from '../../../routes'
import {getCurrentRoute, isStateAtTabRoot, isTab} from 'lib/routes/helpers'
import {makeProfileLink} from 'lib/routes/links'
import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro'
import {useProfileQuery} from '#/state/queries/profile'
import {useSession} from '#/state/session'
import {useUnreadNotifications} from '#/state/queries/notifications/unread'
import {useComposerControls} from '#/state/shell/composer'
import {useFetchHandle} from '#/state/queries/handle'
import {emitSoftReset} from '#/state/events'
import {CommonNavigatorParams, NavigationProp} from 'lib/routes/types'
import {colors, s} from 'lib/styles'
import {NavSignupCard} from '#/view/shell/NavSignupCard'
import {isInvalidHandle} from '#/lib/strings/handles'
import {Link} from 'view/com/util/Link'
import {LoadingPlaceholder} from 'view/com/util/LoadingPlaceholder'
import {PressableWithHover} from 'view/com/util/PressableWithHover'
import {Text} from 'view/com/util/text/Text'
import {UserAvatar} from 'view/com/util/UserAvatar'
import {router} from '../../../routes'
function ProfileCard() {
const {currentAccount} = useSession()
@@ -327,24 +327,6 @@ export function DesktopLeftNav() {
}
label={_(msg`Search`)}
/>
<NavItem
href="/feeds"
icon={
<HashtagIcon
strokeWidth={2.25}
style={pal.text as FontAwesomeIconStyle}
size={isDesktop ? 24 : 28}
/>
}
iconFilled={
<HashtagIcon
strokeWidth={4}
style={pal.text as FontAwesomeIconStyle}
size={isDesktop ? 24 : 28}
/>
}
label={_(msg`Feeds`)}
/>
<NavItem
href="/notifications"
count={numUnread}
@@ -364,6 +346,24 @@ export function DesktopLeftNav() {
}
label={_(msg`Notifications`)}
/>
<NavItem
href="/feeds"
icon={
<HashtagIcon
strokeWidth={2.25}
style={pal.text as FontAwesomeIconStyle}
size={isDesktop ? 24 : 28}
/>
}
iconFilled={
<HashtagIcon
strokeWidth={4}
style={pal.text as FontAwesomeIconStyle}
size={isDesktop ? 24 : 28}
/>
}
label={_(msg`Feeds`)}
/>
<NavItem
href="/lists"
icon={
@@ -382,24 +382,6 @@ export function DesktopLeftNav() {
}
label={_(msg`Lists`)}
/>
<NavItem
href="/moderation"
icon={
<HandIcon
style={pal.text}
size={isDesktop ? 24 : 27}
strokeWidth={5.5}
/>
}
iconFilled={
<FontAwesomeIcon
icon="hand"
style={pal.text as FontAwesomeIconStyle}
size={isDesktop ? 23 : 26}
/>
}
label={_(msg`Moderation`)}
/>
<NavItem
href={currentAccount ? makeProfileLink(currentAccount) : '/'}
icon={
+8 -4
View File
@@ -13,7 +13,7 @@ import * as NavigationBar from 'expo-navigation-bar'
import {StatusBar} from 'expo-status-bar'
import {useNavigationState} from '@react-navigation/native'
import {useSession} from '#/state/session'
import {useAgent, useSession} from '#/state/session'
import {
useIsDrawerOpen,
useIsDrawerSwipeDisabled,
@@ -57,6 +57,7 @@ function ShellInner() {
)
const canGoBack = useNavigationState(state => !isStateAtTabRoot(state))
const {hasSession, currentAccount} = useSession()
const {getAgent} = useAgent()
const closeAnyActiveElement = useCloseAnyActiveElement()
const {importantForAccessibility} = useDialogStateContext()
// start undefined
@@ -78,11 +79,14 @@ function ShellInner() {
// only runs when did changes
if (currentAccount && currentAccountDid.current !== currentAccount.did) {
currentAccountDid.current = currentAccount.did
notifications.requestPermissionsAndRegisterToken(currentAccount)
const unsub = notifications.registerTokenChangeHandler(currentAccount)
notifications.requestPermissionsAndRegisterToken(getAgent, currentAccount)
const unsub = notifications.registerTokenChangeHandler(
getAgent,
currentAccount,
)
return unsub
}
}, [currentAccount])
}, [currentAccount, getAgent])
return (
<>