merge main

This commit is contained in:
Hailey
2024-04-04 16:55:00 -07:00
parent 55acb43655
commit 872f5c062f
163 changed files with 20657 additions and 4939 deletions
+56 -52
View File
@@ -12,19 +12,16 @@ import {
import * as SplashScreen from 'expo-splash-screen'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {PersistQueryClientProvider} from '@tanstack/react-query-persist-client'
import {useQueryClient} from '@tanstack/react-query'
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
import {init as initPersistedState} from '#/state/persisted'
import * as persisted from '#/state/persisted'
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
import {useIntentHandler} from 'lib/hooks/useIntentHandler'
import * as notifications from 'lib/notifications/notifications'
import {
asyncStoragePersister,
dehydrateOptions,
queryClient,
} from 'lib/react-query'
import {useOTAUpdates} from 'lib/hooks/useOTAUpdates'
import {useNotificationsListener} from 'lib/notifications/notifications'
import {QueryProvider} from 'lib/react-query'
import {s} from 'lib/styles'
import {ThemeProvider} from 'lib/ThemeContext'
import {Provider as DialogStateProvider} from 'state/dialogs'
@@ -59,11 +56,12 @@ function InnerApp() {
const {resumeSession} = useSessionApi()
const theme = useColorModeTheme()
const {_} = useLingui()
useIntentHandler()
useOTAUpdates()
// init
useEffect(() => {
notifications.init(queryClient)
listenSessionDropped(() => {
Toast.show(_(msg`Sorry! Your session expired. Please log in again.`))
})
@@ -79,25 +77,29 @@ function InnerApp() {
<React.Fragment
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<StatsigProvider>
<LabelDefsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<UnreadNotifsProvider>
<ThemeProvider theme={theme}>
{/* All components should be within this provider */}
<RootSiblingParent>
<GestureHandlerRootView style={s.h100pct}>
<TestCtrls />
<Shell />
</GestureHandlerRootView>
</RootSiblingParent>
</ThemeProvider>
</UnreadNotifsProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</LabelDefsProvider>
</StatsigProvider>
<QueryProvider currentDid={currentAccount?.did}>
<PushNotificationsListener>
<StatsigProvider>
<LabelDefsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<UnreadNotifsProvider>
<ThemeProvider theme={theme}>
{/* All components should be within this provider */}
<RootSiblingParent>
<GestureHandlerRootView style={s.h100pct}>
<TestCtrls />
<Shell />
</GestureHandlerRootView>
</RootSiblingParent>
</ThemeProvider>
</UnreadNotifsProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</LabelDefsProvider>
</StatsigProvider>
</PushNotificationsListener>
</QueryProvider>
</React.Fragment>
</Splash>
</Alf>
@@ -105,6 +107,12 @@ function InnerApp() {
)
}
function PushNotificationsListener({children}: {children: React.ReactNode}) {
const queryClient = useQueryClient()
useNotificationsListener(queryClient)
return children
}
function App() {
const [isReady, setReady] = useState(false)
@@ -121,31 +129,27 @@ function App() {
* that is set up in the InnerApp component above.
*/
return (
<PersistQueryClientProvider
client={queryClient}
persistOptions={{persister: asyncStoragePersister, dehydrateOptions}}>
<SessionProvider>
<ShellStateProvider>
<PrefsStateProvider>
<MutedThreadsProvider>
<InvitesStateProvider>
<ModalStateProvider>
<DialogStateProvider>
<LightboxStateProvider>
<I18nProvider>
<PortalProvider>
<InnerApp />
</PortalProvider>
</I18nProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
</MutedThreadsProvider>
</PrefsStateProvider>
</ShellStateProvider>
</SessionProvider>
</PersistQueryClientProvider>
<SessionProvider>
<ShellStateProvider>
<PrefsStateProvider>
<MutedThreadsProvider>
<InvitesStateProvider>
<ModalStateProvider>
<DialogStateProvider>
<LightboxStateProvider>
<I18nProvider>
<PortalProvider>
<InnerApp />
</PortalProvider>
</I18nProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
</MutedThreadsProvider>
</PrefsStateProvider>
</ShellStateProvider>
</SessionProvider>
)
}
+66 -74
View File
@@ -1,44 +1,38 @@
import 'lib/sentry' // must be near top
import React, {useState, useEffect} from 'react'
import {PersistQueryClientProvider} from '@tanstack/react-query-persist-client'
import {SafeAreaProvider} from 'react-native-safe-area-context'
import {RootSiblingParent} from 'react-native-root-siblings'
import 'view/icons'
import {ThemeProvider as Alf} from '#/alf'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
import React, {useEffect, useState} from 'react'
import {RootSiblingParent} from 'react-native-root-siblings'
import {SafeAreaProvider} from 'react-native-safe-area-context'
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
import {init as initPersistedState} from '#/state/persisted'
import {Shell} from 'view/shell/index'
import {ToastContainer} from 'view/com/util/Toast.web'
import {ThemeProvider} from 'lib/ThemeContext'
import {
queryClient,
asyncStoragePersister,
dehydrateOptions,
} from 'lib/react-query'
import {Provider as ShellStateProvider} from 'state/shell'
import {Provider as ModalStateProvider} from 'state/modals'
import {Provider as DialogStateProvider} from 'state/dialogs'
import {Provider as LightboxStateProvider} from 'state/lightbox'
import {Provider as MutedThreadsProvider} from 'state/muted-threads'
import {Provider as InvitesStateProvider} from 'state/invites'
import {Provider as PrefsStateProvider} from 'state/preferences'
import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out'
import {Provider as SelectedFeedProvider} from 'state/shell/selected-feed'
import * as persisted from '#/state/persisted'
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
import I18nProvider from './locale/i18nProvider'
import {useIntentHandler} from 'lib/hooks/useIntentHandler'
import {QueryProvider} from 'lib/react-query'
import {ThemeProvider} from 'lib/ThemeContext'
import {Provider as DialogStateProvider} from 'state/dialogs'
import {Provider as InvitesStateProvider} from 'state/invites'
import {Provider as LightboxStateProvider} from 'state/lightbox'
import {Provider as ModalStateProvider} from 'state/modals'
import {Provider as MutedThreadsProvider} from 'state/muted-threads'
import {Provider as PrefsStateProvider} from 'state/preferences'
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
import {
Provider as SessionProvider,
useSession,
useSessionApi,
} from 'state/session'
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
import * as persisted from '#/state/persisted'
import {Provider as ShellStateProvider} from 'state/shell'
import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out'
import {Provider as SelectedFeedProvider} from 'state/shell/selected-feed'
import {ToastContainer} from 'view/com/util/Toast.web'
import {Shell} from 'view/shell/index'
import {ThemeProvider as Alf} from '#/alf'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
import {Provider as PortalProvider} from '#/components/Portal'
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
import {useIntentHandler} from 'lib/hooks/useIntentHandler'
import I18nProvider from './locale/i18nProvider'
function InnerApp() {
const {isInitialLoad, currentAccount} = useSession()
@@ -60,25 +54,27 @@ function InnerApp() {
<React.Fragment
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<StatsigProvider>
<LabelDefsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<UnreadNotifsProvider>
<ThemeProvider theme={theme}>
{/* All components should be within this provider */}
<RootSiblingParent>
<SafeAreaProvider>
<Shell />
</SafeAreaProvider>
</RootSiblingParent>
<ToastContainer />
</ThemeProvider>
</UnreadNotifsProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</LabelDefsProvider>
</StatsigProvider>
<QueryProvider currentDid={currentAccount?.did}>
<StatsigProvider>
<LabelDefsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<UnreadNotifsProvider>
<ThemeProvider theme={theme}>
{/* All components should be within this provider */}
<RootSiblingParent>
<SafeAreaProvider>
<Shell />
</SafeAreaProvider>
</RootSiblingParent>
<ToastContainer />
</ThemeProvider>
</UnreadNotifsProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</LabelDefsProvider>
</StatsigProvider>
</QueryProvider>
</React.Fragment>
</Alf>
)
@@ -100,31 +96,27 @@ function App() {
* that is set up in the InnerApp component above.
*/
return (
<PersistQueryClientProvider
client={queryClient}
persistOptions={{persister: asyncStoragePersister, dehydrateOptions}}>
<SessionProvider>
<ShellStateProvider>
<PrefsStateProvider>
<MutedThreadsProvider>
<InvitesStateProvider>
<ModalStateProvider>
<DialogStateProvider>
<LightboxStateProvider>
<I18nProvider>
<PortalProvider>
<InnerApp />
</PortalProvider>
</I18nProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
</MutedThreadsProvider>
</PrefsStateProvider>
</ShellStateProvider>
</SessionProvider>
</PersistQueryClientProvider>
<SessionProvider>
<ShellStateProvider>
<PrefsStateProvider>
<MutedThreadsProvider>
<InvitesStateProvider>
<ModalStateProvider>
<DialogStateProvider>
<LightboxStateProvider>
<I18nProvider>
<PortalProvider>
<InnerApp />
</PortalProvider>
</I18nProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
</MutedThreadsProvider>
</PrefsStateProvider>
</ShellStateProvider>
</SessionProvider>
)
}
+141
View File
@@ -0,0 +1,141 @@
import React, {useCallback} from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useProfileQuery} from '#/state/queries/profile'
import {type SessionAccount, useSession} from '#/state/session'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme} from '#/alf'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {ChevronRight_Stroke2_Corner0_Rounded as Chevron} from '#/components/icons/Chevron'
import {Button} from './Button'
import {Text} from './Typography'
export function AccountList({
onSelectAccount,
onSelectOther,
otherLabel,
}: {
onSelectAccount: (account: SessionAccount) => void
onSelectOther: () => void
otherLabel?: string
}) {
const {isSwitchingAccounts, currentAccount, accounts} = useSession()
const t = useTheme()
const {_} = useLingui()
const onPressAddAccount = useCallback(() => {
onSelectOther()
}, [onSelectOther])
return (
<View
style={[
a.rounded_md,
a.overflow_hidden,
a.border,
t.atoms.border_contrast_low,
]}>
{accounts.map(account => (
<React.Fragment key={account.did}>
<AccountItem
account={account}
onSelect={onSelectAccount}
isCurrentAccount={account.did === currentAccount?.did}
/>
<View style={[a.border_b, t.atoms.border_contrast_low]} />
</React.Fragment>
))}
<Button
testID="chooseAddAccountBtn"
style={[a.flex_1]}
onPress={isSwitchingAccounts ? undefined : onPressAddAccount}
label={_(msg`Login to account that is not listed`)}>
{({hovered, pressed}) => (
<View
style={[
a.flex_1,
a.flex_row,
a.align_center,
{height: 48},
(hovered || pressed || isSwitchingAccounts) &&
t.atoms.bg_contrast_25,
]}>
<Text
style={[
a.align_baseline,
a.flex_1,
a.flex_row,
a.py_sm,
{paddingLeft: 48},
]}>
{otherLabel ?? <Trans>Other account</Trans>}
</Text>
<Chevron size="sm" style={[t.atoms.text, a.mr_md]} />
</View>
)}
</Button>
</View>
)
}
function AccountItem({
account,
onSelect,
isCurrentAccount,
}: {
account: SessionAccount
onSelect: (account: SessionAccount) => void
isCurrentAccount: boolean
}) {
const t = useTheme()
const {_} = useLingui()
const {data: profile} = useProfileQuery({did: account.did})
const onPress = React.useCallback(() => {
onSelect(account)
}, [account, onSelect])
return (
<Button
testID={`chooseAccountBtn-${account.handle}`}
key={account.did}
style={[a.flex_1]}
onPress={onPress}
label={
isCurrentAccount
? _(msg`Continue as ${account.handle} (currently signed in)`)
: _(msg`Sign in as ${account.handle}`)
}>
{({hovered, pressed}) => (
<View
style={[
a.flex_1,
a.flex_row,
a.align_center,
{height: 48},
(hovered || pressed) && t.atoms.bg_contrast_25,
]}>
<View style={a.p_md}>
<UserAvatar avatar={profile?.avatar} size={24} />
</View>
<Text style={[a.align_baseline, a.flex_1, a.flex_row, a.py_sm]}>
<Text style={[a.font_bold]}>
{profile?.displayName || account.handle}{' '}
</Text>
<Text style={[t.atoms.text_contrast_medium]}>{account.handle}</Text>
</Text>
{isCurrentAccount ? (
<Check
size="sm"
style={[{color: t.palette.positive_600}, a.mr_md]}
/>
) : (
<Chevron size="sm" style={[t.atoms.text, a.mr_md]} />
)}
</View>
)}
</Button>
)
}
+9 -41
View File
@@ -14,7 +14,6 @@ import {
import LinearGradient from 'react-native-linear-gradient'
import {Trans} from '@lingui/macro'
import {logger} from '#/logger'
import {android, atoms as a, flatten, tokens, useTheme} from '#/alf'
import {Props as SVGIconProps} from '#/components/icons/common'
import {normalizeTextStyles} from '#/components/Typography'
@@ -405,51 +404,20 @@ export function Button({
</View>
)}
<Context.Provider value={context}>
<ButtonTextErrorBoundary>
{/* @ts-ignore */}
{typeof children === 'string' || children?.type === Trans ? (
/* @ts-ignore */
<ButtonText>{children}</ButtonText>
) : typeof children === 'function' ? (
children(context)
) : (
children
)}
</ButtonTextErrorBoundary>
{/* @ts-ignore */}
{typeof children === 'string' || children?.type === Trans ? (
/* @ts-ignore */
<ButtonText>{children}</ButtonText>
) : typeof children === 'function' ? (
children(context)
) : (
children
)}
</Context.Provider>
</Pressable>
)
}
export class ButtonTextErrorBoundary extends React.Component<
React.PropsWithChildren<{}>,
{hasError: boolean; error: Error | undefined}
> {
public state = {
hasError: false,
error: undefined,
}
public static getDerivedStateFromError(error: Error) {
return {hasError: true, error}
}
public componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
logger.error('ButtonTextErrorBoundary caught an error', {
message: error.message,
errorInfo,
})
}
public render() {
if (this.state.hasError) {
return <ButtonText>ERROR</ButtonText>
}
return this.props.children
}
}
export function useSharedButtonTextStyles() {
const t = useTheme()
const {color, variant, disabled, size} = useButtonContext()
+2 -2
View File
@@ -1,9 +1,9 @@
import React from 'react'
import {View} from 'react-native'
import {useNavigation} from '@react-navigation/core'
import {StackActions} from '@react-navigation/native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/core'
import {StackActions} from '@react-navigation/native'
import {NavigationProp} from 'lib/routes/types'
import {CenteredView} from 'view/com/util/Views'
+47 -49
View File
@@ -1,47 +1,54 @@
import React from 'react'
import {View} from 'react-native'
import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
import {Trans} from '@lingui/macro'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {logger} from '#/logger'
import {List} from '#/view/com/util/List'
import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
import {useLikedByQuery} from '#/state/queries/post-liked-by'
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
import {ListFooter} from '#/components/Lists'
import {cleanError} from 'lib/strings/errors'
import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
import {List} from '#/view/com/util/List'
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
import {atoms as a, useTheme} from '#/alf'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
function renderItem({item}: {item: GetLikes.Like}) {
return <ProfileCardWithFollowBtn key={item.actor.did} profile={item.actor} />
}
function keyExtractor(item: GetLikes.Like) {
return item.actor.did
}
export function LikedByList({uri}: {uri: string}) {
const t = useTheme()
const {_} = useLingui()
const initialNumToRender = useInitialNumToRender()
const [isPTRing, setIsPTRing] = React.useState(false)
const {
data: resolvedUri,
error: resolveError,
isFetching: isFetchingResolvedUri,
isLoading: isUriLoading,
} = useResolveUriQuery(uri)
const {
data,
isFetching,
isFetched,
isRefetching,
isLoading: isLikedByLoading,
isFetchingNextPage,
hasNextPage,
fetchNextPage,
isError,
error: likedByError,
refetch,
} = useLikedByQuery(resolvedUri?.uri)
const error = resolveError || likedByError
const isError = !!resolveError || !!likedByError
const likes = React.useMemo(() => {
if (data?.pages) {
return data.pages.flatMap(page => page.likes)
}
return []
}, [data])
const initialNumToRender = useInitialNumToRender()
const error = resolveError || likedByError
const onRefresh = React.useCallback(async () => {
setIsPTRing(true)
@@ -54,56 +61,47 @@ export function LikedByList({uri}: {uri: string}) {
}, [refetch, setIsPTRing])
const onEndReached = React.useCallback(async () => {
if (isFetching || !hasNextPage || isError) return
if (isFetchingNextPage || !hasNextPage || isError) return
try {
await fetchNextPage()
} catch (err) {
logger.error('Failed to load more likes', {message: err})
}
}, [isFetching, hasNextPage, isError, fetchNextPage])
}, [isFetchingNextPage, hasNextPage, isError, fetchNextPage])
const renderItem = React.useCallback(({item}: {item: GetLikes.Like}) => {
if (likes.length < 1) {
return (
<ProfileCardWithFollowBtn key={item.actor.did} profile={item.actor} />
)
}, [])
if (isFetchingResolvedUri || !isFetched) {
return (
<View style={[a.w_full, a.align_center, a.p_lg]}>
<Loader size="xl" />
</View>
<ListMaybePlaceholder
isLoading={isUriLoading || isLikedByLoading}
isError={isError}
emptyType="results"
emptyMessage={_(
msg`Nobody has liked this yet. Maybe you should be the first!`,
)}
errorMessage={cleanError(resolveError || error)}
onRetry={isError ? refetch : undefined}
/>
)
}
return likes.length ? (
return (
<List
data={likes}
keyExtractor={item => item.actor.did}
renderItem={renderItem}
keyExtractor={keyExtractor}
refreshing={isPTRing}
onRefresh={onRefresh}
onEndReached={onEndReached}
onEndReachedThreshold={3}
renderItem={renderItem}
initialNumToRender={initialNumToRender}
ListFooterComponent={() => (
ListFooterComponent={
<ListFooter
isFetching={isFetching && !isRefetching}
isError={isError}
error={error ? error.toString() : undefined}
isFetchingNextPage={isFetchingNextPage}
error={cleanError(error)}
onRetry={fetchNextPage}
/>
)}
}
onEndReachedThreshold={3}
initialNumToRender={initialNumToRender}
windowSize={11}
/>
) : (
<View style={[a.p_lg]}>
<View style={[a.p_lg, a.rounded_sm, t.atoms.bg_contrast_25]}>
<Text style={[a.text_md, a.leading_snug]}>
<Trans>
Nobody has liked this yet. Maybe you should be the first!
</Trans>
</Text>
</View>
</View>
)
}
+44 -10
View File
@@ -1,23 +1,24 @@
import React from 'react'
import {GestureResponderEvent} from 'react-native'
import {useLinkProps, StackActions} from '@react-navigation/native'
import {sanitizeUrl} from '@braintree/sanitize-url'
import {StackActions, useLinkProps} from '@react-navigation/native'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {isWeb} from '#/platform/detection'
import {useTheme, web, flatten, TextStyleProp, atoms as a} from '#/alf'
import {Button, ButtonProps} from '#/components/Button'
import {AllNavigatorParams} from '#/lib/routes/types'
import {shareUrl} from '#/lib/sharing'
import {
convertBskyAppUrlIfNeeded,
isExternalUrl,
linkRequiresWarning,
} from '#/lib/strings/url-helpers'
import {isNative, isWeb} from '#/platform/detection'
import {useModalControls} from '#/state/modals'
import {router} from '#/routes'
import {Text, TextProps} from '#/components/Typography'
import {useOpenLink} from 'state/preferences/in-app-browser'
import {useOpenLink} from '#/state/preferences/in-app-browser'
import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped'
import {atoms as a, flatten, TextStyleProp, useTheme, web} from '#/alf'
import {Button, ButtonProps} from '#/components/Button'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {Text, TextProps} from '#/components/Typography'
import {router} from '#/routes'
/**
* Only available within a `Link`, since that inherits from `Button`.
@@ -60,6 +61,11 @@ type BaseLinkProps = Pick<
* Web-only attribute. Sets `download` attr on web.
*/
download?: string
/**
* Native-only attribute. If true, will open the share sheet on long press.
*/
shareOnLongPress?: boolean
}
export function useLink({
@@ -68,6 +74,7 @@ export function useLink({
action = 'push',
disableMismatchWarning,
onPress: outerOnPress,
shareOnLongPress,
}: BaseLinkProps & {
displayText: string
}) {
@@ -157,10 +164,34 @@ export function useLink({
],
)
const handleLongPress = React.useCallback(() => {
const requiresWarning = Boolean(
!disableMismatchWarning &&
displayText &&
isExternal &&
linkRequiresWarning(href, displayText),
)
if (requiresWarning) {
openModal({
name: 'link-warning',
text: displayText,
href: href,
share: true,
})
} else {
shareUrl(href)
}
}, [disableMismatchWarning, displayText, href, isExternal, openModal])
const onLongPress =
isNative && isExternal && shareOnLongPress ? handleLongPress : undefined
return {
isExternal,
href,
onPress,
onLongPress,
}
}
@@ -219,7 +250,7 @@ export type InlineLinkProps = React.PropsWithChildren<
BaseLinkProps & TextStyleProp & Pick<TextProps, 'selectable'>
>
export function InlineLink({
export function InlineLinkText({
children,
to,
action = 'push',
@@ -229,16 +260,18 @@ export function InlineLink({
download,
selectable,
label,
shareOnLongPress,
...rest
}: InlineLinkProps) {
const t = useTheme()
const stringChildren = typeof children === 'string'
const {href, isExternal, onPress} = useLink({
const {href, isExternal, onPress, onLongPress} = useLink({
to,
displayText: stringChildren ? children : '',
action,
disableMismatchWarning,
onPress: outerOnPress,
shareOnLongPress,
})
const {
state: hovered,
@@ -270,6 +303,7 @@ export function InlineLink({
]}
role="link"
onPress={download ? undefined : onPress}
onLongPress={onLongPress}
onPressIn={onPressIn}
onPressOut={onPressOut}
onFocus={onFocus}
+25 -31
View File
@@ -1,25 +1,23 @@
import React from 'react'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro'
import {CenteredView} from 'view/com/util/Views'
import {Loader} from '#/components/Loader'
import {cleanError} from 'lib/strings/errors'
import {CenteredView} from 'view/com/util/Views'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import {Text} from '#/components/Typography'
import {Error} from '#/components/Error'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
export function ListFooter({
isFetching,
isError,
isFetchingNextPage,
error,
onRetry,
height,
}: {
isFetching?: boolean
isError?: boolean
isFetchingNextPage?: boolean
error?: string
onRetry?: () => Promise<unknown>
height?: number
@@ -36,32 +34,26 @@ export function ListFooter({
t.atoms.border_contrast_low,
{height: height ?? 180, paddingTop: 30},
]}>
{isFetching ? (
{isFetchingNextPage ? (
<Loader size="xl" />
) : (
<ListFooterMaybeError
isError={isError}
error={error}
onRetry={onRetry}
/>
<ListFooterMaybeError error={error} onRetry={onRetry} />
)}
</View>
)
}
function ListFooterMaybeError({
isError,
error,
onRetry,
}: {
isError?: boolean
error?: string
onRetry?: () => Promise<unknown>
}) {
const t = useTheme()
const {_} = useLingui()
if (!isError) return null
if (!error) return null
return (
<View style={[a.w_full, a.px_lg]}>
@@ -128,7 +120,7 @@ export function ListHeaderDesktop({
export function ListMaybePlaceholder({
isLoading,
isEmpty,
noEmpty,
isError,
emptyTitle,
emptyMessage,
@@ -138,7 +130,7 @@ export function ListMaybePlaceholder({
onRetry,
}: {
isLoading: boolean
isEmpty?: boolean
noEmpty?: boolean
isError?: boolean
emptyTitle?: string
emptyMessage?: string
@@ -151,16 +143,6 @@ export function ListMaybePlaceholder({
const {_} = useLingui()
const {gtMobile, gtTablet} = useBreakpoints()
if (!isLoading && isError) {
return (
<Error
title={errorTitle ?? _(msg`Oops!`)}
message={errorMessage ?? _(`Something went wrong!`)}
onRetry={onRetry}
/>
)
}
if (isLoading) {
return (
<CenteredView
@@ -180,7 +162,17 @@ export function ListMaybePlaceholder({
)
}
if (isEmpty) {
if (isError) {
return (
<Error
title={errorTitle ?? _(msg`Oops!`)}
message={errorMessage ?? _(`Something went wrong!`)}
onRetry={onRetry}
/>
)
}
if (!noEmpty) {
return (
<Error
title={
@@ -197,4 +189,6 @@ export function ListMaybePlaceholder({
/>
)
}
return null
}
+2 -2
View File
@@ -1,13 +1,13 @@
import React from 'react'
import Animated, {
Easing,
useSharedValue,
useAnimatedStyle,
useSharedValue,
withRepeat,
withTiming,
} from 'react-native-reanimated'
import {atoms as a, useTheme, flatten} from '#/alf'
import {atoms as a, flatten, useTheme} from '#/alf'
import {Props, useCommonSVGProps} from '#/components/icons/common'
import {Loader_Stroke2_Corner0_Rounded as Icon} from '#/components/icons/Loader'
+34
View File
@@ -0,0 +1,34 @@
import React from 'react'
import {View} from 'react-native'
import {atoms as a, flatten, useTheme} from '#/alf'
import {Props, useCommonSVGProps} from '#/components/icons/common'
import {Loader_Stroke2_Corner0_Rounded as Icon} from '#/components/icons/Loader'
export function Loader(props: Props) {
const t = useTheme()
const common = useCommonSVGProps(props)
return (
<View
style={[
a.relative,
a.justify_center,
a.align_center,
{width: common.size, height: common.size},
]}>
{/* css rotation animation - /bskyweb/templates/base.html */}
<div className="rotate-500ms">
<Icon
{...props}
style={[
a.absolute,
a.inset_0,
t.atoms.text_contrast_high,
flatten(props.style),
]}
/>
</div>
</View>
)
}
+7 -8
View File
@@ -3,11 +3,10 @@ import {View} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useTheme, atoms as a, useBreakpoints} from '#/alf'
import {Text} from '#/components/Typography'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonColor, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {Text} from '#/components/Typography'
export {useDialogControl as usePromptControl} from '#/components/Dialog'
@@ -52,7 +51,7 @@ export function Outer({
)
}
export function Title({children}: React.PropsWithChildren<{}>) {
export function TitleText({children}: React.PropsWithChildren<{}>) {
const {titleId} = React.useContext(Context)
return (
<Text nativeID={titleId} style={[a.text_2xl, a.font_bold, a.pb_sm]}>
@@ -61,7 +60,7 @@ export function Title({children}: React.PropsWithChildren<{}>) {
)
}
export function Description({children}: React.PropsWithChildren<{}>) {
export function DescriptionText({children}: React.PropsWithChildren<{}>) {
const t = useTheme()
const {descriptionId} = React.useContext(Context)
return (
@@ -80,7 +79,7 @@ export function Actions({children}: React.PropsWithChildren<{}>) {
<View
style={[
a.w_full,
a.gap_sm,
a.gap_md,
a.justify_end,
gtMobile
? [a.flex_row, a.flex_row_reverse, a.justify_start]
@@ -176,8 +175,8 @@ export function Basic({
}>) {
return (
<Outer control={control} testID="confirmModal">
<Title>{title}</Title>
<Description>{description}</Description>
<TitleText>{title}</TitleText>
<DescriptionText>{description}</DescriptionText>
<Actions>
<Action
cta={confirmButtonCta}
@@ -1,18 +1,16 @@
import React from 'react'
import {View} from 'react-native'
import {AppBskyLabelerDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {AppBskyLabelerDefs} from '@atproto/api'
export {useDialogControl as useReportDialogControl} from '#/components/Dialog'
import {getLabelingServiceTitle} from '#/lib/moderation'
import {atoms as a, useTheme, useBreakpoints} from '#/alf'
import {Text} from '#/components/Typography'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, useButtonContext} from '#/components/Button'
import {Divider} from '#/components/Divider'
import * as LabelingServiceCard from '#/components/LabelingServiceCard'
import {Text} from '#/components/Typography'
import {ReportDialogProps} from './types'
export function SelectLabelerView({
@@ -1,16 +1,15 @@
import React from 'react'
import {View} from 'react-native'
import {AppBskyLabelerDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {AppBskyLabelerDefs} from '@atproto/api'
import {useReportOptions, ReportOption} from '#/lib/moderation/useReportOptions'
import {DMCA_LINK} from '#/components/ReportDialog/const'
import {ReportOption, useReportOptions} from '#/lib/moderation/useReportOptions'
import {Link} from '#/components/Link'
import {DMCA_LINK} from '#/components/ReportDialog/const'
export {useDialogControl as useReportDialogControl} from '#/components/Dialog'
import {atoms as a, useTheme, useBreakpoints} from '#/alf'
import {Text} from '#/components/Typography'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {
Button,
ButtonIcon,
@@ -19,11 +18,11 @@ import {
} from '#/components/Button'
import {Divider} from '#/components/Divider'
import {
ChevronRight_Stroke2_Corner0_Rounded as ChevronRight,
ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft,
ChevronRight_Stroke2_Corner0_Rounded as ChevronRight,
} from '#/components/icons/Chevron'
import {SquareArrowTopRight_Stroke2_Corner0_Rounded as SquareArrowTopRight} from '#/components/icons/SquareArrowTopRight'
import {Text} from '#/components/Typography'
import {ReportDialogProps} from './types'
export function SelectReportOptionView({
+13 -12
View File
@@ -1,15 +1,15 @@
import React from 'react'
import {RichText as RichTextAPI, AppBskyRichtextFacet} from '@atproto/api'
import {useLingui} from '@lingui/react'
import {AppBskyRichtextFacet, RichText as RichTextAPI} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {atoms as a, TextStyleProp, flatten, useTheme, web, native} from '#/alf'
import {InlineLink} from '#/components/Link'
import {Text, TextProps} from '#/components/Typography'
import {toShortUrl} from 'lib/strings/url-helpers'
import {TagMenu, useTagMenuControl} from '#/components/TagMenu'
import {toShortUrl} from '#/lib/strings/url-helpers'
import {isNative} from '#/platform/detection'
import {atoms as a, flatten, native, TextStyleProp, useTheme, web} from '#/alf'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {InlineLinkText} from '#/components/Link'
import {TagMenu, useTagMenuControl} from '#/components/TagMenu'
import {Text, TextProps} from '#/components/Typography'
const WORD_WRAP = {wordWrap: 1}
@@ -84,7 +84,7 @@ export function RichText({
!disableLinks
) {
els.push(
<InlineLink
<InlineLinkText
selectable={selectable}
key={key}
to={`/profile/${mention.did}`}
@@ -92,22 +92,23 @@ export function RichText({
// @ts-ignore TODO
dataSet={WORD_WRAP}>
{segment.text}
</InlineLink>,
</InlineLinkText>,
)
} else if (link && AppBskyRichtextFacet.validateLink(link).success) {
if (disableLinks) {
els.push(toShortUrl(segment.text))
} else {
els.push(
<InlineLink
<InlineLinkText
selectable={selectable}
key={key}
to={link.uri}
style={[...styles, {pointerEvents: 'auto'}]}
// @ts-ignore TODO
dataSet={WORD_WRAP}>
dataSet={WORD_WRAP}
shareOnLongPress>
{toShortUrl(segment.text)}
</InlineLink>,
</InlineLinkText>,
)
}
} else if (
+6 -14
View File
@@ -1,14 +1,9 @@
import React from 'react'
import {
Text as RNText,
StyleProp,
TextStyle,
TextProps as RNTextProps,
} from 'react-native'
import {UITextView} from 'react-native-ui-text-view'
import {StyleProp, TextProps as RNTextProps, TextStyle} from 'react-native'
import {UITextView} from 'react-native-uitextview'
import {useTheme, atoms, web, flatten} from '#/alf'
import {isIOS, isNative} from '#/platform/detection'
import {isNative} from '#/platform/detection'
import {atoms, flatten, useTheme, web} from '#/alf'
export type TextProps = RNTextProps & {
/**
@@ -61,11 +56,8 @@ export function normalizeTextStyles(styles: StyleProp<TextStyle>) {
export function Text({style, selectable, ...rest}: TextProps) {
const t = useTheme()
const s = normalizeTextStyles([atoms.text_sm, t.atoms.text, flatten(style)])
return selectable && isIOS ? (
<UITextView style={s} {...rest} />
) : (
<RNText selectable={selectable} style={s} {...rest} />
)
return <UITextView selectable={selectable} uiTextView style={s} {...rest} />
}
export function createHeadingElement({level}: {level: number}) {
+10 -10
View File
@@ -1,23 +1,23 @@
import React from 'react'
import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import * as Dialog from '#/components/Dialog'
import {Text} from '../Typography'
import {DateInput} from '#/view/com/util/forms/DateInput'
import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {isIOS, isWeb} from '#/platform/detection'
import {
usePreferencesQuery,
usePreferencesSetBirthDateMutation,
UsePreferencesQueryResponse,
usePreferencesSetBirthDateMutation,
} from '#/state/queries/preferences'
import {Button, ButtonIcon, ButtonText} from '../Button'
import {atoms as a, useTheme} from '#/alf'
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
import {cleanError} from '#/lib/strings/errors'
import {isIOS, isWeb} from '#/platform/detection'
import {DateInput} from '#/view/com/util/forms/DateInput'
import {atoms as a, useTheme} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {Loader} from '#/components/Loader'
import {Button, ButtonIcon, ButtonText} from '../Button'
import {Text} from '../Typography'
export function BirthDateSettingsDialog({
control,
+20 -21
View File
@@ -1,37 +1,36 @@
import React from 'react'
import {Keyboard, View} from 'react-native'
import {AppBskyActorDefs, sanitizeMutedWordValue} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {AppBskyActorDefs, sanitizeMutedWordValue} from '@atproto/api'
import {
usePreferencesQuery,
useUpsertMutedWordsMutation,
useRemoveMutedWordMutation,
} from '#/state/queries/preferences'
import {logger} from '#/logger'
import {isNative} from '#/platform/detection'
import {
usePreferencesQuery,
useRemoveMutedWordMutation,
useUpsertMutedWordsMutation,
} from '#/state/queries/preferences'
import {
atoms as a,
useTheme,
native,
useBreakpoints,
useTheme,
ViewStyleProp,
web,
native,
} from '#/alf'
import {Text} from '#/components/Typography'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import * as Dialog from '#/components/Dialog'
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
import {Divider} from '#/components/Divider'
import * as Toggle from '#/components/forms/Toggle'
import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag'
import {PageText_Stroke2_Corner0_Rounded as PageText} from '#/components/icons/PageText'
import {Divider} from '#/components/Divider'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {Loader} from '#/components/Loader'
import {logger} from '#/logger'
import * as Dialog from '#/components/Dialog'
import * as Toggle from '#/components/forms/Toggle'
import * as Prompt from '#/components/Prompt'
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
import {Text} from '#/components/Typography'
export function MutedWordsDialog() {
const {mutedWordsDialogControl: control} = useGlobalDialogsControlContext()
@@ -130,9 +129,9 @@ function MutedWordsInner({}: {control: Dialog.DialogOuterProps['control']}) {
<TargetToggle>
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
<Toggle.Radio />
<Toggle.Label>
<Toggle.LabelText>
<Trans>Mute in text & tags</Trans>
</Toggle.Label>
</Toggle.LabelText>
</View>
<PageText size="sm" />
</TargetToggle>
@@ -145,9 +144,9 @@ function MutedWordsInner({}: {control: Dialog.DialogOuterProps['control']}) {
<TargetToggle>
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
<Toggle.Radio />
<Toggle.Label>
<Toggle.LabelText>
<Trans>Mute in tags only</Trans>
</Toggle.Label>
</Toggle.LabelText>
</View>
<Hashtag size="sm" />
</TargetToggle>
+61
View File
@@ -0,0 +1,61 @@
import React, {useCallback} from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
import {type SessionAccount, useSession} from '#/state/session'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useCloseAllActiveElements} from '#/state/util'
import {atoms as a} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {AccountList} from '../AccountList'
import {Text} from '../Typography'
export function SwitchAccountDialog({
control,
}: {
control: Dialog.DialogControlProps
}) {
const {_} = useLingui()
const {currentAccount} = useSession()
const {onPressSwitchAccount} = useAccountSwitcher()
const {setShowLoggedOut} = useLoggedOutViewControls()
const closeAllActiveElements = useCloseAllActiveElements()
const onSelectAccount = useCallback(
(account: SessionAccount) => {
if (account.did === currentAccount?.did) {
control.close()
} else {
onPressSwitchAccount(account, 'SwitchAccount')
}
},
[currentAccount, control, onPressSwitchAccount],
)
const onPressAddAccount = useCallback(() => {
setShowLoggedOut(true)
closeAllActiveElements()
}, [setShowLoggedOut, closeAllActiveElements])
return (
<Dialog.Outer control={control}>
<Dialog.Handle />
<Dialog.ScrollableInner label={_(msg`Switch Account`)}>
<View style={[a.gap_lg]}>
<Text style={[a.text_2xl, a.font_bold]}>
<Trans>Switch Account</Trans>
</Text>
<AccountList
onSelectAccount={onSelectAccount}
onSelectOther={onPressAddAccount}
otherLabel={_(msg`Add account`)}
/>
</View>
</Dialog.ScrollableInner>
</Dialog.Outer>
)
}
@@ -8,7 +8,7 @@ import * as TextField from '#/components/forms/TextField'
import {DateFieldButton} from './index.shared'
export * as utils from '#/components/forms/DateField/utils'
export const Label = TextField.Label
export const LabelText = TextField.LabelText
export function DateField({
value,
+1 -1
View File
@@ -13,7 +13,7 @@ import * as TextField from '#/components/forms/TextField'
import {DateFieldButton} from './index.shared'
export * as utils from '#/components/forms/DateField/utils'
export const Label = TextField.Label
export const LabelText = TextField.LabelText
/**
* Date-only input. Accepts a date in the format YYYY-MM-DD, and reports date
+1 -1
View File
@@ -9,7 +9,7 @@ import * as TextField from '#/components/forms/TextField'
import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays'
export * as utils from '#/components/forms/DateField/utils'
export const Label = TextField.Label
export const LabelText = TextField.LabelText
const InputBase = React.forwardRef<HTMLInputElement, TextInputProps>(
({style, ...props}, ref) => {
+2 -2
View File
@@ -225,7 +225,7 @@ export function createInput(Component: typeof TextInput) {
export const Input = createInput(TextInput)
export function Label({
export function LabelText({
nativeID,
children,
}: React.PropsWithChildren<{nativeID?: string}>) {
@@ -288,7 +288,7 @@ export function Icon({icon: Comp}: {icon: React.ComponentType<SVGIconProps>}) {
)
}
export function Suffix({
export function SuffixText({
children,
label,
accessibilityHint,
+5 -5
View File
@@ -3,16 +3,16 @@ import {Pressable, View, ViewStyle} from 'react-native'
import {HITSLOP_10} from 'lib/constants'
import {
useTheme,
atoms as a,
native,
flatten,
ViewStyleProp,
native,
TextStyleProp,
useTheme,
ViewStyleProp,
} from '#/alf'
import {Text} from '#/components/Typography'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {CheckThick_Stroke2_Corner0_Rounded as Checkmark} from '#/components/icons/Check'
import {Text} from '#/components/Typography'
export type ItemState = {
name: string
@@ -234,7 +234,7 @@ export function Item({
)
}
export function Label({
export function LabelText({
children,
style,
}: React.PropsWithChildren<TextStyleProp>) {
+9 -10
View File
@@ -1,22 +1,21 @@
import React from 'react'
import {View} from 'react-native'
import {InterpretedLabelValueDefinition, LabelPreference} from '@atproto/api'
import {useLingui} from '@lingui/react'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings'
import {useLabelBehaviorDescription} from '#/lib/moderation/useLabelBehaviorDescription'
import {getLabelStrings} from '#/lib/moderation/useLabelInfo'
import {
usePreferencesQuery,
usePreferencesSetContentLabelMutation,
} from '#/state/queries/preferences'
import {useLabelBehaviorDescription} from '#/lib/moderation/useLabelBehaviorDescription'
import {getLabelStrings} from '#/lib/moderation/useLabelInfo'
import {useTheme, atoms as a, useBreakpoints} from '#/alf'
import {Text} from '#/components/Typography'
import {InlineLink} from '#/components/Link'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '../icons/CircleInfo'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import * as ToggleButton from '#/components/forms/ToggleButton'
import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '../icons/CircleInfo'
export function Outer({children}: React.PropsWithChildren<{}>) {
return (
@@ -244,9 +243,9 @@ export function LabelerLabelPreference({
) : isGlobalLabel ? (
<Trans>
Configured in{' '}
<InlineLink to="/moderation" style={a.text_sm}>
<InlineLinkText to="/moderation" style={a.text_sm}>
moderation settings
</InlineLink>
</InlineLinkText>
.
</Trans>
) : null}
+10 -11
View File
@@ -1,20 +1,19 @@
import React from 'react'
import {View} from 'react-native'
import {ComAtprotoLabelDefs, ComAtprotoModerationDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {ComAtprotoLabelDefs, ComAtprotoModerationDefs} from '@atproto/api'
import {useLabelInfo} from '#/lib/moderation/useLabelInfo'
import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeHandle} from '#/lib/strings/handles'
import {getAgent} from '#/state/session'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
import * as Dialog from '#/components/Dialog'
import {Button, ButtonText} from '#/components/Button'
import {InlineLink} from '#/components/Link'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography'
import {Divider} from '../Divider'
export {useDialogControl as useLabelsOnMeDialogControl} from '#/components/Dialog'
@@ -145,13 +144,13 @@ function Label({
<View style={[a.px_md, a.py_sm, t.atoms.bg_contrast_25]}>
<Text style={[t.atoms.text_contrast_medium]}>
<Trans>Source:</Trans>{' '}
<InlineLink
<InlineLinkText
to={makeProfileLink(
labeler ? labeler.creator : {did: label.src, handle: ''},
)}
onPress={() => control.close()}>
{labeler ? sanitizeHandle(labeler.creator.handle, '@') : label.src}
</InlineLink>
</InlineLinkText>
</Text>
</View>
</View>
@@ -204,14 +203,14 @@ function AppealForm({
<Text style={[a.text_md, a.leading_snug]}>
<Trans>
This appeal will be sent to{' '}
<InlineLink
<InlineLinkText
to={makeProfileLink(
labeler ? labeler.creator : {did: label.src, handle: ''},
)}
onPress={() => control.close()}
style={[a.text_md, a.leading_snug]}>
{labeler ? sanitizeHandle(labeler.creator.handle, '@') : label.src}
</InlineLink>
</InlineLinkText>
.
</Trans>
</Text>
@@ -1,19 +1,18 @@
import React from 'react'
import {View} from 'react-native'
import {ModerationCause} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {ModerationCause} from '@atproto/api'
import {listUriToHref} from '#/lib/strings/url-helpers'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {makeProfileLink} from '#/lib/routes/links'
import {listUriToHref} from '#/lib/strings/url-helpers'
import {isNative} from '#/platform/detection'
import {useTheme, atoms as a} from '#/alf'
import {Text} from '#/components/Typography'
import {atoms as a, useTheme} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {InlineLink} from '#/components/Link'
import {Divider} from '#/components/Divider'
import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography'
export {useDialogControl as useModerationDetailsDialogControl} from '#/components/Dialog'
@@ -55,9 +54,9 @@ function ModerationDetailsDialogInner({
description = (
<Trans>
This user is included in the{' '}
<InlineLink to={listUriToHref(list.uri)} style={[a.text_sm]}>
<InlineLinkText to={listUriToHref(list.uri)} style={[a.text_sm]}>
{list.name}
</InlineLink>{' '}
</InlineLinkText>{' '}
list which you have blocked.
</Trans>
)
@@ -84,9 +83,9 @@ function ModerationDetailsDialogInner({
description = (
<Trans>
This user is included in the{' '}
<InlineLink to={listUriToHref(list.uri)} style={[a.text_sm]}>
<InlineLinkText to={listUriToHref(list.uri)} style={[a.text_sm]}>
{list.name}
</InlineLink>{' '}
</InlineLinkText>{' '}
list which you have muted.
</Trans>
)
@@ -127,12 +126,12 @@ function ModerationDetailsDialogInner({
{modcause.source.type === 'user' ? (
<Trans>the author</Trans>
) : (
<InlineLink
<InlineLinkText
to={makeProfileLink({did: modcause.label.src, handle: ''})}
onPress={() => control.close()}
style={a.text_md}>
{desc.source}
</InlineLink>
</InlineLinkText>
)}
.
</Trans>
+15 -8
View File
@@ -1,27 +1,26 @@
import React from 'react'
import {
TouchableWithoutFeedback,
StyleProp,
TouchableWithoutFeedback,
View,
ViewStyle,
} from 'react-native'
import {useNavigation} from '@react-navigation/native'
import {ModerationUI} from '@atproto/api'
import {Trans, msg} from '@lingui/macro'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {NavigationProp} from 'lib/routes/types'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {useTheme, atoms as a} from '#/alf'
import {CenteredView} from '#/view/com/util/Views'
import {Text} from '#/components/Typography'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {
ModerationDetailsDialog,
useModerationDetailsDialogControl,
} from '#/components/moderation/ModerationDetailsDialog'
import {Text} from '#/components/Typography'
export function ScreenHider({
testID,
@@ -125,7 +124,15 @@ export function ScreenHider({
accessibilityRole="button"
accessibilityLabel={_(msg`Learn more about this warning`)}
accessibilityHint="">
<Text style={[a.text_lg, {color: t.palette.primary_500}]}>
<Text
style={[
a.text_lg,
{
color: t.palette.primary_500,
// @ts-ignore web only -prf
cursor: 'pointer',
},
]}>
<Trans>Learn More</Trans>
</Text>
</TouchableWithoutFeedback>
+7 -3
View File
@@ -1,5 +1,9 @@
import VersionNumber from 'react-native-version-number'
import * as Updates from 'expo-updates'
export const updateChannel = Updates.channel
export const appVersion = `${VersionNumber.appVersion} (${VersionNumber.buildVersion})`
export const IS_DEV = process.env.EXPO_PUBLIC_ENV === 'development'
export const IS_TESTFLIGHT = process.env.EXPO_PUBLIC_ENV === 'testflight'
const UPDATES_CHANNEL = IS_TESTFLIGHT ? 'testflight' : 'production'
export const appVersion = `${VersionNumber.appVersion} (${
VersionNumber.buildVersion
}, ${IS_DEV ? 'development' : UPDATES_CHANNEL})`
+142
View File
@@ -0,0 +1,142 @@
import React from 'react'
import {Alert, AppState, AppStateStatus} from 'react-native'
import app from 'react-native-version-number'
import {
checkForUpdateAsync,
fetchUpdateAsync,
isEnabled,
reloadAsync,
setExtraParamAsync,
useUpdates,
} from 'expo-updates'
import {logger} from '#/logger'
import {IS_TESTFLIGHT} from 'lib/app-info'
import {isIOS} from 'platform/detection'
const MINIMUM_MINIMIZE_TIME = 15 * 60e3
async function setExtraParams() {
await setExtraParamAsync(
isIOS ? 'ios-build-number' : 'android-build-number',
// Hilariously, `buildVersion` is not actually a string on Android even though the TS type says it is.
// This just ensures it gets passed as a string
`${app.buildVersion}`,
)
await setExtraParamAsync(
'channel',
IS_TESTFLIGHT ? 'testflight' : 'production',
)
}
export function useOTAUpdates() {
const appState = React.useRef<AppStateStatus>('active')
const lastMinimize = React.useRef(0)
const ranInitialCheck = React.useRef(false)
const timeout = React.useRef<NodeJS.Timeout>()
const {isUpdatePending} = useUpdates()
const setCheckTimeout = React.useCallback(() => {
timeout.current = setTimeout(async () => {
try {
await setExtraParams()
logger.debug('Checking for update...')
const res = await checkForUpdateAsync()
if (res.isAvailable) {
logger.debug('Attempting to fetch update...')
await fetchUpdateAsync()
} else {
logger.debug('No update available.')
}
} catch (e) {
logger.warn('OTA Update Error', {error: `${e}`})
}
}, 10e3)
}, [])
const onIsTestFlight = React.useCallback(() => {
setTimeout(async () => {
try {
await setExtraParams()
const res = await checkForUpdateAsync()
if (res.isAvailable) {
await fetchUpdateAsync()
Alert.alert(
'Update Available',
'A new version of the app is available. Relaunch now?',
[
{
text: 'No',
style: 'cancel',
},
{
text: 'Relaunch',
style: 'default',
onPress: async () => {
await reloadAsync()
},
},
],
)
}
} catch (e: any) {
// No need to handle
}
}, 3e3)
}, [])
React.useEffect(() => {
// For Testflight users, we can prompt the user to update immediately whenever there's an available update. This
// is suspect however with the Apple App Store guidelines, so we don't want to prompt production users to update
// immediately.
if (IS_TESTFLIGHT) {
onIsTestFlight()
return
} else if (!isEnabled || __DEV__ || ranInitialCheck.current) {
// Development client shouldn't check for updates at all, so we skip that here.
return
}
setCheckTimeout()
ranInitialCheck.current = true
}, [onIsTestFlight, setCheckTimeout])
// After the app has been minimized for 30 minutes, we want to either A. install an update if one has become available
// or B check for an update again.
React.useEffect(() => {
if (!isEnabled) return
const subscription = AppState.addEventListener(
'change',
async nextAppState => {
if (
appState.current.match(/inactive|background/) &&
nextAppState === 'active'
) {
// If it's been 15 minutes since the last "minimize", we should feel comfortable updating the client since
// chances are that there isn't anything important going on in the current session.
if (lastMinimize.current <= Date.now() - MINIMUM_MINIMIZE_TIME) {
if (isUpdatePending) {
await reloadAsync()
} else {
setCheckTimeout()
}
}
} else {
lastMinimize.current = Date.now()
}
appState.current = nextAppState
},
)
return () => {
clearTimeout(timeout.current)
subscription.remove()
}
}, [isUpdatePending, setCheckTimeout])
}
+8 -8
View File
@@ -1,6 +1,6 @@
import React from 'react'
import {StyleProp, TextStyle, ViewStyle} from 'react-native'
import Svg, {Path, Rect, Line, Ellipse} from 'react-native-svg'
import Svg, {Ellipse, Line, Path, Rect} from 'react-native-svg'
export function GridIcon({
style,
@@ -141,8 +141,8 @@ export function MagnifyingGlassIcon2({
width={size || 24}
height={size || 24}
style={style}>
<Ellipse cx="12" cy="11" rx="9" ry="9" />
<Line x1="19" y1="17.3" x2="23.5" y2="21" strokeLinecap="round" />
<Ellipse cx="12" cy="10.5" rx="9" ry="9" />
<Line x1="18.5" y1="17" x2="22" y2="20.5" strokeLinecap="round" />
</Svg>
)
}
@@ -167,14 +167,14 @@ export function MagnifyingGlassIcon2Solid({
style={style}>
<Ellipse
cx="12"
cy="11"
rx="7"
ry="7"
cy="10.5"
rx="6.5"
ry="6.5"
stroke="none"
fill="currentColor"
/>
<Ellipse cx="12" cy="11" rx="9" ry="9" />
<Line x1="19" y1="17.3" x2="23.5" y2="21" strokeLinecap="round" />
<Ellipse cx="12" cy="10.5" rx="9" ry="9" />
<Line x1="18.5" y1="17" x2="22" y2="20.5" strokeLinecap="round" />
</Svg>
)
}
+2 -2
View File
@@ -1,6 +1,6 @@
import {useMemo} from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useMemo} from 'react'
export type GlobalLabelStrings = Record<
string,
@@ -31,7 +31,7 @@ export function useGlobalLabelStrings(): GlobalLabelStrings {
),
},
porn: {
name: _(msg`Pornography`),
name: _(msg`Adult Content`),
description: _(msg`Explicit sexual images.`),
},
sexual: {
+3 -3
View File
@@ -1,7 +1,7 @@
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useMemo} from 'react'
import {ComAtprotoModerationDefs} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
export interface ReportOption {
reason: string
@@ -68,7 +68,7 @@ export function useReportOptions(): ReportOptions {
{
reason: ComAtprotoModerationDefs.REASONSEXUAL,
title: _(msg`Unwanted Sexual Content`),
description: _(msg`Nudity or pornography not labeled as such`),
description: _(msg`Nudity or adult content not labeled as such`),
},
...common,
],
+61 -49
View File
@@ -1,12 +1,14 @@
import {useEffect} from 'react'
import * as Notifications from 'expo-notifications'
import {QueryClient} from '@tanstack/react-query'
import {resetToTab} from '../../Navigation'
import {devicePlatform, isIOS} from 'platform/detection'
import {track} from 'lib/analytics/analytics'
import {logger} from '#/logger'
import {RQKEY as RQKEY_NOTIFS} from '#/state/queries/notifications/feed'
import {truncateAndInvalidate} from '#/state/queries/util'
import {SessionAccount, getAgent} from '#/state/session'
import {getAgent, SessionAccount} from '#/state/session'
import {track} from 'lib/analytics/analytics'
import {devicePlatform, isIOS} from 'platform/detection'
import {resetToTab} from '../../Navigation'
import {logEvent} from '../statsig/statsig'
const SERVICE_DID = (serviceUrl?: string) =>
@@ -80,53 +82,63 @@ export function registerTokenChangeHandler(
}
}
export function init(queryClient: QueryClient) {
// handle notifications that are received, both in the foreground or background
// NOTE: currently just here for debug logging
Notifications.addNotificationReceivedListener(event => {
logger.debug(
'Notifications: received',
{event},
logger.DebugContext.notifications,
)
if (event.request.trigger.type === 'push') {
// handle payload-based deeplinks
let payload
if (isIOS) {
payload = event.request.trigger.payload
} else {
// TODO: handle android payload deeplink
}
if (payload) {
logger.debug(
'Notifications: received payload',
payload,
logger.DebugContext.notifications,
)
// TODO: deeplink notif here
}
}
})
// handle notifications that are tapped on
Notifications.addNotificationResponseReceivedListener(response => {
logger.debug(
'Notifications: response received',
{
actionIdentifier: response.actionIdentifier,
},
logger.DebugContext.notifications,
)
if (response.actionIdentifier === Notifications.DEFAULT_ACTION_IDENTIFIER) {
export function useNotificationsListener(queryClient: QueryClient) {
useEffect(() => {
// handle notifications that are received, both in the foreground or background
// NOTE: currently just here for debug logging
const sub1 = Notifications.addNotificationReceivedListener(event => {
logger.debug(
'User pressed a notification, opening notifications tab',
{},
'Notifications: received',
{event},
logger.DebugContext.notifications,
)
track('Notificatons:OpenApp')
logEvent('notifications:openApp', {})
truncateAndInvalidate(queryClient, RQKEY_NOTIFS())
resetToTab('NotificationsTab') // open notifications tab
if (event.request.trigger.type === 'push') {
// handle payload-based deeplinks
let payload
if (isIOS) {
payload = event.request.trigger.payload
} else {
// TODO: handle android payload deeplink
}
if (payload) {
logger.debug(
'Notifications: received payload',
payload,
logger.DebugContext.notifications,
)
// TODO: deeplink notif here
}
}
})
// handle notifications that are tapped on
const sub2 = Notifications.addNotificationResponseReceivedListener(
response => {
logger.debug(
'Notifications: response received',
{
actionIdentifier: response.actionIdentifier,
},
logger.DebugContext.notifications,
)
if (
response.actionIdentifier === Notifications.DEFAULT_ACTION_IDENTIFIER
) {
logger.debug(
'User pressed a notification, opening notifications tab',
{},
logger.DebugContext.notifications,
)
track('Notificatons:OpenApp')
logEvent('notifications:openApp', {})
truncateAndInvalidate(queryClient, RQKEY_NOTIFS())
resetToTab('NotificationsTab') // open notifications tab
}
},
)
return () => {
sub1.remove()
sub2.remove()
}
})
}, [queryClient])
}
-70
View File
@@ -1,70 +0,0 @@
import {AppState, AppStateStatus} from 'react-native'
import {QueryClient, focusManager} from '@tanstack/react-query'
import {createAsyncStoragePersister} from '@tanstack/query-async-storage-persister'
import AsyncStorage from '@react-native-async-storage/async-storage'
import {PersistQueryClientProviderProps} from '@tanstack/react-query-persist-client'
import {isNative} from '#/platform/detection'
// any query keys in this array will be persisted to AsyncStorage
const STORED_CACHE_QUERY_KEYS = ['labelers-detailed-info']
focusManager.setEventListener(onFocus => {
if (isNative) {
const subscription = AppState.addEventListener(
'change',
(status: AppStateStatus) => {
focusManager.setFocused(status === 'active')
},
)
return () => subscription.remove()
} else if (typeof window !== 'undefined' && window.addEventListener) {
// these handlers are a bit redundant but focus catches when the browser window
// is blurred/focused while visibilitychange seems to only handle when the
// window minimizes (both of them catch tab changes)
// there's no harm to redundant fires because refetchOnWindowFocus is only
// used with queries that employ stale data times
const handler = () => onFocus()
window.addEventListener('focus', handler, false)
window.addEventListener('visibilitychange', handler, false)
return () => {
window.removeEventListener('visibilitychange', handler)
window.removeEventListener('focus', handler)
}
}
})
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
// NOTE
// refetchOnWindowFocus breaks some UIs (like feeds)
// so we only selectively want to enable this
// -prf
refetchOnWindowFocus: false,
// Structural sharing between responses makes it impossible to rely on
// "first seen" timestamps on objects to determine if they're fresh.
// Disable this optimization so that we can rely on "first seen" timestamps.
structuralSharing: false,
// We don't want to retry queries by default, because in most cases we
// want to fail early and show a response to the user. There are
// exceptions, and those can be made on a per-query basis. For others, we
// should give users controls to retry.
retry: false,
},
},
})
export const asyncStoragePersister = createAsyncStoragePersister({
storage: AsyncStorage,
key: 'queryCache',
})
export const dehydrateOptions: PersistQueryClientProviderProps['persistOptions']['dehydrateOptions'] =
{
shouldDehydrateMutation: (_: any) => false,
shouldDehydrateQuery: query => {
return STORED_CACHE_QUERY_KEYS.includes(String(query.queryKey[0]))
},
}
+124
View File
@@ -0,0 +1,124 @@
import React, {useRef, useState} from 'react'
import {AppState, AppStateStatus} from 'react-native'
import AsyncStorage from '@react-native-async-storage/async-storage'
import {createAsyncStoragePersister} from '@tanstack/query-async-storage-persister'
import {focusManager, QueryClient} from '@tanstack/react-query'
import {
PersistQueryClientProvider,
PersistQueryClientProviderProps,
} from '@tanstack/react-query-persist-client'
import {isNative} from '#/platform/detection'
// any query keys in this array will be persisted to AsyncStorage
export const labelersDetailedInfoQueryKeyRoot = 'labelers-detailed-info'
const STORED_CACHE_QUERY_KEY_ROOTS = [labelersDetailedInfoQueryKeyRoot]
focusManager.setEventListener(onFocus => {
if (isNative) {
const subscription = AppState.addEventListener(
'change',
(status: AppStateStatus) => {
focusManager.setFocused(status === 'active')
},
)
return () => subscription.remove()
} else if (typeof window !== 'undefined' && window.addEventListener) {
// these handlers are a bit redundant but focus catches when the browser window
// is blurred/focused while visibilitychange seems to only handle when the
// window minimizes (both of them catch tab changes)
// there's no harm to redundant fires because refetchOnWindowFocus is only
// used with queries that employ stale data times
const handler = () => onFocus()
window.addEventListener('focus', handler, false)
window.addEventListener('visibilitychange', handler, false)
return () => {
window.removeEventListener('visibilitychange', handler)
window.removeEventListener('focus', handler)
}
}
})
const createQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: {
// NOTE
// refetchOnWindowFocus breaks some UIs (like feeds)
// so we only selectively want to enable this
// -prf
refetchOnWindowFocus: false,
// Structural sharing between responses makes it impossible to rely on
// "first seen" timestamps on objects to determine if they're fresh.
// Disable this optimization so that we can rely on "first seen" timestamps.
structuralSharing: false,
// We don't want to retry queries by default, because in most cases we
// want to fail early and show a response to the user. There are
// exceptions, and those can be made on a per-query basis. For others, we
// should give users controls to retry.
retry: false,
},
},
})
const dehydrateOptions: PersistQueryClientProviderProps['persistOptions']['dehydrateOptions'] =
{
shouldDehydrateMutation: (_: any) => false,
shouldDehydrateQuery: query => {
return STORED_CACHE_QUERY_KEY_ROOTS.includes(String(query.queryKey[0]))
},
}
export function QueryProvider({
children,
currentDid,
}: {
children: React.ReactNode
currentDid: string | undefined
}) {
return (
<QueryProviderInner
// Enforce we never reuse cache between users.
// These two props MUST stay in sync.
key={currentDid}
currentDid={currentDid}>
{children}
</QueryProviderInner>
)
}
function QueryProviderInner({
children,
currentDid,
}: {
children: React.ReactNode
currentDid: string | undefined
}) {
const initialDid = useRef(currentDid)
if (currentDid !== initialDid.current) {
throw Error(
'Something is very wrong. Expected did to be stable due to key above.',
)
}
// We create the query client here so that it's scoped to a specific DID.
// Do not move the query client creation outside of this component.
const [queryClient, _setQueryClient] = useState(() => createQueryClient())
const [persistOptions, _setPersistOptions] = useState(() => {
const asyncPersister = createAsyncStoragePersister({
storage: AsyncStorage,
key: 'queryClient-' + (currentDid ?? 'logged-out'),
})
return {
persister: asyncPersister,
dehydrateOptions,
}
})
return (
<PersistQueryClientProvider
client={queryClient}
persistOptions={persistOptions}>
{children}
</PersistQueryClientProvider>
)
}
+4 -3
View File
@@ -1,8 +1,9 @@
import {isIOS, isAndroid} from 'platform/detection'
import {Share} from 'react-native'
// import * as Sharing from 'expo-sharing'
import Clipboard from '@react-native-clipboard/clipboard'
import * as Toast from '../view/com/util/Toast'
import {Share} from 'react-native'
import {isAndroid, isIOS} from 'platform/detection'
import * as Toast from '#/view/com/util/Toast'
/**
* This function shares a URL using the native Share API if available, or copies it to the clipboard
+13 -6
View File
@@ -1,7 +1,8 @@
import {AppBskyFeedDefs, AppBskyFeedPost} from '@atproto/api'
import lande from 'lande'
import {hasProp} from 'lib/type-guards'
import * as bcp47Match from 'bcp-47-match'
import lande from 'lande'
import {hasProp} from 'lib/type-guards'
import {
AppLanguage,
LANGUAGES_MAP_CODE2,
@@ -118,6 +119,8 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
switch (lang) {
case 'en':
return AppLanguage.en
case 'ca':
return AppLanguage.ca
case 'de':
return AppLanguage.de
case 'es':
@@ -126,24 +129,28 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
return AppLanguage.fi
case 'fr':
return AppLanguage.fr
case 'ga':
return AppLanguage.ga
case 'hi':
return AppLanguage.hi
case 'id':
return AppLanguage.id
case 'it':
return AppLanguage.it
case 'ja':
return AppLanguage.ja
case 'ko':
return AppLanguage.ko
case 'pt-BR':
return AppLanguage.pt_BR
case 'tr':
return AppLanguage.tr
case 'uk':
return AppLanguage.uk
case 'ca':
return AppLanguage.ca
case 'zh-CN':
return AppLanguage.zh_CN
case 'it':
return AppLanguage.it
case 'zh-TW':
return AppLanguage.zh_TW
default:
continue
}
+28 -14
View File
@@ -1,30 +1,36 @@
import {useEffect} from 'react'
import {i18n} from '@lingui/core'
import {useLanguagePrefs} from '#/state/preferences'
import {messages as messagesEn} from '#/locale/locales/en/messages'
import {sanitizeAppLanguageSetting} from '#/locale/helpers'
import {AppLanguage} from '#/locale/languages'
import {messages as messagesCa} from '#/locale/locales/ca/messages'
import {messages as messagesDe} from '#/locale/locales/de/messages'
import {messages as messagesId} from '#/locale/locales/id/messages'
import {messages as messagesEn} from '#/locale/locales/en/messages'
import {messages as messagesEs} from '#/locale/locales/es/messages'
import {messages as messagesFi} from '#/locale/locales/fi/messages'
import {messages as messagesFr} from '#/locale/locales/fr/messages'
import {messages as messagesGa} from '#/locale/locales/ga/messages'
import {messages as messagesHi} from '#/locale/locales/hi/messages'
import {messages as messagesId} from '#/locale/locales/id/messages'
import {messages as messagesIt} from '#/locale/locales/it/messages'
import {messages as messagesJa} from '#/locale/locales/ja/messages'
import {messages as messagesKo} from '#/locale/locales/ko/messages'
import {messages as messagesPt_BR} from '#/locale/locales/pt-BR/messages'
import {messages as messagesTr} from '#/locale/locales/tr/messages'
import {messages as messagesUk} from '#/locale/locales/uk/messages'
import {messages as messagesCa} from '#/locale/locales/ca/messages'
import {messages as messagesZh_CN} from '#/locale/locales/zh-CN/messages'
import {messages as messagesIt} from '#/locale/locales/it/messages'
import {sanitizeAppLanguageSetting} from '#/locale/helpers'
import {AppLanguage} from '#/locale/languages'
import {messages as messagesZh_TW} from '#/locale/locales/zh-TW/messages'
import {useLanguagePrefs} from '#/state/preferences'
/**
* We do a dynamic import of just the catalog that we need
*/
export async function dynamicActivate(locale: AppLanguage) {
switch (locale) {
case AppLanguage.ca: {
i18n.loadAndActivate({locale, messages: messagesCa})
break
}
case AppLanguage.de: {
i18n.loadAndActivate({locale, messages: messagesDe})
break
@@ -41,6 +47,10 @@ export async function dynamicActivate(locale: AppLanguage) {
i18n.loadAndActivate({locale, messages: messagesFr})
break
}
case AppLanguage.ga: {
i18n.loadAndActivate({locale, messages: messagesGa})
break
}
case AppLanguage.hi: {
i18n.loadAndActivate({locale, messages: messagesHi})
break
@@ -49,6 +59,10 @@ export async function dynamicActivate(locale: AppLanguage) {
i18n.loadAndActivate({locale, messages: messagesId})
break
}
case AppLanguage.it: {
i18n.loadAndActivate({locale, messages: messagesIt})
break
}
case AppLanguage.ja: {
i18n.loadAndActivate({locale, messages: messagesJa})
break
@@ -61,20 +75,20 @@ export async function dynamicActivate(locale: AppLanguage) {
i18n.loadAndActivate({locale, messages: messagesPt_BR})
break
}
case AppLanguage.uk: {
i18n.loadAndActivate({locale, messages: messagesUk})
case AppLanguage.tr: {
i18n.loadAndActivate({locale, messages: messagesTr})
break
}
case AppLanguage.ca: {
i18n.loadAndActivate({locale, messages: messagesCa})
case AppLanguage.uk: {
i18n.loadAndActivate({locale, messages: messagesUk})
break
}
case AppLanguage.zh_CN: {
i18n.loadAndActivate({locale, messages: messagesZh_CN})
break
}
case AppLanguage.it: {
i18n.loadAndActivate({locale, messages: messagesIt})
case AppLanguage.zh_TW: {
i18n.loadAndActivate({locale, messages: messagesZh_TW})
break
}
default: {
+19 -7
View File
@@ -1,9 +1,9 @@
import {useEffect} from 'react'
import {i18n} from '@lingui/core'
import {useLanguagePrefs} from '#/state/preferences'
import {sanitizeAppLanguageSetting} from '#/locale/helpers'
import {AppLanguage} from '#/locale/languages'
import {useLanguagePrefs} from '#/state/preferences'
/**
* We do a dynamic import of just the catalog that we need
@@ -12,6 +12,10 @@ export async function dynamicActivate(locale: AppLanguage) {
let mod: any
switch (locale) {
case AppLanguage.ca: {
mod = await import(`./locales/ca/messages`)
break
}
case AppLanguage.de: {
mod = await import(`./locales/de/messages`)
break
@@ -28,6 +32,10 @@ export async function dynamicActivate(locale: AppLanguage) {
mod = await import(`./locales/fr/messages`)
break
}
case AppLanguage.ga: {
mod = await import(`./locales/ga/messages`)
break
}
case AppLanguage.hi: {
mod = await import(`./locales/hi/messages`)
break
@@ -36,6 +44,10 @@ export async function dynamicActivate(locale: AppLanguage) {
mod = await import(`./locales/id/messages`)
break
}
case AppLanguage.it: {
mod = await import(`./locales/it/messages`)
break
}
case AppLanguage.ja: {
mod = await import(`./locales/ja/messages`)
break
@@ -48,20 +60,20 @@ export async function dynamicActivate(locale: AppLanguage) {
mod = await import(`./locales/pt-BR/messages`)
break
}
case AppLanguage.uk: {
mod = await import(`./locales/uk/messages`)
case AppLanguage.tr: {
mod = await import(`./locales/tr/messages`)
break
}
case AppLanguage.ca: {
mod = await import(`./locales/ca/messages`)
case AppLanguage.uk: {
mod = await import(`./locales/uk/messages`)
break
}
case AppLanguage.zh_CN: {
mod = await import(`./locales/zh-CN/messages`)
break
}
case AppLanguage.it: {
mod = await import(`./locales/it/messages`)
case AppLanguage.zh_TW: {
mod = await import(`./locales/zh-TW/messages`)
break
}
default: {
+11 -5
View File
@@ -6,19 +6,22 @@ interface Language {
export enum AppLanguage {
en = 'en',
ca = 'ca',
de = 'de',
es = 'es',
fi = 'fi',
fr = 'fr',
ga = 'ga',
hi = 'hi',
id = 'id',
it = 'it',
ja = 'ja',
ko = 'ko',
pt_BR = 'pt-BR',
tr = 'tr',
uk = 'uk',
ca = 'ca',
zh_CN = 'zh-CN',
it = 'it',
zh_TW = 'zh-TW',
}
interface AppLanguageConfig {
@@ -28,19 +31,22 @@ interface AppLanguageConfig {
export const APP_LANGUAGES: AppLanguageConfig[] = [
{code2: AppLanguage.en, name: 'English'},
{code2: AppLanguage.ca, name: 'Català – Catalan'},
{code2: AppLanguage.de, name: 'Deutsch – German'},
{code2: AppLanguage.es, name: 'Español – Spanish'},
{code2: AppLanguage.fi, name: 'Suomi – Finnish'},
{code2: AppLanguage.fr, name: 'Français – French'},
{code2: AppLanguage.ga, name: 'Gaeilge – Irish'},
{code2: AppLanguage.hi, name: 'हिंदी – Hindi'},
{code2: AppLanguage.id, name: 'Bahasa Indonesia – Indonesian'},
{code2: AppLanguage.it, name: 'Italiano – Italian'},
{code2: AppLanguage.ja, name: '日本語 – Japanese'},
{code2: AppLanguage.ko, name: '한국어 – Korean'},
{code2: AppLanguage.pt_BR, name: 'Português (BR) – Portuguese (BR)'},
{code2: AppLanguage.tr, name: 'Türkçe – Turkish'},
{code2: AppLanguage.uk, name: 'Українська – Ukrainian'},
{code2: AppLanguage.ca, name: 'Català – Catalan'},
{code2: AppLanguage.zh_CN, name: '简体中文(中国) – Chinese (Simplified)'},
{code2: AppLanguage.it, name: 'Italiano - Italian'},
{code2: AppLanguage.zh_CN, name: '简体中文(中国)– Chinese (Simplified)'},
{code2: AppLanguage.zh_TW, name: '繁體中文(臺灣)– Chinese (Traditional)'},
]
export const LANGUAGES: Language[] = [
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+29
View File
@@ -6,3 +6,32 @@ findLast.shim()
// @ts-ignore whatever typescript wants to complain about here, I dont care about -prf
window.setImmediate = (cb: () => void) => setTimeout(cb, 0)
if (process.env.NODE_ENV !== 'production') {
// In development, react-native-web's <View> tries to validate that
// text is wrapped into <Text>. It doesn't catch all cases but is useful.
// Unfortunately, it only does that via console.error so it's easy to miss.
// This is a hack to get it showing as a redbox on the web so we catch it early.
const realConsoleError = console.error
const thrownErrors = new WeakSet()
console.error = function consoleErrorWrapper(msgOrError) {
if (
typeof msgOrError === 'string' &&
msgOrError.startsWith('Unexpected text node')
) {
if (
msgOrError ===
'Unexpected text node: . A text node cannot be a child of a <View>.'
) {
// This is due to a stray empty string.
// React already handles this fine, so RNW warning is a false positive. Ignore.
return
}
const err = new Error(msgOrError)
thrownErrors.add(err)
throw err
} else if (!thrownErrors.has(msgOrError)) {
return realConsoleError.apply(this, arguments as any)
}
}
}
+33 -33
View File
@@ -1,28 +1,30 @@
import React from 'react'
import {ListRenderItemInfo, Pressable} from 'react-native'
import {useFocusEffect} from '@react-navigation/native'
import {useSetMinimalShellMode} from 'state/shell'
import {ViewHeader} from 'view/com/util/ViewHeader'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {CommonNavigatorParams} from 'lib/routes/types'
import {useSearchPostsQuery} from 'state/queries/search-posts'
import {Post} from 'view/com/post/Post'
import {PostView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {HITSLOP_10} from 'lib/constants'
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
import {CommonNavigatorParams} from 'lib/routes/types'
import {shareUrl} from 'lib/sharing'
import {cleanError} from 'lib/strings/errors'
import {sanitizeHandle} from 'lib/strings/handles'
import {enforceLen} from 'lib/strings/helpers'
import {isNative} from 'platform/detection'
import {useSearchPostsQuery} from 'state/queries/search-posts'
import {useSetMinimalShellMode} from 'state/shell'
import {Post} from 'view/com/post/Post'
import {List} from 'view/com/util/List'
import {ViewHeader} from 'view/com/util/ViewHeader'
import {ArrowOutOfBox_Stroke2_Corner0_Rounded} from '#/components/icons/ArrowOutOfBox'
import {
ListFooter,
ListHeaderDesktop,
ListMaybePlaceholder,
} from '#/components/Lists'
import {List} from 'view/com/util/List'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {sanitizeHandle} from 'lib/strings/handles'
import {ArrowOutOfBox_Stroke2_Corner0_Rounded} from '#/components/icons/ArrowOutOfBox'
import {shareUrl} from 'lib/sharing'
import {HITSLOP_10} from 'lib/constants'
import {isNative} from 'platform/detection'
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
const renderItem = ({item}: ListRenderItemInfo<PostView>) => {
return <Post post={item} />
@@ -61,9 +63,8 @@ export default function HashtagScreen({
const {
data,
isFetching,
isFetchingNextPage,
isLoading,
isRefetching,
isError,
error,
refetch,
@@ -97,9 +98,9 @@ export default function HashtagScreen({
}, [refetch])
const onEndReached = React.useCallback(() => {
if (isFetching || !hasNextPage || error) return
if (isFetchingNextPage || !hasNextPage || error) return
fetchNextPage()
}, [isFetching, hasNextPage, error, fetchNextPage])
}, [isFetchingNextPage, hasNextPage, error, fetchNextPage])
return (
<>
@@ -123,16 +124,16 @@ export default function HashtagScreen({
: undefined
}
/>
<ListMaybePlaceholder
isLoading={isLoading || isRefetching}
isError={isError}
isEmpty={posts.length < 1}
onRetry={refetch}
emptyTitle="results"
emptyMessage={_(msg`We couldn't find any results for that hashtag.`)}
/>
{!isLoading && posts.length > 0 && (
<List<PostView>
{posts.length < 1 ? (
<ListMaybePlaceholder
isLoading={isLoading}
isError={isError}
onRetry={refetch}
emptyType="results"
emptyMessage={_(msg`We couldn't find any results for that hashtag.`)}
/>
) : (
<List
data={posts}
renderItem={renderItem}
keyExtractor={keyExtractor}
@@ -150,9 +151,8 @@ export default function HashtagScreen({
}
ListFooterComponent={
<ListFooter
isFetching={isFetching && !isRefetching}
isError={isError}
error={error?.name}
isFetchingNextPage={isFetchingNextPage}
error={cleanError(error)}
onRetry={fetchNextPage}
/>
}
+10 -114
View File
@@ -5,76 +5,15 @@ import {useLingui} from '@lingui/react'
import {useAnalytics} from '#/lib/analytics/analytics'
import {logEvent} from '#/lib/statsig/statsig'
import {colors} from '#/lib/styles'
import {useProfileQuery} from '#/state/queries/profile'
import {SessionAccount, useSession, useSessionApi} from '#/state/session'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import * as Toast from '#/view/com/util/Toast'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme} from '#/alf'
import {atoms as a} from '#/alf'
import {AccountList} from '#/components/AccountList'
import {Button} from '#/components/Button'
import * as TextField from '#/components/forms/TextField'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {ChevronRight_Stroke2_Corner0_Rounded as Chevron} from '#/components/icons/Chevron'
import {Text} from '#/components/Typography'
import {FormContainer} from './FormContainer'
function AccountItem({
account,
onSelect,
isCurrentAccount,
}: {
account: SessionAccount
onSelect: (account: SessionAccount) => void
isCurrentAccount: boolean
}) {
const t = useTheme()
const {_} = useLingui()
const {data: profile} = useProfileQuery({did: account.did})
const onPress = React.useCallback(() => {
onSelect(account)
}, [account, onSelect])
return (
<Button
testID={`chooseAccountBtn-${account.handle}`}
key={account.did}
style={[a.flex_1]}
onPress={onPress}
label={
isCurrentAccount
? _(msg`Continue as ${account.handle} (currently signed in)`)
: _(msg`Sign in as ${account.handle}`)
}>
{({hovered, pressed}) => (
<View
style={[
a.flex_1,
a.flex_row,
a.align_center,
{height: 48},
(hovered || pressed) && t.atoms.bg_contrast_25,
]}>
<View style={a.p_md}>
<UserAvatar avatar={profile?.avatar} size={24} />
</View>
<Text style={[a.align_baseline, a.flex_1, a.flex_row, a.py_sm]}>
<Text style={[a.font_bold]}>
{profile?.displayName || account.handle}{' '}
</Text>
<Text style={[t.atoms.text_contrast_medium]}>{account.handle}</Text>
</Text>
{isCurrentAccount ? (
<Check size="sm" style={[{color: colors.green3}, a.mr_md]} />
) : (
<Chevron size="sm" style={[t.atoms.text, a.mr_md]} />
)}
</View>
)}
</Button>
)
}
export const ChooseAccountForm = ({
onSelectAccount,
onPressBack,
@@ -84,8 +23,7 @@ export const ChooseAccountForm = ({
}) => {
const {track, screen} = useAnalytics()
const {_} = useLingui()
const t = useTheme()
const {accounts, currentAccount} = useSession()
const {currentAccount} = useSession()
const {initSession} = useSessionApi()
const {setShowLoggedOut} = useLoggedOutViewControls()
@@ -120,57 +58,15 @@ export const ChooseAccountForm = ({
return (
<FormContainer
testID="chooseAccountForm"
title={<Trans>Select account</Trans>}>
titleText={<Trans>Select account</Trans>}>
<View>
<TextField.Label>
<TextField.LabelText>
<Trans>Sign in as...</Trans>
</TextField.Label>
<View
style={[
a.rounded_md,
a.overflow_hidden,
a.border,
t.atoms.border_contrast_low,
]}>
{accounts.map(account => (
<React.Fragment key={account.did}>
<AccountItem
account={account}
onSelect={onSelect}
isCurrentAccount={account.did === currentAccount?.did}
/>
<View style={[a.border_b, t.atoms.border_contrast_low]} />
</React.Fragment>
))}
<Button
testID="chooseNewAccountBtn"
style={[a.flex_1]}
onPress={() => onSelectAccount(undefined)}
label={_(msg`Login to account that is not listed`)}>
{({hovered, pressed}) => (
<View
style={[
a.flex_1,
a.flex_row,
a.align_center,
{height: 48},
(hovered || pressed) && t.atoms.bg_contrast_25,
]}>
<Text
style={[
a.align_baseline,
a.flex_1,
a.flex_row,
a.py_sm,
{paddingLeft: 48},
]}>
<Trans>Other account</Trans>
</Text>
<Chevron size="sm" style={[t.atoms.text, a.mr_md]} />
</View>
)}
</Button>
</View>
</TextField.LabelText>
<AccountList
onSelectAccount={onSelect}
onSelectOther={() => onSelectAccount()}
/>
</View>
<View style={[a.flex_row]}>
<Button
+4 -4
View File
@@ -6,12 +6,12 @@ import {Text} from '#/components/Typography'
export function FormContainer({
testID,
title,
titleText,
children,
style,
}: {
testID?: string
title?: React.ReactNode
titleText?: React.ReactNode
children: React.ReactNode
style?: StyleProp<ViewStyle>
}) {
@@ -21,9 +21,9 @@ export function FormContainer({
<View
testID={testID}
style={[a.gap_md, a.flex_1, !gtMobile && [a.px_lg, a.py_md], style]}>
{title && !gtMobile && (
{titleText && !gtMobile && (
<Text style={[a.text_xl, a.font_bold, t.atoms.text_contrast_high]}>
{title}
{titleText}
</Text>
)}
{children}
+35 -37
View File
@@ -1,51 +1,49 @@
import React from 'react'
import {View} from 'react-native'
import {useFocusEffect} from '@react-navigation/native'
import {ComAtprotoLabelDefs} from '@atproto/api'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {LABELS} from '@atproto/api'
import {useSafeAreaFrame} from 'react-native-safe-area-context'
import {ComAtprotoLabelDefs} from '@atproto/api'
import {LABELS} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {NativeStackScreenProps, CommonNavigatorParams} from '#/lib/routes/types'
import {CenteredView} from '#/view/com/util/Views'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {useAnalytics} from 'lib/analytics/analytics'
import {useSetMinimalShellMode} from '#/state/shell'
import {useSession} from '#/state/session'
import {getLabelingServiceTitle} from '#/lib/moderation'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {logger} from '#/logger'
import {
useMyLabelersQuery,
usePreferencesQuery,
UsePreferencesQueryResponse,
usePreferencesSetAdultContentMutation,
} from '#/state/queries/preferences'
import {
useProfileQuery,
useProfileUpdateMutation,
} from '#/state/queries/profile'
import {useSession} from '#/state/session'
import {useSetMinimalShellMode} from '#/state/shell'
import {useAnalytics} from 'lib/analytics/analytics'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {CenteredView} from '#/view/com/util/Views'
import {ScrollView} from '#/view/com/util/Views'
import {
UsePreferencesQueryResponse,
useMyLabelersQuery,
usePreferencesQuery,
usePreferencesSetAdultContentMutation,
} from '#/state/queries/preferences'
import {getLabelingServiceTitle} from '#/lib/moderation'
import {logger} from '#/logger'
import {useTheme, atoms as a, useBreakpoints, ViewStyleProp} from '#/alf'
import {atoms as a, useBreakpoints, useTheme, ViewStyleProp} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings'
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
import {Divider} from '#/components/Divider'
import * as Toggle from '#/components/forms/Toggle'
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign'
import {Props as SVGIconProps} from '#/components/icons/common'
import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter'
import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person'
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter'
import {Text} from '#/components/Typography'
import * as Toggle from '#/components/forms/Toggle'
import {InlineLink, Link} from '#/components/Link'
import {Button, ButtonText} from '#/components/Button'
import {Loader} from '#/components/Loader'
import * as LabelingService from '#/components/LabelingServiceCard'
import {InlineLinkText, Link} from '#/components/Link'
import {Loader} from '#/components/Loader'
import {GlobalLabelPreference} from '#/components/moderation/LabelPreference'
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
import {Props as SVGIconProps} from '#/components/icons/common'
import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings'
import * as Dialog from '#/components/Dialog'
import {Text} from '#/components/Typography'
function ErrorState({error}: {error: string}) {
const t = useTheme()
@@ -520,11 +518,11 @@ function PwiOptOut() {
msg`Discourage apps from showing my account to logged-out users`,
)}>
<Toggle.Switch />
<Toggle.Label style={[a.text_md, a.flex_1]}>
<Toggle.LabelText style={[a.text_md, a.flex_1]}>
<Trans>
Discourage apps from showing my account to logged-out users
</Trans>
</Toggle.Label>
</Toggle.LabelText>
</Toggle.Item>
{updateProfile.isPending && <Loader />}
@@ -547,9 +545,9 @@ function PwiOptOut() {
</Trans>
</Text>
<InlineLink to="https://blueskyweb.zendesk.com/hc/en-us/articles/15835264007693-Data-Privacy">
<InlineLinkText to="https://blueskyweb.zendesk.com/hc/en-us/articles/15835264007693-Data-Privacy">
<Trans>Learn more about what is public on Bluesky.</Trans>
</InlineLink>
</InlineLinkText>
</View>
</View>
)
+17 -19
View File
@@ -1,29 +1,27 @@
import React from 'react'
import {View} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {IS_DEV} from '#/env'
import {isWeb} from '#/platform/detection'
import {useOnboardingDispatch} from '#/state/shell'
import {
useTheme,
atoms as a,
useBreakpoints,
web,
native,
flatten,
TextStyleProp,
} from '#/alf'
import {P, leading, Text} from '#/components/Typography'
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
import {Button, ButtonIcon} from '#/components/Button'
import {ScrollView} from '#/view/com/util/Views'
import {createPortalGroup} from '#/components/Portal'
import {Context} from '#/screens/Onboarding/state'
import {
atoms as a,
flatten,
native,
TextStyleProp,
useBreakpoints,
useTheme,
web,
} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
import {createPortalGroup} from '#/components/Portal'
import {leading, P, Text} from '#/components/Typography'
import {IS_DEV} from '#/env'
const COL_WIDTH = 500
@@ -204,7 +202,7 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
)
}
export function Title({
export function TitleText({
children,
style,
}: React.PropsWithChildren<TextStyleProp>) {
@@ -224,7 +222,7 @@ export function Title({
)
}
export function Description({
export function DescriptionText({
children,
style,
}: React.PropsWithChildren<TextStyleProp>) {
@@ -6,9 +6,9 @@ import {useLingui} from '@lingui/react'
import {useAnalytics} from '#/lib/analytics/analytics'
import {logEvent} from '#/lib/statsig/statsig'
import {
Description,
DescriptionText,
OnboardingControls,
Title,
TitleText,
} from '#/screens/Onboarding/Layout'
import {Context} from '#/screens/Onboarding/state'
import {FeedCard} from '#/screens/Onboarding/StepAlgoFeeds/FeedCard'
@@ -105,15 +105,15 @@ export function StepAlgoFeeds() {
<View style={[a.align_start]}>
<IconCircle icon={ListSparkle} style={[a.mb_2xl]} />
<Title>
<TitleText>
<Trans>Choose your main feeds</Trans>
</Title>
<Description>
</TitleText>
<DescriptionText>
<Trans>
Custom feeds built by the community bring you new experiences and help
you find the content you love.
</Trans>
</Description>
</DescriptionText>
<View style={[a.w_full, a.pb_2xl]}>
<Toggle.Group
+6 -6
View File
@@ -10,9 +10,9 @@ import {useSetSaveFeedsMutation} from '#/state/queries/preferences'
import {getAgent} from '#/state/session'
import {useOnboardingDispatch} from '#/state/shell'
import {
Description,
DescriptionText,
OnboardingControls,
Title,
TitleText,
} from '#/screens/Onboarding/Layout'
import {Context} from '#/screens/Onboarding/state'
import {
@@ -87,12 +87,12 @@ export function StepFinished() {
<View style={[a.align_start]}>
<IconCircle icon={Check} style={[a.mb_2xl]} />
<Title>
<TitleText>
<Trans>You're ready to go!</Trans>
</Title>
<Description>
</TitleText>
<DescriptionText>
<Trans>We hope you have a wonderful time. Remember, Bluesky is:</Trans>
</Description>
</DescriptionText>
<View style={[a.pt_5xl, a.gap_3xl]}>
<View style={[a.flex_row, a.align_center, a.w_full, a.gap_lg]}>
+8 -8
View File
@@ -10,9 +10,9 @@ import {
useSetFeedViewPreferencesMutation,
} from 'state/queries/preferences'
import {
Description,
DescriptionText,
OnboardingControls,
Title,
TitleText,
} from '#/screens/Onboarding/Layout'
import {Context} from '#/screens/Onboarding/state'
import {atoms as a} from '#/alf'
@@ -58,12 +58,12 @@ export function StepFollowingFeed() {
<View style={[a.align_start]}>
<IconCircle icon={FilterTimeline} style={[a.mb_2xl]} />
<Title>
<TitleText>
<Trans>Your default feed is "Following"</Trans>
</Title>
<Description style={[a.mb_md]}>
</TitleText>
<DescriptionText style={[a.mb_md]}>
<Trans>It shows posts from the people you follow as they happen.</Trans>
</Description>
</DescriptionText>
<View style={[a.w_full]}>
<Toggle.Item
@@ -139,9 +139,9 @@ export function StepFollowingFeed() {
</Toggle.Item>
</View>
<Description style={[a.mt_lg]}>
<DescriptionText style={[a.mt_lg]}>
<Trans>You can change these settings later.</Trans>
</Description>
</DescriptionText>
<OnboardingControls.Portal>
<Button
@@ -11,9 +11,9 @@ import {logger} from '#/logger'
import {getAgent} from '#/state/session'
import {useOnboardingDispatch} from '#/state/shell'
import {
Description,
DescriptionText,
OnboardingControls,
Title,
TitleText,
} from '#/screens/Onboarding/Layout'
import {ApiResponseMap, Context} from '#/screens/Onboarding/state'
import {InterestButton} from '#/screens/Onboarding/StepInterests/InterestButton'
@@ -163,8 +163,8 @@ export function StepInterests() {
]}
/>
<Title>{title}</Title>
<Description>{description}</Description>
<TitleText>{title}</TitleText>
<DescriptionText>{description}</DescriptionText>
<View style={[a.w_full, a.pt_2xl]}>
{isLoading ? (
@@ -113,15 +113,15 @@ export function AdultContentEnabledPref({
)}
<Prompt.Outer control={prompt}>
<Prompt.Title>
<Prompt.TitleText>
<Trans>Adult Content</Trans>
</Prompt.Title>
<Prompt.Description>
</Prompt.TitleText>
<Prompt.DescriptionText>
<Trans>
Due to Apple policies, adult content can only be enabled on the web
after completing sign up.
</Trans>
</Prompt.Description>
</Prompt.DescriptionText>
<Prompt.Actions>
<Prompt.Action onPress={() => prompt.close()} cta={_(msg`OK`)} />
</Prompt.Actions>
@@ -9,9 +9,9 @@ import {logEvent} from '#/lib/statsig/statsig'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {usePreferencesSetAdultContentMutation} from 'state/queries/preferences'
import {
Description,
DescriptionText,
OnboardingControls,
Title,
TitleText,
} from '#/screens/Onboarding/Layout'
import {Context} from '#/screens/Onboarding/state'
import {AdultContentEnabledPref} from '#/screens/Onboarding/StepModeration/AdultContentEnabledPref'
@@ -56,14 +56,14 @@ export function StepModeration() {
<View style={[a.align_start]}>
<IconCircle icon={EyeSlash} style={[a.mb_2xl]} />
<Title>
<TitleText>
<Trans>You're in control</Trans>
</Title>
<Description style={[a.mb_xl]}>
</TitleText>
<DescriptionText style={[a.mb_xl]}>
<Trans>
Select what you want to see (or not see), and we’ll handle the rest.
</Trans>
</Description>
</DescriptionText>
{!preferences ? (
<View style={[a.pt_md]}>
@@ -10,9 +10,9 @@ import {capitalize} from '#/lib/strings/capitalize'
import {useModerationOpts} from '#/state/queries/preferences'
import {useProfilesQuery} from '#/state/queries/profile'
import {
Description,
DescriptionText,
OnboardingControls,
Title,
TitleText,
} from '#/screens/Onboarding/Layout'
import {Context} from '#/screens/Onboarding/state'
import {
@@ -136,16 +136,16 @@ export function StepSuggestedAccounts() {
<View style={[a.align_start]}>
<IconCircle icon={At} style={[a.mb_2xl]} />
<Title>
<TitleText>
<Trans>Here are some accounts for you to follow</Trans>
</Title>
<Description>
</TitleText>
<DescriptionText>
{state.interestsStepResults.selectedInterests.length ? (
<Trans>Based on your interest in {interestsText}</Trans>
) : (
<Trans>These are popular accounts you might like:</Trans>
)}
</Description>
</DescriptionText>
<View style={[a.w_full, a.pt_xl]}>
{isLoading ? (
+6 -6
View File
@@ -9,9 +9,9 @@ import {capitalize} from '#/lib/strings/capitalize'
import {IS_TEST_USER} from 'lib/constants'
import {useSession} from 'state/session'
import {
Description,
DescriptionText,
OnboardingControls,
Title,
TitleText,
} from '#/screens/Onboarding/Layout'
import {Context} from '#/screens/Onboarding/state'
import {FeedCard} from '#/screens/Onboarding/StepAlgoFeeds/FeedCard'
@@ -76,10 +76,10 @@ export function StepTopicalFeeds() {
<View style={[a.align_start]}>
<IconCircle icon={ListMagnifyingGlass} style={[a.mb_2xl]} />
<Title>
<TitleText>
<Trans>Feeds can be topical as well!</Trans>
</Title>
<Description>
</TitleText>
<DescriptionText>
{state.interestsStepResults.selectedInterests.length ? (
<Trans>
Here are some topical feeds based on your interests: {interestsText}
@@ -91,7 +91,7 @@ export function StepTopicalFeeds() {
many as you like.
</Trans>
)}
</Description>
</DescriptionText>
<View style={[a.w_full, a.pb_2xl, a.pt_2xl]}>
<Toggle.Group
+7 -8
View File
@@ -1,17 +1,16 @@
import React from 'react'
import {View} from 'react-native'
import {AppBskyActorDefs} from '@atproto/api'
import {Trans, msg} from '@lingui/macro'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {Shadow} from '#/state/cache/types'
import {pluralize} from '#/lib/strings/helpers'
import {Shadow} from '#/state/cache/types'
import {makeProfileLink} from 'lib/routes/links'
import {formatCount} from 'view/com/util/numeric/format'
import {atoms as a, useTheme} from '#/alf'
import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography'
import {InlineLink} from '#/components/Link'
export function ProfileHeaderMetrics({
profile,
@@ -28,7 +27,7 @@ export function ProfileHeaderMetrics({
<View
style={[a.flex_row, a.gap_sm, a.align_center, a.pb_md]}
pointerEvents="box-none">
<InlineLink
<InlineLinkText
testID="profileHeaderFollowersButton"
style={[a.flex_row, t.atoms.text]}
to={makeProfileLink(profile, 'followers')}
@@ -37,8 +36,8 @@ export function ProfileHeaderMetrics({
<Text style={[t.atoms.text_contrast_medium, a.text_md]}>
{pluralizedFollowers}
</Text>
</InlineLink>
<InlineLink
</InlineLinkText>
<InlineLinkText
testID="profileHeaderFollowsButton"
style={[a.flex_row, t.atoms.text]}
to={makeProfileLink(profile, 'follows')}
@@ -49,7 +48,7 @@ export function ProfileHeaderMetrics({
following
</Text>
</Trans>
</InlineLink>
</InlineLinkText>
<Text style={[a.font_bold, t.atoms.text, a.text_md]}>
{formatCount(profile.postsCount || 0)}{' '}
<Text style={[t.atoms.text_contrast_medium, a.font_normal, a.text_md]}>
@@ -242,6 +242,8 @@ let ProfileHeaderLabeler = ({
style={[a.text_md]}
numberOfLines={15}
value={descriptionRT}
enableTags
authorHandle={profile.handle}
/>
</View>
) : undefined}
@@ -314,13 +316,13 @@ function CantSubscribePrompt({
const {_} = useLingui()
return (
<Prompt.Outer control={control}>
<Prompt.Title>Unable to subscribe</Prompt.Title>
<Prompt.Description>
<Prompt.TitleText>Unable to subscribe</Prompt.TitleText>
<Prompt.DescriptionText>
<Trans>
We're sorry! You can only subscribe to ten labelers, and you've
reached your limit of ten.
</Trans>
</Prompt.Description>
</Prompt.DescriptionText>
<Prompt.Actions>
<Prompt.Action onPress={control.close} cta={_(msg`OK`)} />
</Prompt.Actions>
@@ -2,39 +2,38 @@ import React, {memo, useMemo} from 'react'
import {View} from 'react-native'
import {
AppBskyActorDefs,
ModerationOpts,
moderateProfile,
ModerationOpts,
RichText as RichTextAPI,
} from '@atproto/api'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {useAnalytics} from 'lib/analytics/analytics'
import {useSession, useRequireAuth} from '#/state/session'
import {Shadow} from '#/state/cache/types'
import {useProfileShadow} from 'state/cache/profile-shadow'
import {
useProfileFollowMutationQueue,
useProfileBlockMutationQueue,
} from '#/state/queries/profile'
import {logger} from '#/logger'
import {Shadow} from '#/state/cache/types'
import {useModalControls} from '#/state/modals'
import {
useProfileBlockMutationQueue,
useProfileFollowMutationQueue,
} from '#/state/queries/profile'
import {useRequireAuth, useSession} from '#/state/session'
import {useAnalytics} from 'lib/analytics/analytics'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText, ButtonIcon} from '#/components/Button'
import * as Toast from '#/view/com/util/Toast'
import {ProfileHeaderShell} from './Shell'
import {useProfileShadow} from 'state/cache/profile-shadow'
import {ProfileHeaderSuggestedFollows} from '#/view/com/profile/ProfileHeaderSuggestedFollows'
import {ProfileMenu} from '#/view/com/profile/ProfileMenu'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import * as Prompt from '#/components/Prompt'
import {RichText} from '#/components/RichText'
import {ProfileHeaderDisplayName} from './DisplayName'
import {ProfileHeaderHandle} from './Handle'
import {ProfileHeaderMetrics} from './Metrics'
import {ProfileHeaderSuggestedFollows} from '#/view/com/profile/ProfileHeaderSuggestedFollows'
import {RichText} from '#/components/RichText'
import * as Prompt from '#/components/Prompt'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {ProfileHeaderShell} from './Shell'
interface Props {
profile: AppBskyActorDefs.ProfileViewDetailed
@@ -248,6 +247,8 @@ let ProfileHeaderStandard = ({
style={[a.text_md]}
numberOfLines={15}
value={descriptionRT}
enableTags
authorHandle={profile.handle}
/>
</View>
) : undefined}
+4 -17
View File
@@ -4,13 +4,11 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {NativeStackScreenProps, CommonNavigatorParams} from '#/lib/routes/types'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {makeRecordUri} from '#/lib/strings/url-helpers'
import {useSetMinimalShellMode} from '#/state/shell'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {LikedByList} from '#/components/LikedByList'
import {useSetMinimalShellMode} from '#/state/shell'
import {makeRecordUri} from '#/lib/strings/url-helpers'
import {atoms as a, useBreakpoints} from '#/alf'
export function ProfileLabelerLikedByScreen({
route,
@@ -19,7 +17,6 @@ export function ProfileLabelerLikedByScreen({
const {name: handleOrDid} = route.params
const uri = makeRecordUri(handleOrDid, 'app.bsky.labeler.service', 'self')
const {_} = useLingui()
const {gtMobile} = useBreakpoints()
useFocusEffect(
React.useCallback(() => {
@@ -28,17 +25,7 @@ export function ProfileLabelerLikedByScreen({
)
return (
<View
style={[
a.mx_auto,
a.w_full,
a.h_full_vh,
gtMobile && [
{
maxWidth: 600,
},
],
]}>
<View style={{flex: 1}}>
<ViewHeader title={_(msg`Liked By`)} />
<LikedByList uri={uri} />
</View>
+13 -14
View File
@@ -1,30 +1,29 @@
import React from 'react'
import {View} from 'react-native'
import {useSafeAreaFrame} from 'react-native-safe-area-context'
import {
AppBskyLabelerDefs,
ModerationOpts,
interpretLabelValueDefinitions,
InterpretedLabelValueDefinition,
interpretLabelValueDefinitions,
ModerationOpts,
} from '@atproto/api'
import {Trans, msg} from '@lingui/macro'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useSafeAreaFrame} from 'react-native-safe-area-context'
import {useScrollHandlers} from '#/lib/ScrollContext'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
import {isLabelerSubscribed, lookupLabelValueDefinition} from '#/lib/moderation'
import {ListRef} from '#/view/com/util/List'
import {SectionRef} from './types'
import {useScrollHandlers} from '#/lib/ScrollContext'
import {isNative} from '#/platform/detection'
import {useTheme, atoms as a} from '#/alf'
import {Text} from '#/components/Typography'
import {Loader} from '#/components/Loader'
import {Divider} from '#/components/Divider'
import {ListRef} from '#/view/com/util/List'
import {CenteredView, ScrollView} from '#/view/com/util/Views'
import {ErrorState} from '../ErrorState'
import {LabelerLabelPreference} from '#/components/moderation/LabelPreference'
import {atoms as a, useTheme} from '#/alf'
import {Divider} from '#/components/Divider'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {Loader} from '#/components/Loader'
import {LabelerLabelPreference} from '#/components/moderation/LabelPreference'
import {Text} from '#/components/Typography'
import {ErrorState} from '../ErrorState'
import {SectionRef} from './types'
interface LabelsSectionProps {
isLabelerLoading: boolean
+5 -5
View File
@@ -6,7 +6,7 @@ import {useLingui} from '@lingui/react'
import {atoms as a, useTheme} from '#/alf'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {InlineLink} from '#/components/Link'
import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography'
export const Policies = ({
@@ -45,16 +45,16 @@ export const Policies = ({
const els = []
if (tos) {
els.push(
<InlineLink key="tos" to={tos}>
<InlineLinkText key="tos" to={tos}>
{_(msg`Terms of Service`)}
</InlineLink>,
</InlineLinkText>,
)
}
if (pp) {
els.push(
<InlineLink key="pp" to={pp}>
<InlineLinkText key="pp" to={pp}>
{_(msg`Privacy Policy`)}
</InlineLink>,
</InlineLinkText>,
)
}
if (els.length === 2) {
+10 -10
View File
@@ -36,9 +36,9 @@ export function StepInfo() {
<View style={[a.gap_md]}>
<FormError error={state.error} />
<View>
<TextField.Label>
<TextField.LabelText>
<Trans>Hosting provider</Trans>
</TextField.Label>
</TextField.LabelText>
<HostingProvider
serviceUrl={state.serviceUrl}
onSelectServiceUrl={v =>
@@ -54,9 +54,9 @@ export function StepInfo() {
<>
{state.serviceDescription.inviteCodeRequired && (
<View>
<TextField.Label>
<TextField.LabelText>
<Trans>Invite code</Trans>
</TextField.Label>
</TextField.LabelText>
<TextField.Root>
<TextField.Icon icon={Ticket} />
<TextField.Input
@@ -76,9 +76,9 @@ export function StepInfo() {
</View>
)}
<View>
<TextField.Label>
<TextField.LabelText>
<Trans>Email</Trans>
</TextField.Label>
</TextField.LabelText>
<TextField.Root>
<TextField.Icon icon={Envelope} />
<TextField.Input
@@ -97,9 +97,9 @@ export function StepInfo() {
</TextField.Root>
</View>
<View>
<TextField.Label>
<TextField.LabelText>
<Trans>Password</Trans>
</TextField.Label>
</TextField.LabelText>
<TextField.Root>
<TextField.Icon icon={Lock} />
<TextField.Input
@@ -117,9 +117,9 @@ export function StepInfo() {
</TextField.Root>
</View>
<View>
<DateField.Label>
<DateField.LabelText>
<Trans>Your birth date</Trans>
</DateField.Label>
</DateField.LabelText>
<DateField.DateField
testID="date"
value={DateField.utils.toSimpleDateString(state.dateOfBirth)}
+3 -3
View File
@@ -24,7 +24,7 @@ import {StepInfo} from '#/screens/Signup/StepInfo'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {Divider} from '#/components/Divider'
import {InlineLink} from '#/components/Link'
import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography'
export function Signup({onPressBack}: {onPressBack: () => void}) {
@@ -215,9 +215,9 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
<View style={[a.w_full, a.py_lg]}>
<Text style={[t.atoms.text_contrast_medium]}>
<Trans>Having trouble?</Trans>{' '}
<InlineLink to={FEEDBACK_FORM_URL({email: state.email})}>
<InlineLinkText to={FEEDBACK_FORM_URL({email: state.email})}>
<Trans>Contact support</Trans>
</InlineLink>
</InlineLinkText>
</Text>
</View>
</View>
+12 -6
View File
@@ -1,13 +1,14 @@
import {useEffect, useState, useMemo} from 'react'
import EventEmitter from 'eventemitter3'
import {useEffect, useMemo, useState} from 'react'
import {AppBskyFeedDefs} from '@atproto/api'
import {QueryClient} from '@tanstack/react-query'
import EventEmitter from 'eventemitter3'
import {batchedUpdates} from '#/lib/batchedUpdates'
import {Shadow, castAsShadow} from './types'
import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from '../queries/notifications/feed'
import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from '../queries/post-feed'
import {findAllPostsInQueryData as findAllPostsInThreadQueryData} from '../queries/post-thread'
import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from '../queries/search-posts'
import {queryClient} from 'lib/react-query'
import {castAsShadow, Shadow} from './types'
export type {Shadow} from './types'
export interface PostShadow {
@@ -93,8 +94,12 @@ function mergeShadow(
})
}
export function updatePostShadow(uri: string, value: Partial<PostShadow>) {
const cachedPosts = findPostsInCache(uri)
export function updatePostShadow(
queryClient: QueryClient,
uri: string,
value: Partial<PostShadow>,
) {
const cachedPosts = findPostsInCache(queryClient, uri)
for (let post of cachedPosts) {
shadows.set(post, {...shadows.get(post), ...value})
}
@@ -104,6 +109,7 @@ export function updatePostShadow(uri: string, value: Partial<PostShadow>) {
}
function* findPostsInCache(
queryClient: QueryClient,
uri: string,
): Generator<AppBskyFeedDefs.PostView, void> {
for (let post of findAllPostsInFeedQueryData(queryClient, uri)) {
+9 -6
View File
@@ -1,7 +1,10 @@
import {useEffect, useState, useMemo} from 'react'
import EventEmitter from 'eventemitter3'
import {useEffect, useMemo, useState} from 'react'
import {AppBskyActorDefs} from '@atproto/api'
import {QueryClient} from '@tanstack/react-query'
import EventEmitter from 'eventemitter3'
import {batchedUpdates} from '#/lib/batchedUpdates'
import {findAllProfilesInQueryData as findAllProfilesInActorSearchQueryData} from '../queries/actor-search'
import {findAllProfilesInQueryData as findAllProfilesInListMembersQueryData} from '../queries/list-members'
import {findAllProfilesInQueryData as findAllProfilesInMyBlockedAccountsQueryData} from '../queries/my-blocked-accounts'
import {findAllProfilesInQueryData as findAllProfilesInMyMutedAccountsQueryData} from '../queries/my-muted-accounts'
@@ -11,9 +14,7 @@ import {findAllProfilesInQueryData as findAllProfilesInProfileQueryData} from '.
import {findAllProfilesInQueryData as findAllProfilesInProfileFollowersQueryData} from '../queries/profile-followers'
import {findAllProfilesInQueryData as findAllProfilesInProfileFollowsQueryData} from '../queries/profile-follows'
import {findAllProfilesInQueryData as findAllProfilesInSuggestedFollowsQueryData} from '../queries/suggested-follows'
import {findAllProfilesInQueryData as findAllProfilesInActorSearchQueryData} from '../queries/actor-search'
import {Shadow, castAsShadow} from './types'
import {queryClient} from 'lib/react-query'
import {castAsShadow, Shadow} from './types'
export type {Shadow} from './types'
export interface ProfileShadow {
@@ -58,10 +59,11 @@ export function useProfileShadow<
}
export function updateProfileShadow(
queryClient: QueryClient,
did: string,
value: Partial<ProfileShadow>,
) {
const cachedProfiles = findProfilesInCache(did)
const cachedProfiles = findProfilesInCache(queryClient, did)
for (let post of cachedProfiles) {
shadows.set(post, {...shadows.get(post), ...value})
}
@@ -90,6 +92,7 @@ function mergeShadow<TProfileView extends AppBskyActorDefs.ProfileView>(
}
function* findProfilesInCache(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileView, void> {
yield* findAllProfilesInListMembersQueryData(queryClient, did)
+4 -8
View File
@@ -1,11 +1,11 @@
import React from 'react'
import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api'
import {Image as RNImage} from 'react-native-image-crop-picker'
import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api'
import {ImageModel} from '#/state/models/media/image'
import {GalleryModel} from '#/state/models/media/gallery'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {EmbedPlayerSource} from '#/lib/strings/embed-player'
import {GalleryModel} from '#/state/models/media/gallery'
import {ImageModel} from '#/state/models/media/image'
import {ThreadgateSetting} from '../queries/threadgate'
export interface EditProfileModal {
@@ -118,14 +118,11 @@ export interface ChangePasswordModal {
name: 'change-password'
}
export interface SwitchAccountModal {
name: 'switch-account'
}
export interface LinkWarningModal {
name: 'link-warning'
text: string
href: string
share?: boolean
}
export interface EmbedConsentModal {
@@ -148,7 +145,6 @@ export type Modal =
| VerifyEmailModal
| ChangeEmailModal
| ChangePasswordModal
| SwitchAccountModal
// Curation
| ContentLanguagesSettingsModal
+11 -10
View File
@@ -1,21 +1,22 @@
import React from 'react'
import {AppBskyActorDefs, ModerationOpts, moderateProfile} from '@atproto/api'
import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
import {useQuery, useQueryClient} from '@tanstack/react-query'
import {logger} from '#/logger'
import {getAgent} from '#/state/session'
import {useMyFollowsQuery} from '#/state/queries/my-follows'
import {STALE} from '#/state/queries'
import {DEFAULT_LOGGED_OUT_PREFERENCES, useModerationOpts} from './preferences'
import {isInvalidHandle} from '#/lib/strings/handles'
import {isJustAMute} from '#/lib/moderation'
import {isInvalidHandle} from '#/lib/strings/handles'
import {logger} from '#/logger'
import {STALE} from '#/state/queries'
import {useMyFollowsQuery} from '#/state/queries/my-follows'
import {getAgent} from '#/state/session'
import {DEFAULT_LOGGED_OUT_PREFERENCES, useModerationOpts} from './preferences'
const DEFAULT_MOD_OPTS = {
userDid: undefined,
prefs: DEFAULT_LOGGED_OUT_PREFERENCES.moderationPrefs,
}
export const RQKEY = (prefix: string) => ['actor-autocomplete', prefix]
const RQKEY_ROOT = 'actor-autocomplete'
export const RQKEY = (prefix: string) => [RQKEY_ROOT, prefix]
export function useActorAutocompleteQuery(prefix: string) {
const {data: follows, isFetching} = useMyFollowsQuery()
@@ -29,7 +30,7 @@ export function useActorAutocompleteQuery(prefix: string) {
async queryFn() {
const res = prefix
? await getAgent().searchActorsTypeahead({
term: prefix,
q: prefix,
limit: 8,
})
: undefined
@@ -67,7 +68,7 @@ export function useActorAutocompleteFn() {
queryKey: RQKEY(query || ''),
queryFn: () =>
getAgent().searchActorsTypeahead({
term: query,
q: query,
limit,
}),
})
+5 -4
View File
@@ -1,10 +1,11 @@
import {AppBskyActorDefs} from '@atproto/api'
import {QueryClient, useQuery} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
import {STALE} from '#/state/queries'
import {getAgent} from '#/state/session'
export const RQKEY = (prefix: string) => ['actor-search', prefix]
const RQKEY_ROOT = 'actor-search'
export const RQKEY = (prefix: string) => [RQKEY_ROOT, prefix]
export function useActorSearch(prefix: string) {
return useQuery<AppBskyActorDefs.ProfileView[]>({
@@ -12,7 +13,7 @@ export function useActorSearch(prefix: string) {
queryKey: RQKEY(prefix || ''),
async queryFn() {
const res = await getAgent().searchActors({
term: prefix,
q: prefix,
})
return res.data.actors
},
@@ -26,7 +27,7 @@ export function* findAllProfilesInQueryData(
) {
const queryDatas = queryClient.getQueriesData<AppBskyActorDefs.ProfileView[]>(
{
queryKey: ['actor-search'],
queryKey: [RQKEY_ROOT],
},
)
for (const [_queryKey, queryData] of queryDatas) {
+3 -2
View File
@@ -1,10 +1,11 @@
import {ComAtprotoServerCreateAppPassword} from '@atproto/api'
import {useQuery, useQueryClient, useMutation} from '@tanstack/react-query'
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {getAgent} from '../session'
export const RQKEY = () => ['app-passwords']
const RQKEY_ROOT = 'app-passwords'
export const RQKEY = () => [RQKEY_ROOT]
export function useAppPasswordsQuery() {
return useQuery({
+17 -14
View File
@@ -1,24 +1,24 @@
import {
useQuery,
useInfiniteQuery,
InfiniteData,
QueryKey,
useMutation,
} from '@tanstack/react-query'
import {
AtUri,
RichText,
AppBskyFeedDefs,
AppBskyGraphDefs,
AppBskyUnspeccedGetPopularFeedGenerators,
AtUri,
RichText,
} from '@atproto/api'
import {
InfiniteData,
QueryKey,
useInfiniteQuery,
useMutation,
useQuery,
} from '@tanstack/react-query'
import {router} from '#/routes'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {getAgent} from '#/state/session'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {STALE} from '#/state/queries'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {getAgent} from '#/state/session'
import {router} from '#/routes'
export type FeedSourceFeedInfo = {
type: 'feed'
@@ -56,8 +56,9 @@ export type FeedSourceListInfo = {
export type FeedSourceInfo = FeedSourceFeedInfo | FeedSourceListInfo
const feedSourceInfoQueryKeyRoot = 'getFeedSourceInfo'
export const feedSourceInfoQueryKey = ({uri}: {uri: string}) => [
'getFeedSourceInfo',
feedSourceInfoQueryKeyRoot,
uri,
]
@@ -216,6 +217,8 @@ const FOLLOWING_FEED_STUB: FeedSourceInfo = {
likeUri: '',
}
const pinnedFeedInfosQueryKeyRoot = 'pinnedFeedsInfos'
export function usePinnedFeedsInfos() {
const {data: preferences, isLoading: isLoadingPrefs} = usePreferencesQuery()
const pinnedUris = preferences?.feeds?.pinned ?? []
@@ -223,7 +226,7 @@ export function usePinnedFeedsInfos() {
return useQuery({
staleTime: STALE.INFINITY,
enabled: !isLoadingPrefs,
queryKey: ['pinnedFeedsInfos', pinnedUris.join(',')],
queryKey: [pinnedFeedInfosQueryKeyRoot, pinnedUris.join(',')],
queryFn: async () => {
let resolved = new Map()
+9 -4
View File
@@ -1,11 +1,16 @@
import React from 'react'
import {useQueryClient, useMutation} from '@tanstack/react-query'
import {useMutation, useQueryClient} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
import {STALE} from '#/state/queries'
import {getAgent} from '#/state/session'
const fetchHandleQueryKey = (handleOrDid: string) => ['handle', handleOrDid]
const fetchDidQueryKey = (handleOrDid: string) => ['did', handleOrDid]
const handleQueryKeyRoot = 'handle'
const fetchHandleQueryKey = (handleOrDid: string) => [
handleQueryKeyRoot,
handleOrDid,
]
const didQueryKeyRoot = 'did'
const fetchDidQueryKey = (handleOrDid: string) => [didQueryKeyRoot, handleOrDid]
export function useFetchHandle() {
const queryClient = useQueryClient()
+5 -3
View File
@@ -1,14 +1,16 @@
import {ComAtprotoServerDefs} from '@atproto/api'
import {useQuery} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
import {STALE} from '#/state/queries'
import {cleanError} from '#/lib/strings/errors'
import {STALE} from '#/state/queries'
import {getAgent} from '#/state/session'
function isInviteAvailable(invite: ComAtprotoServerDefs.InviteCode): boolean {
return invite.available - invite.uses.length > 0 && !invite.disabled
}
const inviteCodesQueryKeyRoot = 'inviteCodes'
export type InviteCodesQueryResponse = Exclude<
ReturnType<typeof useInviteCodesQuery>['data'],
undefined
@@ -16,7 +18,7 @@ export type InviteCodesQueryResponse = Exclude<
export function useInviteCodesQuery() {
return useQuery({
staleTime: STALE.MINUTES.FIVE,
queryKey: ['inviteCodes'],
queryKey: [inviteCodesQueryKeyRoot],
queryFn: async () => {
const res = await getAgent()
.com.atproto.server.getAccountInviteCodes({})
+17 -9
View File
@@ -1,18 +1,26 @@
import {z} from 'zod'
import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query'
import {AppBskyLabelerDefs} from '@atproto/api'
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {z} from 'zod'
import {getAgent} from '#/state/session'
import {preferencesQueryKey} from '#/state/queries/preferences'
import {labelersDetailedInfoQueryKeyRoot} from '#/lib/react-query'
import {STALE} from '#/state/queries'
import {preferencesQueryKey} from '#/state/queries/preferences'
import {getAgent} from '#/state/session'
export const labelerInfoQueryKey = (did: string) => ['labeler-info', did]
export const labelersInfoQueryKey = (dids: string[]) => [
'labelers-info',
dids.sort(),
const labelerInfoQueryKeyRoot = 'labeler-info'
export const labelerInfoQueryKey = (did: string) => [
labelerInfoQueryKeyRoot,
did,
]
const labelersInfoQueryKeyRoot = 'labelers-info'
export const labelersInfoQueryKey = (dids: string[]) => [
labelersInfoQueryKeyRoot,
dids.slice().sort(),
]
export const labelersDetailedInfoQueryKey = (dids: string[]) => [
'labelers-detailed-info',
labelersDetailedInfoQueryKeyRoot,
dids,
]
+5 -4
View File
@@ -1,18 +1,19 @@
import {AppBskyActorDefs, AppBskyGraphGetList} from '@atproto/api'
import {
useInfiniteQuery,
InfiniteData,
QueryClient,
QueryKey,
useInfiniteQuery,
} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
import {STALE} from '#/state/queries'
import {getAgent} from '#/state/session'
const PAGE_SIZE = 30
type RQPageParam = string | undefined
export const RQKEY = (uri: string) => ['list-members', uri]
const RQKEY_ROOT = 'list-members'
export const RQKEY = (uri: string) => [RQKEY_ROOT, uri]
export function useListMembersQuery(uri: string) {
return useInfiniteQuery<
@@ -44,7 +45,7 @@ export function* findAllProfilesInQueryData(
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyGraphGetList.OutputSchema>
>({
queryKey: ['list-members'],
queryKey: [RQKEY_ROOT],
})
for (const [_queryKey, queryData] of queryDatas) {
if (!queryData) {
+4 -3
View File
@@ -17,16 +17,17 @@
import {AtUri} from '@atproto/api'
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {useSession, getAgent} from '#/state/session'
import {RQKEY as LIST_MEMBERS_RQKEY} from '#/state/queries/list-members'
import {STALE} from '#/state/queries'
import {RQKEY as LIST_MEMBERS_RQKEY} from '#/state/queries/list-members'
import {getAgent, useSession} from '#/state/session'
// sanity limit is SANITY_PAGE_LIMIT*PAGE_SIZE total records
const SANITY_PAGE_LIMIT = 1000
const PAGE_SIZE = 100
// ...which comes 100,000k list members
export const RQKEY = () => ['list-memberships']
const RQKEY_ROOT = 'list-memberships'
export const RQKEY = () => [RQKEY_ROOT]
export interface ListMembersip {
membershipUri: string
+10 -8
View File
@@ -1,21 +1,23 @@
import {Image as RNImage} from 'react-native-image-crop-picker'
import {
AtUri,
AppBskyGraphDefs,
AppBskyGraphGetList,
AppBskyGraphList,
AppBskyGraphDefs,
AtUri,
Facet,
} from '@atproto/api'
import {Image as RNImage} from 'react-native-image-crop-picker'
import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query'
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import chunk from 'lodash.chunk'
import {useSession, getAgent} from '../session'
import {invalidate as invalidateMyLists} from './my-lists'
import {RQKEY as PROFILE_LISTS_RQKEY} from './profile-lists'
import {uploadBlob} from '#/lib/api'
import {until} from '#/lib/async/until'
import {STALE} from '#/state/queries'
import {getAgent, useSession} from '../session'
import {invalidate as invalidateMyLists} from './my-lists'
import {RQKEY as PROFILE_LISTS_RQKEY} from './profile-lists'
export const RQKEY = (uri: string) => ['list', uri]
const RQKEY_ROOT = 'list'
export const RQKEY = (uri: string) => [RQKEY_ROOT, uri]
export function useListQuery(uri?: string) {
return useQuery<AppBskyGraphDefs.ListView, Error>({
+4 -3
View File
@@ -1,14 +1,15 @@
import {AppBskyActorDefs, AppBskyGraphGetBlocks} from '@atproto/api'
import {
useInfiniteQuery,
InfiniteData,
QueryClient,
QueryKey,
useInfiniteQuery,
} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
export const RQKEY = () => ['my-blocked-accounts']
const RQKEY_ROOT = 'my-blocked-accounts'
export const RQKEY = () => [RQKEY_ROOT]
type RQPageParam = string | undefined
export function useMyBlockedAccountsQuery() {
@@ -39,7 +40,7 @@ export function* findAllProfilesInQueryData(
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyGraphGetBlocks.OutputSchema>
>({
queryKey: ['my-blocked-accounts'],
queryKey: [RQKEY_ROOT],
})
for (const [_queryKey, queryData] of queryDatas) {
if (!queryData?.pages) {
+4 -2
View File
@@ -1,14 +1,16 @@
import {AppBskyActorDefs} from '@atproto/api'
import {useQuery} from '@tanstack/react-query'
import {useSession, getAgent} from '../session'
import {STALE} from '#/state/queries'
import {getAgent, useSession} from '../session'
// sanity limit is SANITY_PAGE_LIMIT*PAGE_SIZE total records
const SANITY_PAGE_LIMIT = 1000
const PAGE_SIZE = 100
// ...which comes 10,000k follows
export const RQKEY = () => ['my-follows']
const RQKEY_ROOT = 'my-follows'
export const RQKEY = () => [RQKEY_ROOT]
export function useMyFollowsQuery() {
const {currentAccount} = useSession()
+6 -4
View File
@@ -1,16 +1,18 @@
import {AppBskyGraphDefs} from '@atproto/api'
import {useQuery, QueryClient} from '@tanstack/react-query'
import {QueryClient, useQuery} from '@tanstack/react-query'
import {accumulate} from '#/lib/async/accumulate'
import {useSession, getAgent} from '#/state/session'
import {STALE} from '#/state/queries'
import {getAgent, useSession} from '#/state/session'
export type MyListsFilter =
| 'all'
| 'curate'
| 'mod'
| 'all-including-subscribed'
export const RQKEY = (filter: MyListsFilter) => ['my-lists', filter]
const RQKEY_ROOT = 'my-lists'
export const RQKEY = (filter: MyListsFilter) => [RQKEY_ROOT, filter]
export function useMyListsQuery(filter: MyListsFilter) {
const {currentAccount} = useSession()
@@ -91,6 +93,6 @@ export function invalidate(qc: QueryClient, filter?: MyListsFilter) {
if (filter) {
qc.invalidateQueries({queryKey: RQKEY(filter)})
} else {
qc.invalidateQueries({queryKey: ['my-lists']})
qc.invalidateQueries({queryKey: [RQKEY_ROOT]})
}
}
+4 -3
View File
@@ -1,14 +1,15 @@
import {AppBskyActorDefs, AppBskyGraphGetMutes} from '@atproto/api'
import {
useInfiniteQuery,
InfiniteData,
QueryClient,
QueryKey,
useInfiniteQuery,
} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
export const RQKEY = () => ['my-muted-accounts']
const RQKEY_ROOT = 'my-muted-accounts'
export const RQKEY = () => [RQKEY_ROOT]
type RQPageParam = string | undefined
export function useMyMutedAccountsQuery() {
@@ -39,7 +40,7 @@ export function* findAllProfilesInQueryData(
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyGraphGetMutes.OutputSchema>
>({
queryKey: ['my-muted-accounts'],
queryKey: [RQKEY_ROOT],
})
for (const [_queryKey, queryData] of queryDatas) {
if (!queryData?.pages) {
+12 -10
View File
@@ -19,28 +19,30 @@
import {useEffect, useRef} from 'react'
import {AppBskyFeedDefs} from '@atproto/api'
import {
useInfiniteQuery,
InfiniteData,
QueryKey,
useQueryClient,
QueryClient,
QueryKey,
useInfiniteQuery,
useQueryClient,
} from '@tanstack/react-query'
import {useModerationOpts} from '../preferences'
import {useUnreadNotificationsApi} from './unread'
import {fetchPage} from './util'
import {FeedPage} from './types'
import {useMutedThreads} from '#/state/muted-threads'
import {STALE} from '..'
import {useModerationOpts} from '../preferences'
import {embedViewRecordToPostView, getEmbeddedPost} from '../util'
import {FeedPage} from './types'
import {useUnreadNotificationsApi} from './unread'
import {fetchPage} from './util'
export type {NotificationType, FeedNotification, FeedPage} from './types'
export type {FeedNotification, FeedPage, NotificationType} from './types'
const PAGE_SIZE = 30
type RQPageParam = string | undefined
const RQKEY_ROOT = 'notification-feed'
export function RQKEY() {
return ['notification-feed']
return [RQKEY_ROOT]
}
export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
@@ -138,7 +140,7 @@ export function* findAllPostsInQueryData(
uri: string,
): Generator<AppBskyFeedDefs.PostView, void> {
const queryDatas = queryClient.getQueriesData<InfiniteData<FeedPage>>({
queryKey: ['notification-feed'],
queryKey: [RQKEY_ROOT],
})
for (const [_queryKey, queryData] of queryDatas) {
if (!queryData?.pages) {
+28 -23
View File
@@ -3,37 +3,37 @@ import {AppState} from 'react-native'
import {
AppBskyFeedDefs,
AppBskyFeedPost,
ModerationDecision,
AtUri,
ModerationDecision,
} from '@atproto/api'
import {
useInfiniteQuery,
InfiniteData,
QueryKey,
QueryClient,
QueryKey,
useInfiniteQuery,
useQueryClient,
} from '@tanstack/react-query'
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
import {useFeedTuners} from '../preferences/feed-tuners'
import {FeedTuner, FeedTunerFn, NoopFeedTuner} from 'lib/api/feed-manip'
import {FeedAPI, ReasonFeedSource} from 'lib/api/feed/types'
import {FollowingFeedAPI} from 'lib/api/feed/following'
import {AuthorFeedAPI} from 'lib/api/feed/author'
import {LikesFeedAPI} from 'lib/api/feed/likes'
import {CustomFeedAPI} from 'lib/api/feed/custom'
import {ListFeedAPI} from 'lib/api/feed/list'
import {MergeFeedAPI} from 'lib/api/feed/merge'
import {HomeFeedAPI} from '#/lib/api/feed/home'
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
import {logger} from '#/logger'
import {STALE} from '#/state/queries'
import {precacheFeedPostProfiles} from './profile'
import {getAgent} from '#/state/session'
import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences/const'
import {KnownError} from '#/view/com/posts/FeedErrorMessage'
import {embedViewRecordToPostView, getEmbeddedPost} from './util'
import {useModerationOpts} from './preferences'
import {queryClient} from 'lib/react-query'
import {getAgent} from '#/state/session'
import {AuthorFeedAPI} from 'lib/api/feed/author'
import {CustomFeedAPI} from 'lib/api/feed/custom'
import {FollowingFeedAPI} from 'lib/api/feed/following'
import {LikesFeedAPI} from 'lib/api/feed/likes'
import {ListFeedAPI} from 'lib/api/feed/list'
import {MergeFeedAPI} from 'lib/api/feed/merge'
import {FeedAPI, ReasonFeedSource} from 'lib/api/feed/types'
import {FeedTuner, FeedTunerFn, NoopFeedTuner} from 'lib/api/feed-manip'
import {BSKY_FEED_OWNER_DIDS} from 'lib/constants'
import {KnownError} from '#/view/com/posts/FeedErrorMessage'
import {useFeedTuners} from '../preferences/feed-tuners'
import {useModerationOpts} from './preferences'
import {precacheFeedPostProfiles} from './profile'
import {embedViewRecordToPostView, getEmbeddedPost} from './util'
type ActorDid = string
type AuthorFilter =
@@ -58,8 +58,9 @@ export interface FeedParams {
type RQPageParam = {cursor: string | undefined; api: FeedAPI} | undefined
const RQKEY_ROOT = 'post-feed'
export function RQKEY(feedDesc: FeedDescriptor, params?: FeedParams) {
return ['post-feed', feedDesc, params || {}]
return [RQKEY_ROOT, feedDesc, params || {}]
}
export interface FeedPostSliceItem {
@@ -402,7 +403,7 @@ export function* findAllPostsInQueryData(
const queryDatas = queryClient.getQueriesData<
InfiniteData<FeedPageUnselected>
>({
queryKey: ['post-feed'],
queryKey: [RQKEY_ROOT],
})
for (const [_queryKey, queryData] of queryDatas) {
if (!queryData?.pages) {
@@ -458,12 +459,16 @@ function assertSomePostsPassModeration(feed: AppBskyFeedDefs.FeedViewPost[]) {
}
}
export function resetProfilePostsQueries(did: string, timeout = 0) {
export function resetProfilePostsQueries(
queryClient: QueryClient,
did: string,
timeout = 0,
) {
setTimeout(() => {
queryClient.resetQueries({
predicate: query =>
!!(
query.queryKey[0] === 'post-feed' &&
query.queryKey[0] === RQKEY_ROOT &&
(query.queryKey[1] as string)?.includes(did)
),
})
+4 -3
View File
@@ -1,9 +1,9 @@
import {AppBskyActorDefs, AppBskyFeedGetLikes} from '@atproto/api'
import {
useInfiniteQuery,
InfiniteData,
QueryClient,
QueryKey,
useInfiniteQuery,
} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
@@ -12,7 +12,8 @@ const PAGE_SIZE = 30
type RQPageParam = string | undefined
// TODO refactor invalidate on mutate?
export const RQKEY = (resolvedUri: string) => ['liked-by', resolvedUri]
const RQKEY_ROOT = 'liked-by'
export const RQKEY = (resolvedUri: string) => [RQKEY_ROOT, resolvedUri]
export function useLikedByQuery(resolvedUri: string | undefined) {
return useInfiniteQuery<
@@ -44,7 +45,7 @@ export function* findAllProfilesInQueryData(
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyFeedGetLikes.OutputSchema>
>({
queryKey: ['post-liked-by'],
queryKey: [RQKEY_ROOT],
})
for (const [_queryKey, queryData] of queryDatas) {
if (!queryData?.pages) {
+4 -3
View File
@@ -1,9 +1,9 @@
import {AppBskyActorDefs, AppBskyFeedGetRepostedBy} from '@atproto/api'
import {
useInfiniteQuery,
InfiniteData,
QueryClient,
QueryKey,
useInfiniteQuery,
} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
@@ -12,7 +12,8 @@ const PAGE_SIZE = 30
type RQPageParam = string | undefined
// TODO refactor invalidate on mutate?
export const RQKEY = (resolvedUri: string) => ['post-reposted-by', resolvedUri]
const RQKEY_ROOT = 'post-reposted-by'
export const RQKEY = (resolvedUri: string) => [RQKEY_ROOT, resolvedUri]
export function usePostRepostedByQuery(resolvedUri: string | undefined) {
return useInfiniteQuery<
@@ -44,7 +45,7 @@ export function* findAllProfilesInQueryData(
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyFeedGetRepostedBy.OutputSchema>
>({
queryKey: ['post-reposted-by'],
queryKey: [RQKEY_ROOT],
})
for (const [_queryKey, queryData] of queryDatas) {
if (!queryData?.pages) {
+9 -8
View File
@@ -1,19 +1,20 @@
import {
AppBskyFeedDefs,
AppBskyFeedPost,
AppBskyFeedGetPostThread,
AppBskyEmbedRecord,
AppBskyFeedDefs,
AppBskyFeedGetPostThread,
AppBskyFeedPost,
} from '@atproto/api'
import {useQuery, useQueryClient, QueryClient} from '@tanstack/react-query'
import {QueryClient, useQuery, useQueryClient} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from './post-feed'
import {getAgent} from '#/state/session'
import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from './notifications/feed'
import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from './post-feed'
import {precacheThreadPostProfiles} from './profile'
import {getEmbeddedPost} from './util'
export const RQKEY = (uri: string) => ['post-thread', uri]
const RQKEY_ROOT = 'post-thread'
export const RQKEY = (uri: string) => [RQKEY_ROOT, uri]
type ThreadViewNode = AppBskyFeedGetPostThread.OutputSchema['thread']
export interface ThreadCtx {
@@ -233,7 +234,7 @@ export function* findAllPostsInQueryData(
uri: string,
): Generator<ThreadNode, void> {
const queryDatas = queryClient.getQueriesData<ThreadNode>({
queryKey: ['post-thread'],
queryKey: [RQKEY_ROOT],
})
for (const [_queryKey, queryData] of queryDatas) {
if (!queryData) {
+22 -17
View File
@@ -1,14 +1,16 @@
import {useCallback} from 'react'
import {AppBskyFeedDefs, AtUri} from '@atproto/api'
import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query'
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {track} from '#/lib/analytics/analytics'
import {useToggleMutationQueue} from '#/lib/hooks/useToggleMutationQueue'
import {logEvent, LogEvents} from '#/lib/statsig/statsig'
import {updatePostShadow} from '#/state/cache/post-shadow'
import {Shadow} from '#/state/cache/types'
import {getAgent} from '#/state/session'
import {updatePostShadow} from '#/state/cache/post-shadow'
import {track} from '#/lib/analytics/analytics'
import {logEvent, LogEvents} from '#/lib/statsig/statsig'
import {useToggleMutationQueue} from '#/lib/hooks/useToggleMutationQueue'
export const RQKEY = (postUri: string) => ['post', postUri]
const RQKEY_ROOT = 'post'
export const RQKEY = (postUri: string) => [RQKEY_ROOT, postUri]
export function usePostQuery(uri: string | undefined) {
return useQuery<AppBskyFeedDefs.PostView>({
@@ -62,6 +64,7 @@ export function usePostLikeMutationQueue(
logContext: LogEvents['post:like']['logContext'] &
LogEvents['post:unlike']['logContext'],
) {
const queryClient = useQueryClient()
const postUri = post.uri
const postCid = post.cid
const initialLikeUri = post.viewer?.like
@@ -89,7 +92,7 @@ export function usePostLikeMutationQueue(
},
onSuccess(finalLikeUri) {
// finalize
updatePostShadow(postUri, {
updatePostShadow(queryClient, postUri, {
likeUri: finalLikeUri,
})
},
@@ -97,19 +100,19 @@ export function usePostLikeMutationQueue(
const queueLike = useCallback(() => {
// optimistically update
updatePostShadow(postUri, {
updatePostShadow(queryClient, postUri, {
likeUri: 'pending',
})
return queueToggle(true)
}, [postUri, queueToggle])
}, [queryClient, postUri, queueToggle])
const queueUnlike = useCallback(() => {
// optimistically update
updatePostShadow(postUri, {
updatePostShadow(queryClient, postUri, {
likeUri: undefined,
})
return queueToggle(false)
}, [postUri, queueToggle])
}, [queryClient, postUri, queueToggle])
return [queueLike, queueUnlike]
}
@@ -149,6 +152,7 @@ export function usePostRepostMutationQueue(
logContext: LogEvents['post:repost']['logContext'] &
LogEvents['post:unrepost']['logContext'],
) {
const queryClient = useQueryClient()
const postUri = post.uri
const postCid = post.cid
const initialRepostUri = post.viewer?.repost
@@ -176,7 +180,7 @@ export function usePostRepostMutationQueue(
},
onSuccess(finalRepostUri) {
// finalize
updatePostShadow(postUri, {
updatePostShadow(queryClient, postUri, {
repostUri: finalRepostUri,
})
},
@@ -184,19 +188,19 @@ export function usePostRepostMutationQueue(
const queueRepost = useCallback(() => {
// optimistically update
updatePostShadow(postUri, {
updatePostShadow(queryClient, postUri, {
repostUri: 'pending',
})
return queueToggle(true)
}, [postUri, queueToggle])
}, [queryClient, postUri, queueToggle])
const queueUnrepost = useCallback(() => {
// optimistically update
updatePostShadow(postUri, {
updatePostShadow(queryClient, postUri, {
repostUri: undefined,
})
return queueToggle(false)
}, [postUri, queueToggle])
}, [queryClient, postUri, queueToggle])
return [queueRepost, queueUnrepost]
}
@@ -234,12 +238,13 @@ function usePostUnrepostMutation(
}
export function usePostDeleteMutation() {
const queryClient = useQueryClient()
return useMutation<void, Error, {uri: string}>({
mutationFn: async ({uri}) => {
await getAgent().deletePost(uri)
},
onSuccess(data, variables) {
updatePostShadow(variables.uri, {isDeleted: true})
updatePostShadow(queryClient, variables.uri, {isDeleted: true})
track('Post:Delete')
},
})
+17 -16
View File
@@ -1,35 +1,36 @@
import {useMemo, createContext, useContext} from 'react'
import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query'
import {createContext, useContext, useMemo} from 'react'
import {
LabelPreference,
BskyFeedViewPreference,
ModerationOpts,
AppBskyActorDefs,
BSKY_LABELER_DID,
BskyFeedViewPreference,
LabelPreference,
ModerationOpts,
} from '@atproto/api'
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {track} from '#/lib/analytics/analytics'
import {getAge} from '#/lib/strings/time'
import {getAgent, useSession} from '#/state/session'
import {
UsePreferencesQueryResponse,
ThreadViewPreferences,
} from '#/state/queries/preferences/types'
import {useHiddenPosts, useLabelDefinitions} from '#/state/preferences'
import {STALE} from '#/state/queries'
import {
DEFAULT_HOME_FEED_PREFS,
DEFAULT_THREAD_VIEW_PREFS,
DEFAULT_LOGGED_OUT_PREFERENCES,
DEFAULT_THREAD_VIEW_PREFS,
} from '#/state/queries/preferences/const'
import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/moderation'
import {STALE} from '#/state/queries'
import {useHiddenPosts, useLabelDefinitions} from '#/state/preferences'
import {
ThreadViewPreferences,
UsePreferencesQueryResponse,
} from '#/state/queries/preferences/types'
import {getAgent, useSession} from '#/state/session'
import {saveLabelers} from '#/state/session/agent-config'
export * from '#/state/queries/preferences/types'
export * from '#/state/queries/preferences/moderation'
export * from '#/state/queries/preferences/const'
export * from '#/state/queries/preferences/moderation'
export * from '#/state/queries/preferences/types'
export const preferencesQueryKey = ['getPreferences']
const preferencesQueryKeyRoot = 'getPreferences'
export const preferencesQueryKey = [preferencesQueryKeyRoot]
export function usePreferencesQuery() {
return useQuery({
+3 -2
View File
@@ -1,5 +1,5 @@
import {AppBskyFeedGetActorFeeds} from '@atproto/api'
import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query'
import {InfiniteData, QueryKey, useInfiniteQuery} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
@@ -7,7 +7,8 @@ const PAGE_SIZE = 30
type RQPageParam = string | undefined
// TODO refactor invalidate on mutate?
export const RQKEY = (did: string) => ['profile-feedgens', did]
const RQKEY_ROOT = 'profile-feedgens'
export const RQKEY = (did: string) => [RQKEY_ROOT, did]
export function useProfileFeedgensQuery(
did: string,
+4 -3
View File
@@ -1,9 +1,9 @@
import {AppBskyActorDefs, AppBskyGraphGetFollowers} from '@atproto/api'
import {
useInfiniteQuery,
InfiniteData,
QueryClient,
QueryKey,
useInfiniteQuery,
} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
@@ -11,7 +11,8 @@ import {getAgent} from '#/state/session'
const PAGE_SIZE = 30
type RQPageParam = string | undefined
export const RQKEY = (did: string) => ['profile-followers', did]
const RQKEY_ROOT = 'profile-followers'
export const RQKEY = (did: string) => [RQKEY_ROOT, did]
export function useProfileFollowersQuery(did: string | undefined) {
return useInfiniteQuery<
@@ -43,7 +44,7 @@ export function* findAllProfilesInQueryData(
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyGraphGetFollowers.OutputSchema>
>({
queryKey: ['profile-followers'],
queryKey: [RQKEY_ROOT],
})
for (const [_queryKey, queryData] of queryDatas) {
if (!queryData?.pages) {
+5 -4
View File
@@ -1,19 +1,20 @@
import {AppBskyActorDefs, AppBskyGraphGetFollows} from '@atproto/api'
import {
useInfiniteQuery,
InfiniteData,
QueryClient,
QueryKey,
useInfiniteQuery,
} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
import {STALE} from '#/state/queries'
import {getAgent} from '#/state/session'
const PAGE_SIZE = 30
type RQPageParam = string | undefined
// TODO refactor invalidate on mutate?
export const RQKEY = (did: string) => ['profile-follows', did]
const RQKEY_ROOT = 'profile-follows'
export const RQKEY = (did: string) => [RQKEY_ROOT, did]
export function useProfileFollowsQuery(did: string | undefined) {
return useInfiniteQuery<
@@ -46,7 +47,7 @@ export function* findAllProfilesInQueryData(
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyGraphGetFollows.OutputSchema>
>({
queryKey: ['profile-follows'],
queryKey: [RQKEY_ROOT],
})
for (const [_queryKey, queryData] of queryDatas) {
if (!queryData?.pages) {
+4 -2
View File
@@ -1,11 +1,13 @@
import {AppBskyGraphGetLists} from '@atproto/api'
import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query'
import {InfiniteData, QueryKey, useInfiniteQuery} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
const PAGE_SIZE = 30
type RQPageParam = string | undefined
export const RQKEY = (did: string) => ['profile-lists', did]
const RQKEY_ROOT = 'profile-lists'
export const RQKEY = (did: string) => [RQKEY_ROOT, did]
export function useProfileListsQuery(did: string, opts?: {enabled?: boolean}) {
const enabled = opts?.enabled !== false
+48 -35
View File
@@ -1,38 +1,47 @@
import {useCallback} from 'react'
import {Image as RNImage} from 'react-native-image-crop-picker'
import {
AtUri,
AppBskyActorDefs,
AppBskyActorProfile,
AppBskyActorGetProfile,
AppBskyFeedDefs,
AppBskyActorProfile,
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
AppBskyFeedDefs,
AtUri,
} from '@atproto/api'
import {
QueryClient,
useMutation,
useQuery,
useQueryClient,
useMutation,
QueryClient,
} from '@tanstack/react-query'
import {Image as RNImage} from 'react-native-image-crop-picker'
import {useSession, getAgent} from '../session'
import {updateProfileShadow} from '../cache/profile-shadow'
import {track} from '#/lib/analytics/analytics'
import {uploadBlob} from '#/lib/api'
import {until} from '#/lib/async/until'
import {Shadow} from '#/state/cache/types'
import {resetProfilePostsQueries} from '#/state/queries/post-feed'
import {useToggleMutationQueue} from '#/lib/hooks/useToggleMutationQueue'
import {RQKEY as RQKEY_MY_MUTED} from './my-muted-accounts'
import {RQKEY as RQKEY_MY_BLOCKED} from './my-blocked-accounts'
import {STALE} from '#/state/queries'
import {track} from '#/lib/analytics/analytics'
import {logEvent, LogEvents} from '#/lib/statsig/statsig'
import {Shadow} from '#/state/cache/types'
import {STALE} from '#/state/queries'
import {resetProfilePostsQueries} from '#/state/queries/post-feed'
import {updateProfileShadow} from '../cache/profile-shadow'
import {getAgent, useSession} from '../session'
import {RQKEY as RQKEY_MY_BLOCKED} from './my-blocked-accounts'
import {RQKEY as RQKEY_MY_MUTED} from './my-muted-accounts'
import {ThreadNode} from './post-thread'
export const RQKEY = (did: string) => ['profile', did]
export const profilesQueryKey = (handles: string[]) => ['profiles', handles]
const RQKEY_ROOT = 'profile'
export const RQKEY = (did: string) => [RQKEY_ROOT, did]
const profilesQueryKeyRoot = 'profiles'
export const profilesQueryKey = (handles: string[]) => [
profilesQueryKeyRoot,
handles,
]
const profileBasicQueryKeyRoot = 'profileBasic'
export const profileBasicQueryKey = (didOrHandle: string) => [
'profileBasic',
profileBasicQueryKeyRoot,
didOrHandle,
]
@@ -190,6 +199,7 @@ export function useProfileFollowMutationQueue(
logContext: LogEvents['profile:follow']['logContext'] &
LogEvents['profile:unfollow']['logContext'],
) {
const queryClient = useQueryClient()
const did = profile.did
const initialFollowingUri = profile.viewer?.following
const followMutation = useProfileFollowMutation(logContext)
@@ -215,7 +225,7 @@ export function useProfileFollowMutationQueue(
},
onSuccess(finalFollowingUri) {
// finalize
updateProfileShadow(did, {
updateProfileShadow(queryClient, did, {
followingUri: finalFollowingUri,
})
},
@@ -223,19 +233,19 @@ export function useProfileFollowMutationQueue(
const queueFollow = useCallback(() => {
// optimistically update
updateProfileShadow(did, {
updateProfileShadow(queryClient, did, {
followingUri: 'pending',
})
return queueToggle(true)
}, [did, queueToggle])
}, [queryClient, did, queueToggle])
const queueUnfollow = useCallback(() => {
// optimistically update
updateProfileShadow(did, {
updateProfileShadow(queryClient, did, {
followingUri: undefined,
})
return queueToggle(false)
}, [did, queueToggle])
}, [queryClient, did, queueToggle])
return [queueFollow, queueUnfollow]
}
@@ -269,6 +279,7 @@ function useProfileUnfollowMutation(
export function useProfileMuteMutationQueue(
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>,
) {
const queryClient = useQueryClient()
const did = profile.did
const initialMuted = profile.viewer?.muted
const muteMutation = useProfileMuteMutation()
@@ -291,25 +302,25 @@ export function useProfileMuteMutationQueue(
},
onSuccess(finalMuted) {
// finalize
updateProfileShadow(did, {muted: finalMuted})
updateProfileShadow(queryClient, did, {muted: finalMuted})
},
})
const queueMute = useCallback(() => {
// optimistically update
updateProfileShadow(did, {
updateProfileShadow(queryClient, did, {
muted: true,
})
return queueToggle(true)
}, [did, queueToggle])
}, [queryClient, did, queueToggle])
const queueUnmute = useCallback(() => {
// optimistically update
updateProfileShadow(did, {
updateProfileShadow(queryClient, did, {
muted: false,
})
return queueToggle(false)
}, [did, queueToggle])
}, [queryClient, did, queueToggle])
return [queueMute, queueUnmute]
}
@@ -341,6 +352,7 @@ function useProfileUnmuteMutation() {
export function useProfileBlockMutationQueue(
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>,
) {
const queryClient = useQueryClient()
const did = profile.did
const initialBlockingUri = profile.viewer?.blocking
const blockMutation = useProfileBlockMutation()
@@ -366,7 +378,7 @@ export function useProfileBlockMutationQueue(
},
onSuccess(finalBlockingUri) {
// finalize
updateProfileShadow(did, {
updateProfileShadow(queryClient, did, {
blockingUri: finalBlockingUri,
})
},
@@ -374,19 +386,19 @@ export function useProfileBlockMutationQueue(
const queueBlock = useCallback(() => {
// optimistically update
updateProfileShadow(did, {
updateProfileShadow(queryClient, did, {
blockingUri: 'pending',
})
return queueToggle(true)
}, [did, queueToggle])
}, [queryClient, did, queueToggle])
const queueUnblock = useCallback(() => {
// optimistically update
updateProfileShadow(did, {
updateProfileShadow(queryClient, did, {
blockingUri: undefined,
})
return queueToggle(false)
}, [did, queueToggle])
}, [queryClient, did, queueToggle])
return [queueBlock, queueUnblock]
}
@@ -406,13 +418,14 @@ function useProfileBlockMutation() {
},
onSuccess(_, {did}) {
queryClient.invalidateQueries({queryKey: RQKEY_MY_BLOCKED()})
resetProfilePostsQueries(did, 1000)
resetProfilePostsQueries(queryClient, did, 1000)
},
})
}
function useProfileUnblockMutation() {
const {currentAccount} = useSession()
const queryClient = useQueryClient()
return useMutation<void, Error, {did: string; blockUri: string}>({
mutationFn: async ({blockUri}) => {
if (!currentAccount) {
@@ -425,7 +438,7 @@ function useProfileUnblockMutation() {
})
},
onSuccess(_, {did}) {
resetProfilePostsQueries(did, 1000)
resetProfilePostsQueries(queryClient, did, 1000)
},
})
}
@@ -506,7 +519,7 @@ export function* findAllProfilesInQueryData(
): Generator<AppBskyActorDefs.ProfileViewDetailed, void> {
const queryDatas =
queryClient.getQueriesData<AppBskyActorDefs.ProfileViewDetailed>({
queryKey: ['profile'],
queryKey: [RQKEY_ROOT],
})
for (const [_queryKey, queryData] of queryDatas) {
if (!queryData) {

Some files were not shown because too many files have changed in this diff Show More