Merge branch 'main' into hailey/tweak-env-var

This commit is contained in:
Hailey
2025-01-26 15:23:55 -08:00
116 changed files with 40432 additions and 11315 deletions
+1
View File
@@ -93,6 +93,7 @@ module.exports = {
], ],
}, },
], ],
'@typescript-eslint/ban-ts-comment': 'warn',
}, },
ignorePatterns: [ ignorePatterns: [
'**/__mocks__/*.ts', '**/__mocks__/*.ts',
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright 20232024 Bluesky PBC Copyright 20232025 Bluesky PBC
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+1 -2
View File
@@ -10,8 +10,7 @@ appId: xyz.blueskyweb.app
id: "e2eOpenLoggedOutView" id: "e2eOpenLoggedOutView"
- tapOn: - tapOn:
id: "createAccountButton" id: "createAccountButton"
- tapOn: - tapOn: "Bluesky Social"
id: "selectServiceButton"
- tapOn: - tapOn:
id: "customSelectBtn" id: "customSelectBtn"
- tapOn: - tapOn:
+1 -1
View File
@@ -21,7 +21,7 @@ appId: xyz.blueskyweb.app
label: "Feeds button disappears after pinning a feed" label: "Feeds button disappears after pinning a feed"
id: "bottomBarProfileBtn" id: "bottomBarProfileBtn"
- tapOn: - tapOn:
id: "profilePager-selector-4" id: "profilePager-selector-5"
- tapOn: "alice-favs" - tapOn: "alice-favs"
- tapOn: "Pin to Home" - tapOn: "Pin to Home"
- tapOn: - tapOn:
+21 -3
View File
@@ -75,9 +75,11 @@ module.exports = function (config) {
'an', 'an',
'ast', 'ast',
'ca', 'ca',
'da',
'de', 'de',
'el', 'el',
'es', 'es',
'eu',
'fi', 'fi',
'fr', 'fr',
'ga', 'ga',
@@ -95,11 +97,12 @@ module.exports = function (config) {
'pt-BR', 'pt-BR',
'ro', 'ro',
'ru', 'ru',
'sv',
'th', 'th',
'tr', 'tr',
'uk', 'uk',
'vi', 'vi',
'yue-Hant', 'yue',
'zh-Hans', 'zh-Hans',
'zh-Hant', 'zh-Hant',
], ],
@@ -191,6 +194,7 @@ module.exports = function (config) {
channel: UPDATES_CHANNEL, channel: UPDATES_CHANNEL,
}, },
plugins: [ plugins: [
'expo-video',
'expo-localization', 'expo-localization',
USE_SENTRY && [ USE_SENTRY && [
'@sentry/react-native/expo', '@sentry/react-native/expo',
@@ -225,8 +229,12 @@ module.exports = function (config) {
}, },
], ],
'react-native-compressor', 'react-native-compressor',
// TODO: Reenable when the build issue is fixed. [
// '@bitdrift/react-native', '@bitdrift/react-native',
{
networkInstrumentation: true,
},
],
'./plugins/starterPackAppClipExtension/withStarterPackAppClip.js', './plugins/starterPackAppClipExtension/withStarterPackAppClip.js',
'./plugins/withAndroidManifestPlugin.js', './plugins/withAndroidManifestPlugin.js',
'./plugins/withAndroidManifestFCMIconPlugin.js', './plugins/withAndroidManifestFCMIconPlugin.js',
@@ -347,6 +355,16 @@ module.exports = function (config) {
}, },
], ],
['expo-screen-orientation', {initialOrientation: 'PORTRAIT_UP'}], ['expo-screen-orientation', {initialOrientation: 'PORTRAIT_UP'}],
[
'react-native-vision-camera',
{
enableLocation: false,
cameraPermissionText: 'Bluesky needs access to your camera.',
enableMicrophonePermission: true,
microphonePermissionText:
'Bluesky needs access to your microphone.',
},
],
].filter(Boolean), ].filter(Boolean),
extra: { extra: {
eas: { eas: {
+13 -7
View File
@@ -214,15 +214,21 @@ func serve(cctx *cli.Context) error {
e.GET("/iframe/youtube.html", echo.WrapHandler(staticHandler)) e.GET("/iframe/youtube.html", echo.WrapHandler(staticHandler))
e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler)), func(next echo.HandlerFunc) echo.HandlerFunc { e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler)), func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
path := c.Request().URL.Path c.Response().Before(func() {
maxAge := 1 * (60 * 60) // default is 1 hour if c.Response().Status >= 300 {
return
}
// all assets in /static/js, /static/css, /static/media are content-hashed and can be cached for a long time path := c.Request().URL.Path
if strings.HasPrefix(path, "/static/js/") || strings.HasPrefix(path, "/static/css/") || strings.HasPrefix(path, "/static/media/") { maxAge := 1 * (60 * 60) // default is 1 hour
maxAge = 365 * (60 * 60 * 24) // 1 year
}
c.Response().Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", maxAge)) // all assets in /static/js, /static/css, /static/media are content-hashed and can be cached for a long time
if strings.HasPrefix(path, "/static/js/") || strings.HasPrefix(path, "/static/css/") || strings.HasPrefix(path, "/static/media/") {
maxAge = 365 * (60 * 60 * 24) // 1 year
}
c.Response().Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", maxAge))
})
return next(c) return next(c)
} }
}) })
-1
View File
@@ -8,7 +8,6 @@
Preconnect to essential domains Preconnect to essential domains
--> -->
<link rel="preconnect" href="https://bsky.social"> <link rel="preconnect" href="https://bsky.social">
<link rel="preconnect" href="https://bsky.network">
<title>{%- block head_title -%}Bluesky{%- endblock -%}</title> <title>{%- block head_title -%}Bluesky{%- endblock -%}</title>
<!-- Hello Humans! API docs at https://atproto.com --> <!-- Hello Humans! API docs at https://atproto.com -->
+3
View File
@@ -5,10 +5,12 @@ module.exports = {
'an', 'an',
'ast', 'ast',
'ca', 'ca',
'da',
'de', 'de',
'el', 'el',
'en-GB', 'en-GB',
'es', 'es',
'eu',
'fi', 'fi',
'fr', 'fr',
'ga', 'ga',
@@ -26,6 +28,7 @@ module.exports = {
'pt-BR', 'pt-BR',
'ro', 'ro',
'ru', 'ru',
'sv',
'th', 'th',
'tr', 'tr',
'uk', 'uk',
+5 -2
View File
@@ -54,7 +54,8 @@
"icons:optimize": "svgo -f ./assets/icons" "icons:optimize": "svgo -f ./assets/icons"
}, },
"dependencies": { "dependencies": {
"@atproto/api": "^0.13.21", "@atproto/api": "^0.13.31",
"@bitdrift/react-native": "^0.6.2",
"@braintree/sanitize-url": "^6.0.2", "@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
"@emoji-mart/react": "^1.1.1", "@emoji-mart/react": "^1.1.1",
@@ -145,6 +146,7 @@
"expo-system-ui": "^4.0.4", "expo-system-ui": "^4.0.4",
"expo-task-manager": "~12.0.3", "expo-task-manager": "~12.0.3",
"expo-updates": "~0.26.10", "expo-updates": "~0.26.10",
"expo-video": "^2.0.5",
"expo-web-browser": "~14.0.1", "expo-web-browser": "~14.0.1",
"fast-text-encoding": "^1.0.6", "fast-text-encoding": "^1.0.6",
"history": "^5.3.0", "history": "^5.3.0",
@@ -178,7 +180,7 @@
"react-native-ios-context-menu": "^1.15.3", "react-native-ios-context-menu": "^1.15.3",
"react-native-keyboard-controller": "^1.14.5", "react-native-keyboard-controller": "^1.14.5",
"react-native-mmkv": "^2.12.2", "react-native-mmkv": "^2.12.2",
"react-native-pager-view": "6.6.1", "react-native-pager-view": "6.7.0",
"react-native-picker-select": "^9.3.1", "react-native-picker-select": "^9.3.1",
"react-native-progress": "bluesky-social/react-native-progress", "react-native-progress": "bluesky-social/react-native-progress",
"react-native-qrcode-styled": "^0.3.3", "react-native-qrcode-styled": "^0.3.3",
@@ -191,6 +193,7 @@
"react-native-url-polyfill": "^1.3.0", "react-native-url-polyfill": "^1.3.0",
"react-native-uuid": "^2.0.2", "react-native-uuid": "^2.0.2",
"react-native-view-shot": "^4.0.3", "react-native-view-shot": "^4.0.3",
"react-native-vision-camera": "^4.6.3",
"react-native-web": "~0.19.13", "react-native-web": "~0.19.13",
"react-native-web-webview": "^1.0.2", "react-native-web-webview": "^1.0.2",
"react-native-webview": "13.12.3", "react-native-webview": "13.12.3",
+9
View File
@@ -86,6 +86,7 @@ import {
StarterPackScreenShort, StarterPackScreenShort,
} from '#/screens/StarterPack/StarterPackScreen' } from '#/screens/StarterPack/StarterPackScreen'
import {Wizard} from '#/screens/StarterPack/Wizard' import {Wizard} from '#/screens/StarterPack/Wizard'
import {VideoFeed} from '#/screens/VideoFeed'
import {useTheme} from '#/alf' import {useTheme} from '#/alf'
import {router} from '#/routes' import {router} from '#/routes'
import {Referrer} from '../modules/expo-bluesky-swiss-army' import {Referrer} from '../modules/expo-bluesky-swiss-army'
@@ -422,6 +423,14 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
getComponent={() => Wizard} getComponent={() => Wizard}
options={{title: title(msg`Edit your starter pack`), requireAuth: true}} options={{title: title(msg`Edit your starter pack`), requireAuth: true}}
/> />
<Stack.Screen
name="VideoFeed"
getComponent={() => VideoFeed}
options={{
title: title(msg`Video Feed`),
requireAuth: true,
}}
/>
</> </>
) )
} }
+1 -1
View File
@@ -497,7 +497,7 @@ export function createThemes({
color: dimPalette.contrast_400, color: dimPalette.contrast_400,
}, },
text_contrast_medium: { text_contrast_medium: {
color: dimPalette.contrast_700, color: dimPalette.contrast_600,
}, },
text_contrast_high: { text_contrast_high: {
color: dimPalette.contrast_900, color: dimPalette.contrast_900,
+3 -1
View File
@@ -27,6 +27,7 @@ import {useA11y} from '#/state/a11y'
import {useDialogStateControlContext} from '#/state/dialogs' import {useDialogStateControlContext} from '#/state/dialogs'
import {List, ListMethods, ListProps} from '#/view/com/util/List' import {List, ListMethods, ListProps} from '#/view/com/util/List'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {useThemeName} from '#/alf/util/useColorModeTheme'
import {Context, useDialogContext} from '#/components/Dialog/context' import {Context, useDialogContext} from '#/components/Dialog/context'
import { import {
DialogControlProps, DialogControlProps,
@@ -55,7 +56,8 @@ export function Outer({
nativeOptions, nativeOptions,
testID, testID,
}: React.PropsWithChildren<DialogOuterProps>) { }: React.PropsWithChildren<DialogOuterProps>) {
const t = useTheme() const themeName = useThemeName()
const t = useTheme(themeName)
const ref = React.useRef<BottomSheetNativeComponent>(null) const ref = React.useRef<BottomSheetNativeComponent>(null)
const closeCallbacks = React.useRef<(() => void)[]>([]) const closeCallbacks = React.useRef<(() => void)[]>([])
const {setDialogIsOpen, setFullyExpandedCount} = const {setDialogIsOpen, setFullyExpandedCount} =
+59 -51
View File
@@ -1,6 +1,5 @@
import React from 'react' import React from 'react'
import {View} from 'react-native' import {ScrollView, View} from 'react-native'
import {ScrollView} from 'react-native-gesture-handler'
import {AppBskyActorDefs, AppBskyFeedDefs, AtUri} from '@atproto/api' import {AppBskyActorDefs, AppBskyFeedDefs, AtUri} from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@@ -17,6 +16,7 @@ import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import * as userActionHistory from '#/state/userActionHistory' import * as userActionHistory from '#/state/userActionHistory'
import {SeenPost} from '#/state/userActionHistory' import {SeenPost} from '#/state/userActionHistory'
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
import {atoms as a, useBreakpoints, useTheme, ViewStyleProp, web} from '#/alf' import {atoms as a, useBreakpoints, useTheme, ViewStyleProp, web} from '#/alf'
import {Button} from '#/components/Button' import {Button} from '#/components/Button'
import * as FeedCard from '#/components/FeedCard' import * as FeedCard from '#/components/FeedCard'
@@ -358,33 +358,37 @@ export function ProfileGrid({
</View> </View>
</View> </View>
) : ( ) : (
<ScrollView <BlockDrawerGesture>
horizontal <ScrollView
showsHorizontalScrollIndicator={false} horizontal
snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap} showsHorizontalScrollIndicator={false}
decelerationRate="fast"> snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap}
<View style={[a.px_lg, a.pt_sm, a.pb_lg, a.flex_row, a.gap_md]}> decelerationRate="fast">
{content} <View style={[a.px_lg, a.pt_sm, a.pb_lg, a.flex_row, a.gap_md]}>
{content}
<Button <Button
label={_(msg`Browse more accounts on the Explore page`)} label={_(msg`Browse more accounts on the Explore page`)}
onPress={() => { onPress={() => {
navigation.navigate('SearchTab') navigation.navigate('SearchTab')
}}> }}>
<CardOuter style={[a.flex_1, {borderWidth: 0}]}> <CardOuter style={[a.flex_1, {borderWidth: 0}]}>
<View style={[a.flex_1, a.justify_center]}> <View style={[a.flex_1, a.justify_center]}>
<View style={[a.flex_row, a.px_lg]}> <View style={[a.flex_row, a.px_lg]}>
<Text style={[a.pr_xl, a.flex_1, a.leading_snug]}> <Text style={[a.pr_xl, a.flex_1, a.leading_snug]}>
<Trans>Browse more suggestions on the Explore page</Trans> <Trans>
</Text> Browse more suggestions on the Explore page
</Trans>
</Text>
<Arrow size="xl" /> <Arrow size="xl" />
</View>
</View> </View>
</View> </CardOuter>
</CardOuter> </Button>
</Button> </View>
</View> </ScrollView>
</ScrollView> </BlockDrawerGesture>
)} )}
</View> </View>
) )
@@ -491,34 +495,38 @@ export function SuggestedFeeds() {
</View> </View>
</View> </View>
) : ( ) : (
<ScrollView <BlockDrawerGesture>
horizontal <ScrollView
showsHorizontalScrollIndicator={false} horizontal
snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap} showsHorizontalScrollIndicator={false}
decelerationRate="fast"> snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap}
<View style={[a.px_lg, a.pt_md, a.pb_xl, a.flex_row, a.gap_md]}> decelerationRate="fast">
{content} <View style={[a.px_lg, a.pt_md, a.pb_xl, a.flex_row, a.gap_md]}>
{content}
<Button <Button
label={_(msg`Browse more feeds on the Explore page`)} label={_(msg`Browse more feeds on the Explore page`)}
onPress={() => { onPress={() => {
navigation.navigate('SearchTab') navigation.navigate('SearchTab')
}} }}
style={[a.flex_col]}> style={[a.flex_col]}>
<CardOuter style={[a.flex_1]}> <CardOuter style={[a.flex_1]}>
<View style={[a.flex_1, a.justify_center]}> <View style={[a.flex_1, a.justify_center]}>
<View style={[a.flex_row, a.px_lg]}> <View style={[a.flex_row, a.px_lg]}>
<Text style={[a.pr_xl, a.flex_1, a.leading_snug]}> <Text style={[a.pr_xl, a.flex_1, a.leading_snug]}>
<Trans>Browse more suggestions on the Explore page</Trans> <Trans>
</Text> Browse more suggestions on the Explore page
</Trans>
</Text>
<Arrow size="xl" /> <Arrow size="xl" />
</View>
</View> </View>
</View> </CardOuter>
</CardOuter> </Button>
</Button> </View>
</View> </ScrollView>
</ScrollView> </BlockDrawerGesture>
)} )}
</View> </View>
) )
+59
View File
@@ -0,0 +1,59 @@
import {createContext, useContext, useMemo} from 'react'
import {View} from 'react-native'
import {atoms as a, ViewStyleProp} from '#/alf'
const Context = createContext({
gap: 0,
})
export function Row({
children,
gap = 0,
style,
}: ViewStyleProp & {
children: React.ReactNode
gap?: number
}) {
return (
<Context.Provider value={useMemo(() => ({gap}), [gap])}>
<View
style={[
a.flex_row,
a.flex_1,
{
marginLeft: -gap / 2,
marginRight: -gap / 2,
},
style,
]}>
{children}
</View>
</Context.Provider>
)
}
export function Col({
children,
width = 1,
style,
}: ViewStyleProp & {
children: React.ReactNode
width?: number
}) {
const {gap} = useContext(Context)
return (
<View
style={[
a.flex_col,
{
paddingLeft: gap / 2,
paddingRight: gap / 2,
width: `${width * 100}%`,
},
style,
]}>
{children}
</View>
)
}
+5 -1
View File
@@ -122,7 +122,11 @@ export function BackButton({onPress, style, ...props}: Partial<ButtonProps>) {
shape="square" shape="square"
onPress={onPressBack} onPress={onPressBack}
hitSlop={HITSLOP_30} hitSlop={HITSLOP_30}
style={[{marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET}, style]} style={[
{marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET},
a.bg_transparent,
style,
]}
{...props}> {...props}>
<ButtonIcon icon={ArrowLeft} size="lg" /> <ButtonIcon icon={ArrowLeft} size="lg" />
</Button> </Button>
+2
View File
@@ -58,6 +58,7 @@ export const Content = React.memo(function Content({
contentContainerStyle, contentContainerStyle,
...props ...props
}: ContentProps) { }: ContentProps) {
const t = useTheme()
const {footerHeight} = useShellLayout() const {footerHeight} = useShellLayout()
const animatedProps = useAnimatedProps(() => { const animatedProps = useAnimatedProps(() => {
return { return {
@@ -73,6 +74,7 @@ export const Content = React.memo(function Content({
<Animated.ScrollView <Animated.ScrollView
id="content" id="content"
automaticallyAdjustsScrollIndicatorInsets={false} automaticallyAdjustsScrollIndicatorInsets={false}
indicatorStyle={t.scheme === 'dark' ? 'white' : 'black'}
// sets the scroll inset to the height of the footer // sets the scroll inset to the height of the footer
animatedProps={animatedProps} animatedProps={animatedProps}
style={[scrollViewStyles.common, style]} style={[scrollViewStyles.common, style]}
+10 -4
View File
@@ -6,12 +6,18 @@ import {gradients} from '#/alf/tokens'
export function LinearGradientBackground({ export function LinearGradientBackground({
style, style,
gradient = 'sky',
children, children,
start,
end,
}: { }: {
style: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
children: React.ReactNode gradient?: keyof typeof gradients
children?: React.ReactNode
start?: [number, number]
end?: [number, number]
}) { }) {
const gradient = gradients.sky.values.map(([_, color]) => { const colors = gradients[gradient].values.map(([_, color]) => {
return color return color
}) as [string, string, ...string[]] }) as [string, string, ...string[]]
@@ -20,7 +26,7 @@ export function LinearGradientBackground({
} }
return ( return (
<LinearGradient colors={gradient} style={style}> <LinearGradient colors={colors} style={style} start={start} end={end}>
{children} {children}
</LinearGradient> </LinearGradient>
) )
+120 -24
View File
@@ -15,7 +15,6 @@ import {
linkRequiresWarning, linkRequiresWarning,
} from '#/lib/strings/url-helpers' } from '#/lib/strings/url-helpers'
import {isNative, isWeb} from '#/platform/detection' import {isNative, isWeb} from '#/platform/detection'
import {shouldClickOpenNewTab} from '#/platform/urls'
import {useModalControls} from '#/state/modals' import {useModalControls} from '#/state/modals'
import {atoms as a, flatten, TextStyleProp, useTheme, web} from '#/alf' import {atoms as a, flatten, TextStyleProp, useTheme, web} from '#/alf'
import {Button, ButtonProps} from '#/components/Button' import {Button, ButtonProps} from '#/components/Button'
@@ -55,6 +54,12 @@ type BaseLinkProps = Pick<
*/ */
onPress?: (e: GestureResponderEvent) => void | false onPress?: (e: GestureResponderEvent) => void | false
/**
* Callback for when the link is long pressed (on native). Prevent default
* and return `false` to exit early and prevent default long press hander.
*/
onLongPress?: (e: GestureResponderEvent) => void | false
/** /**
* Web-only attribute. Sets `download` attr on web. * Web-only attribute. Sets `download` attr on web.
*/ */
@@ -72,9 +77,12 @@ export function useLink({
action = 'push', action = 'push',
disableMismatchWarning, disableMismatchWarning,
onPress: outerOnPress, onPress: outerOnPress,
onLongPress: outerOnLongPress,
shareOnLongPress, shareOnLongPress,
overridePresentation,
}: BaseLinkProps & { }: BaseLinkProps & {
displayText: string displayText: string
overridePresentation?: boolean
}) { }) {
const navigation = useNavigationDeduped() const navigation = useNavigationDeduped()
const {href} = useLinkProps<AllNavigatorParams>({ const {href} = useLinkProps<AllNavigatorParams>({
@@ -110,7 +118,7 @@ export function useLink({
}) })
} else { } else {
if (isExternal) { if (isExternal) {
openLink(href) openLink(href, overridePresentation)
} else { } else {
const shouldOpenInNewTab = shouldClickOpenNewTab(e) const shouldOpenInNewTab = shouldClickOpenNewTab(e)
@@ -152,6 +160,7 @@ export function useLink({
closeModal, closeModal,
action, action,
navigation, navigation,
overridePresentation,
], ],
) )
@@ -175,8 +184,14 @@ export function useLink({
} }
}, [disableMismatchWarning, displayText, href, isExternal, openModal]) }, [disableMismatchWarning, displayText, href, isExternal, openModal])
const onLongPress = const onLongPress = React.useCallback(
isNative && isExternal && shareOnLongPress ? handleLongPress : undefined (e: GestureResponderEvent) => {
const exitEarlyIfFalse = outerOnLongPress?.(e)
if (exitEarlyIfFalse === false) return
return isNative && shareOnLongPress ? handleLongPress() : undefined
},
[outerOnLongPress, handleLongPress, shareOnLongPress],
)
return { return {
isExternal, isExternal,
@@ -202,14 +217,16 @@ export function Link({
to, to,
action = 'push', action = 'push',
onPress: outerOnPress, onPress: outerOnPress,
onLongPress: outerOnLongPress,
download, download,
...rest ...rest
}: LinkProps) { }: LinkProps) {
const {href, isExternal, onPress} = useLink({ const {href, isExternal, onPress, onLongPress} = useLink({
to, to,
displayText: typeof children === 'string' ? children : '', displayText: typeof children === 'string' ? children : '',
action, action,
onPress: outerOnPress, onPress: outerOnPress,
onLongPress: outerOnLongPress,
}) })
return ( return (
@@ -220,6 +237,7 @@ export function Link({
accessibilityRole="link" accessibilityRole="link"
href={href} href={href}
onPress={download ? undefined : onPress} onPress={download ? undefined : onPress}
onLongPress={onLongPress}
{...web({ {...web({
hrefAttrs: { hrefAttrs: {
target: download ? undefined : isExternal ? 'blank' : undefined, target: download ? undefined : isExternal ? 'blank' : undefined,
@@ -239,12 +257,13 @@ export function Link({
export type InlineLinkProps = React.PropsWithChildren< export type InlineLinkProps = React.PropsWithChildren<
BaseLinkProps & BaseLinkProps &
TextStyleProp & TextStyleProp &
Pick<TextProps, 'selectable' | 'numberOfLines'> Pick<TextProps, 'selectable' | 'numberOfLines'> &
> & Pick<ButtonProps, 'label' | 'accessibilityHint'> & {
Pick<ButtonProps, 'label'> & { disableUnderline?: boolean
disableUnderline?: boolean title?: TextProps['title']
title?: TextProps['title'] overridePresentation?: boolean
} }
>
export function InlineLinkText({ export function InlineLinkText({
children, children,
@@ -253,11 +272,13 @@ export function InlineLinkText({
disableMismatchWarning, disableMismatchWarning,
style, style,
onPress: outerOnPress, onPress: outerOnPress,
onLongPress: outerOnLongPress,
download, download,
selectable, selectable,
label, label,
shareOnLongPress, shareOnLongPress,
disableUnderline, disableUnderline,
overridePresentation,
...rest ...rest
}: InlineLinkProps) { }: InlineLinkProps) {
const t = useTheme() const t = useTheme()
@@ -268,7 +289,9 @@ export function InlineLinkText({
action, action,
disableMismatchWarning, disableMismatchWarning,
onPress: outerOnPress, onPress: outerOnPress,
onLongPress: outerOnLongPress,
shareOnLongPress, shareOnLongPress,
overridePresentation,
}) })
const { const {
state: hovered, state: hovered,
@@ -319,6 +342,21 @@ export function InlineLinkText({
) )
} }
export function WebOnlyInlineLinkText({
children,
to,
onPress,
...props
}: Omit<InlineLinkProps, 'onLongPress'>) {
return isWeb ? (
<InlineLinkText {...props} to={to} onPress={onPress}>
{children}
</InlineLinkText>
) : (
<Text {...props}>{children}</Text>
)
}
/** /**
* Utility to create a static `onPress` handler for a `Link` that would otherwise link to a URI * Utility to create a static `onPress` handler for a `Link` that would otherwise link to a URI
* *
@@ -327,7 +365,10 @@ export function InlineLinkText({
*/ */
export function createStaticClick( export function createStaticClick(
onPressHandler: Exclude<BaseLinkProps['onPress'], undefined>, onPressHandler: Exclude<BaseLinkProps['onPress'], undefined>,
): Pick<BaseLinkProps, 'to' | 'onPress'> { ): {
to: BaseLinkProps['to']
onPress: Exclude<BaseLinkProps['onPress'], undefined>
} {
return { return {
to: '#', to: '#',
onPress(e: GestureResponderEvent) { onPress(e: GestureResponderEvent) {
@@ -338,17 +379,72 @@ export function createStaticClick(
} }
} }
export function WebOnlyInlineLinkText({ /**
children, * Utility to create a static `onPress` handler for a `Link`, but only if the
to, * click was not modified in some way e.g. `Cmd` or a middle click.
onPress, *
...props * On native, this behaves the same as `createStaticClick` because there are no
}: InlineLinkProps) { * options to "modify" the click in this sense.
return isWeb ? ( *
<InlineLinkText {...props} to={to} onPress={onPress}> * Example:
{children} * `<Link {...createStaticClick(e => {...})} />`
</InlineLinkText> */
) : ( export function createStaticClickIfUnmodified(
<Text {...props}>{children}</Text> onPressHandler: Exclude<BaseLinkProps['onPress'], undefined>,
): {onPress: Exclude<BaseLinkProps['onPress'], undefined>} {
return {
onPress(e: GestureResponderEvent) {
if (!isWeb || !isModifiedClickEvent(e)) {
e.preventDefault()
onPressHandler(e)
return false
}
},
}
}
/**
* Determines if the click event has a meta key pressed, indicating the user
* intends to deviate from default behavior.
*/
export function isClickEventWithMetaKey(e: GestureResponderEvent) {
if (!isWeb) return false
const event = e as unknown as MouseEvent
return event.metaKey || event.altKey || event.ctrlKey || event.shiftKey
}
/**
* Determines if the web click target is anything other than `_self`
*/
export function isClickTargetExternal(e: GestureResponderEvent) {
if (!isWeb) return false
const event = e as unknown as MouseEvent
const el = event.currentTarget as HTMLAnchorElement
return el && el.target && el.target !== '_self'
}
/**
* Determines if a click event has been modified in some way from its default
* behavior, e.g. `Cmd` or a middle click.
* {@link https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button}
*/
export function isModifiedClickEvent(e: GestureResponderEvent): boolean {
if (!isWeb) return false
const event = e as unknown as MouseEvent
const isPrimaryButton = event.button === 0
return (
isClickEventWithMetaKey(e) || isClickTargetExternal(e) || !isPrimaryButton
) )
} }
/**
* Determines if a click event has been modified in a way that should indiciate
* that the user intends to open a new tab.
* {@link https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button}
*/
export function shouldClickOpenNewTab(e: GestureResponderEvent) {
if (!isWeb) return false
const event = e as unknown as MouseEvent
const isMiddleClick = isWeb && event.button === 1
return isClickEventWithMetaKey(e) || isClickTargetExternal(e) || isMiddleClick
}
+9 -3
View File
@@ -20,6 +20,7 @@ export function ListFooter({
style, style,
showEndMessage = false, showEndMessage = false,
endMessageText, endMessageText,
renderEndMessage,
}: { }: {
isFetchingNextPage?: boolean isFetchingNextPage?: boolean
hasNextPage?: boolean hasNextPage?: boolean
@@ -29,6 +30,7 @@ export function ListFooter({
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
showEndMessage?: boolean showEndMessage?: boolean
endMessageText?: string endMessageText?: string
renderEndMessage?: () => React.ReactNode
}) { }) {
const t = useTheme() const t = useTheme()
@@ -48,9 +50,13 @@ export function ListFooter({
) : error ? ( ) : error ? (
<ListFooterMaybeError error={error} onRetry={onRetry} /> <ListFooterMaybeError error={error} onRetry={onRetry} />
) : !hasNextPage && showEndMessage ? ( ) : !hasNextPage && showEndMessage ? (
<Text style={[a.text_sm, t.atoms.text_contrast_low]}> renderEndMessage ? (
{endMessageText ?? <Trans>You have reached the end</Trans>} renderEndMessage()
</Text> ) : (
<Text style={[a.text_sm, t.atoms.text_contrast_low]}>
{endMessageText ?? <Trans>You have reached the end</Trans>}
</Text>
)
) : null} ) : null}
</View> </View>
) )
+8 -1
View File
@@ -47,7 +47,12 @@ export function Root({
return <Context.Provider value={context}>{children}</Context.Provider> return <Context.Provider value={context}>{children}</Context.Provider>
} }
export function Trigger({children, label, role = 'button'}: TriggerProps) { export function Trigger({
children,
label,
role = 'button',
hint,
}: TriggerProps) {
const context = useMenuContext() const context = useMenuContext()
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
const { const {
@@ -65,11 +70,13 @@ export function Trigger({children, label, role = 'button'}: TriggerProps) {
pressed, pressed,
}, },
props: { props: {
ref: null,
onPress: context.control.open, onPress: context.control.open,
onFocus, onFocus,
onBlur, onBlur,
onPressIn, onPressIn,
onPressOut, onPressOut,
accessibilityHint: hint,
accessibilityLabel: label, accessibilityLabel: label,
accessibilityRole: role, accessibilityRole: role,
}, },
+7 -1
View File
@@ -110,7 +110,12 @@ const RadixTriggerPassThrough = React.forwardRef(
) )
RadixTriggerPassThrough.displayName = 'RadixTriggerPassThrough' RadixTriggerPassThrough.displayName = 'RadixTriggerPassThrough'
export function Trigger({children, label, role = 'button'}: TriggerProps) { export function Trigger({
children,
label,
role = 'button',
hint,
}: TriggerProps) {
const {control} = useMenuContext() const {control} = useMenuContext()
const { const {
state: hovered, state: hovered,
@@ -153,6 +158,7 @@ export function Trigger({children, label, role = 'button'}: TriggerProps) {
onBlur: onBlur, onBlur: onBlur,
onMouseEnter, onMouseEnter,
onMouseLeave, onMouseLeave,
accessibilityHint: hint,
accessibilityLabel: label, accessibilityLabel: label,
accessibilityRole: role, accessibilityRole: role,
}, },
+5
View File
@@ -19,6 +19,7 @@ export type ItemContextType = {
} }
export type RadixPassThroughTriggerProps = { export type RadixPassThroughTriggerProps = {
ref: React.RefObject<any>
id: string id: string
type: 'button' type: 'button'
disabled: boolean disabled: boolean
@@ -37,6 +38,7 @@ export type RadixPassThroughTriggerProps = {
export type TriggerProps = { export type TriggerProps = {
children(props: TriggerChildProps): React.ReactNode children(props: TriggerChildProps): React.ReactNode
label: string label: string
hint?: string
role?: AccessibilityRole role?: AccessibilityRole
} }
export type TriggerChildProps = export type TriggerChildProps =
@@ -59,11 +61,13 @@ export type TriggerChildProps =
* object is empty. * object is empty.
*/ */
props: { props: {
ref: null
onPress: () => void onPress: () => void
onFocus: () => void onFocus: () => void
onBlur: () => void onBlur: () => void
onPressIn: () => void onPressIn: () => void
onPressOut: () => void onPressOut: () => void
accessibilityHint?: string
accessibilityLabel: string accessibilityLabel: string
accessibilityRole: AccessibilityRole accessibilityRole: AccessibilityRole
} }
@@ -85,6 +89,7 @@ export type TriggerChildProps =
onBlur: () => void onBlur: () => void
onMouseEnter: () => void onMouseEnter: () => void
onMouseLeave: () => void onMouseLeave: () => void
accessibilityHint?: string
accessibilityLabel: string accessibilityLabel: string
accessibilityRole: AccessibilityRole accessibilityRole: AccessibilityRole
} }
+2 -1
View File
@@ -16,6 +16,7 @@ import * as Dialog from '#/components/Dialog'
import * as Toggle from '#/components/forms/Toggle' import * as Toggle from '#/components/forms/Toggle'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron' import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
import {PaperPlane_Stroke2_Corner0_Rounded as SendIcon} from '#/components/icons/PaperPlane'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {ReportDialogProps} from './types' import {ReportDialogProps} from './types'
@@ -223,7 +224,7 @@ export function SubmitView({
<ButtonText> <ButtonText>
<Trans>Send report</Trans> <Trans>Send report</Trans>
</ButtonText> </ButtonText>
{submitting && <ButtonIcon icon={Loader} />} <ButtonIcon icon={submitting ? Loader : SendIcon} />
</Button> </Button>
</View> </View>
{/* Maybe fix this later -h */} {/* Maybe fix this later -h */}
+13 -91
View File
@@ -1,25 +1,19 @@
import React from 'react' import React from 'react'
import {TextStyle} from 'react-native' import {TextStyle} from 'react-native'
import {AppBskyRichtextFacet, RichText as RichTextAPI} from '@atproto/api' import {AppBskyRichtextFacet, RichText as RichTextAPI} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {NavigationProp} from '#/lib/routes/types'
import {toShortUrl} from '#/lib/strings/url-helpers' import {toShortUrl} from '#/lib/strings/url-helpers'
import {isNative} from '#/platform/detection' import {atoms as a, flatten, TextStyleProp} from '#/alf'
import {atoms as a, flatten, native, TextStyleProp, useTheme, web} from '#/alf'
import {isOnlyEmoji} from '#/alf/typography' import {isOnlyEmoji} from '#/alf/typography'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {InlineLinkText, LinkProps} from '#/components/Link' import {InlineLinkText, LinkProps} from '#/components/Link'
import {ProfileHoverCard} from '#/components/ProfileHoverCard' import {ProfileHoverCard} from '#/components/ProfileHoverCard'
import {TagMenu, useTagMenuControl} from '#/components/TagMenu' import {RichTextTag} from '#/components/RichTextTag'
import {Text, TextProps} from '#/components/Typography' import {Text, TextProps} from '#/components/Typography'
const WORD_WRAP = {wordWrap: 1} const WORD_WRAP = {wordWrap: 1}
export type RichTextProps = TextStyleProp & export type RichTextProps = TextStyleProp &
Pick<TextProps, 'selectable'> & { Pick<TextProps, 'selectable' | 'onLayout' | 'onTextLayout'> & {
value: RichTextAPI | string value: RichTextAPI | string
testID?: string testID?: string
numberOfLines?: number numberOfLines?: number
@@ -43,6 +37,8 @@ export function RichText({
onLinkPress, onLinkPress,
interactiveStyle, interactiveStyle,
emojiMultiplier = 1.85, emojiMultiplier = 1.85,
onLayout,
onTextLayout,
}: RichTextProps) { }: RichTextProps) {
const richText = React.useMemo( const richText = React.useMemo(
() => () =>
@@ -70,6 +66,8 @@ export function RichText({
selectable={selectable} selectable={selectable}
testID={testID} testID={testID}
style={[plainStyles, {fontSize}]} style={[plainStyles, {fontSize}]}
onLayout={onLayout}
onTextLayout={onTextLayout}
// @ts-ignore web only -prf // @ts-ignore web only -prf
dataSet={WORD_WRAP}> dataSet={WORD_WRAP}>
{text} {text}
@@ -83,6 +81,8 @@ export function RichText({
testID={testID} testID={testID}
style={plainStyles} style={plainStyles}
numberOfLines={numberOfLines} numberOfLines={numberOfLines}
onLayout={onLayout}
onTextLayout={onTextLayout}
// @ts-ignore web only -prf // @ts-ignore web only -prf
dataSet={WORD_WRAP}> dataSet={WORD_WRAP}>
{text} {text}
@@ -143,10 +143,9 @@ export function RichText({
els.push( els.push(
<RichTextTag <RichTextTag
key={key} key={key}
text={segment.text} display={segment.text}
tag={tag.tag} tag={tag.tag}
style={interactiveStyles} textStyle={interactiveStyles}
selectable={selectable}
authorHandle={authorHandle} authorHandle={authorHandle}
/>, />,
) )
@@ -163,88 +162,11 @@ export function RichText({
testID={testID} testID={testID}
style={plainStyles} style={plainStyles}
numberOfLines={numberOfLines} numberOfLines={numberOfLines}
onLayout={onLayout}
onTextLayout={onTextLayout}
// @ts-ignore web only -prf // @ts-ignore web only -prf
dataSet={WORD_WRAP}> dataSet={WORD_WRAP}>
{els} {els}
</Text> </Text>
) )
} }
function RichTextTag({
text,
tag,
style,
selectable,
authorHandle,
}: {
text: string
tag: string
selectable?: boolean
authorHandle?: string
} & TextStyleProp) {
const t = useTheme()
const {_} = useLingui()
const control = useTagMenuControl()
const {
state: hovered,
onIn: onHoverIn,
onOut: onHoverOut,
} = useInteractionState()
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
const navigation = useNavigation<NavigationProp>()
const navigateToPage = React.useCallback(() => {
navigation.push('Hashtag', {
tag: encodeURIComponent(tag),
})
}, [navigation, tag])
const openDialog = React.useCallback(() => {
control.open()
}, [control])
/*
* N.B. On web, this is wrapped in another pressable comopnent with a11y
* labels, etc. That's why only some of these props are applied here.
*/
return (
<React.Fragment>
<TagMenu control={control} tag={tag} authorHandle={authorHandle}>
<Text
emoji
selectable={selectable}
{...native({
accessibilityLabel: _(msg`Hashtag: #${tag}`),
accessibilityHint: _(msg`Long press to open tag menu for #${tag}`),
accessibilityRole: isNative ? 'button' : undefined,
onPress: navigateToPage,
onLongPress: openDialog,
})}
{...web({
onMouseEnter: onHoverIn,
onMouseLeave: onHoverOut,
})}
// @ts-ignore
onFocus={onFocus}
onBlur={onBlur}
style={[
web({
cursor: 'pointer',
}),
{color: t.palette.primary_500},
(hovered || focused) && {
...web({
outline: 0,
textDecorationLine: 'underline',
textDecorationColor: t.palette.primary_500,
}),
},
style,
]}>
{text}
</Text>
</TagMenu>
</React.Fragment>
)
}
+160
View File
@@ -0,0 +1,160 @@
import React from 'react'
import {StyleProp, Text as RNText, TextStyle} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {NavigationProp} from '#/lib/routes/types'
import {isInvalidHandle} from '#/lib/strings/handles'
import {isNative, isWeb} from '#/platform/detection'
import {
usePreferencesQuery,
useRemoveMutedWordsMutation,
useUpsertMutedWordsMutation,
} from '#/state/queries/preferences'
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person'
import {
createStaticClick,
createStaticClickIfUnmodified,
InlineLinkText,
} from '#/components/Link'
import {Loader} from '#/components/Loader'
import * as Menu from '#/components/Menu'
export function RichTextTag({
tag,
display,
authorHandle,
textStyle,
}: {
tag: string
display: string
authorHandle?: string
textStyle: StyleProp<TextStyle>
}) {
const {_} = useLingui()
const {isLoading: isPreferencesLoading, data: preferences} =
usePreferencesQuery()
const {
mutateAsync: upsertMutedWord,
variables: optimisticUpsert,
reset: resetUpsert,
} = useUpsertMutedWordsMutation()
const {
mutateAsync: removeMutedWords,
variables: optimisticRemove,
reset: resetRemove,
} = useRemoveMutedWordsMutation()
const navigation = useNavigation<NavigationProp>()
const label = _(msg`Hashtag ${tag}`)
const hint = isNative
? _(msg`Long press to open tag menu for #${tag}`)
: _(msg`Click to open tag menu for ${tag}`)
const isMuted = Boolean(
(preferences?.moderationPrefs.mutedWords?.find(
m => m.value === tag && m.targets.includes('tag'),
) ??
optimisticUpsert?.find(
m => m.value === tag && m.targets.includes('tag'),
)) &&
!optimisticRemove?.find(m => m?.value === tag),
)
/*
* Mute word records that exactly match the tag in question.
*/
const removeableMuteWords = React.useMemo(() => {
return (
preferences?.moderationPrefs.mutedWords?.filter(word => {
return word.value === tag
}) || []
)
}, [tag, preferences?.moderationPrefs?.mutedWords])
return (
<Menu.Root>
<Menu.Trigger label={label} hint={hint}>
{({props: menuProps}) => (
<InlineLinkText
to={{
screen: 'Hashtag',
params: {tag: encodeURIComponent(tag)},
}}
{...menuProps}
onPress={e => {
if (isWeb) {
return createStaticClickIfUnmodified(() => {
if (!isNative) {
menuProps.onPress()
}
}).onPress(e)
}
}}
onLongPress={createStaticClick(menuProps.onPress).onPress}
accessibilityHint={hint}
label={label}
style={textStyle}>
{isNative ? (
display
) : (
<RNText ref={menuProps.ref}>{display}</RNText>
)}
</InlineLinkText>
)}
</Menu.Trigger>
<Menu.Outer>
<Menu.Group>
<Menu.Item
label={_(msg`See ${tag} posts`)}
onPress={() => {
navigation.push('Hashtag', {
tag: encodeURIComponent(tag),
})
}}>
<Menu.ItemText>
<Trans>See #{tag} posts</Trans>
</Menu.ItemText>
<Menu.ItemIcon icon={Search} />
</Menu.Item>
{authorHandle && !isInvalidHandle(authorHandle) && (
<Menu.Item
label={_(msg`See ${tag} posts by user`)}
onPress={() => {
navigation.push('Hashtag', {
tag: encodeURIComponent(tag),
author: authorHandle,
})
}}>
<Menu.ItemText>
<Trans>See #{tag} posts by user</Trans>
</Menu.ItemText>
<Menu.ItemIcon icon={Person} />
</Menu.Item>
)}
</Menu.Group>
<Menu.Divider />
<Menu.Item
label={isMuted ? _(msg`Unmute ${tag}`) : _(msg`Mute ${tag}`)}
onPress={() => {
if (isMuted) {
resetUpsert()
removeMutedWords(removeableMuteWords)
} else {
resetRemove()
upsertMutedWord([
{value: tag, targets: ['tag'], actorTarget: 'all'},
])
}
}}>
<Menu.ItemText>
{isMuted ? _(msg`Unmute ${tag}`) : _(msg`Mute ${tag}`)}
</Menu.ItemText>
<Menu.ItemIcon icon={isPreferencesLoading ? Loader : Mute} />
</Menu.Item>
</Menu.Outer>
</Menu.Root>
)
}
@@ -136,7 +136,6 @@ export const ProfileStarterPacks = React.forwardRef<
headerOffset={headerOffset} headerOffset={headerOffset}
progressViewOffset={ios(0)} progressViewOffset={ios(0)}
contentContainerStyle={{paddingBottom: headerOffset + bottomBarOffset}} contentContainerStyle={{paddingBottom: headerOffset + bottomBarOffset}}
indicatorStyle={t.name === 'light' ? 'black' : 'white'}
removeClippedSubviews={true} removeClippedSubviews={true}
desktopFixedHeight desktopFixedHeight
onEndReached={onEndReached} onEndReached={onEndReached}
-290
View File
@@ -1,290 +0,0 @@
import React from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {NavigationProp} from '#/lib/routes/types'
import {isInvalidHandle} from '#/lib/strings/handles'
import {
usePreferencesQuery,
useRemoveMutedWordsMutation,
useUpsertMutedWordsMutation,
} from '#/state/queries/preferences'
import {atoms as a, native, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {Divider} from '#/components/Divider'
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person'
import {createStaticClick, Link} from '#/components/Link'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
export function useTagMenuControl() {
return Dialog.useDialogControl()
}
export function TagMenu({
children,
control,
tag,
authorHandle,
}: React.PropsWithChildren<{
control: Dialog.DialogOuterProps['control']
/**
* This should be the sanitized tag value from the facet itself, not the
* "display" value with a leading `#`.
*/
tag: string
authorHandle?: string
}>) {
const navigation = useNavigation<NavigationProp>()
return (
<>
{children}
<Dialog.Outer control={control}>
<Dialog.Handle />
<TagMenuInner
control={control}
tag={tag}
authorHandle={authorHandle}
navigation={navigation}
/>
</Dialog.Outer>
</>
)
}
function TagMenuInner({
control,
tag,
authorHandle,
navigation,
}: {
control: Dialog.DialogOuterProps['control']
tag: string
authorHandle?: string
// Passed down because on native, we don't use real portals (and context would be wrong).
navigation: NavigationProp
}) {
const {_} = useLingui()
const t = useTheme()
const {isLoading: isPreferencesLoading, data: preferences} =
usePreferencesQuery()
const {
mutateAsync: upsertMutedWord,
variables: optimisticUpsert,
reset: resetUpsert,
} = useUpsertMutedWordsMutation()
const {
mutateAsync: removeMutedWords,
variables: optimisticRemove,
reset: resetRemove,
} = useRemoveMutedWordsMutation()
const displayTag = '#' + tag
const isMuted = Boolean(
(preferences?.moderationPrefs.mutedWords?.find(
m => m.value === tag && m.targets.includes('tag'),
) ??
optimisticUpsert?.find(
m => m.value === tag && m.targets.includes('tag'),
)) &&
!optimisticRemove?.find(m => m?.value === tag),
)
/*
* Mute word records that exactly match the tag in question.
*/
const removeableMuteWords = React.useMemo(() => {
return (
preferences?.moderationPrefs.mutedWords?.filter(word => {
return word.value === tag
}) || []
)
}, [tag, preferences?.moderationPrefs?.mutedWords])
return (
<Dialog.Inner label={_(msg`Tag menu: ${displayTag}`)}>
{isPreferencesLoading ? (
<View style={[a.w_full, a.align_center]}>
<Loader size="lg" />
</View>
) : (
<>
<View
style={[
a.rounded_md,
a.border,
a.mb_md,
t.atoms.border_contrast_low,
t.atoms.bg_contrast_25,
]}>
<Link
label={_(msg`View all posts with tag ${displayTag}`)}
{...createStaticClick(() => {
control.close(() => {
navigation.push('Hashtag', {
tag: encodeURIComponent(tag),
})
})
})}>
<View
style={[
a.w_full,
a.flex_row,
a.align_center,
a.justify_start,
a.gap_md,
a.px_lg,
a.py_md,
]}>
<Search size="lg" style={[t.atoms.text_contrast_medium]} />
<Text
numberOfLines={1}
ellipsizeMode="middle"
style={[
a.flex_1,
a.text_md,
a.font_bold,
native({top: 2}),
t.atoms.text_contrast_medium,
]}>
<Trans>
See{' '}
<Text style={[a.text_md, a.font_bold, t.atoms.text]}>
{displayTag}
</Text>{' '}
posts
</Trans>
</Text>
</View>
</Link>
{authorHandle && !isInvalidHandle(authorHandle) && (
<>
<Divider />
<Link
label={_(
msg`View all posts by @${authorHandle} with tag ${displayTag}`,
)}
{...createStaticClick(() => {
control.close(() => {
navigation.push('Hashtag', {
tag: encodeURIComponent(tag),
author: authorHandle,
})
})
})}>
<View
style={[
a.w_full,
a.flex_row,
a.align_center,
a.justify_start,
a.gap_md,
a.px_lg,
a.py_md,
]}>
<Person size="lg" style={[t.atoms.text_contrast_medium]} />
<Text
numberOfLines={1}
ellipsizeMode="middle"
style={[
a.flex_1,
a.text_md,
a.font_bold,
native({top: 2}),
t.atoms.text_contrast_medium,
]}>
<Trans>
See{' '}
<Text style={[a.text_md, a.font_bold, t.atoms.text]}>
{displayTag}
</Text>{' '}
posts by this user
</Trans>
</Text>
</View>
</Link>
</>
)}
{preferences ? (
<>
<Divider />
<Button
label={
isMuted
? _(msg`Unmute all ${displayTag} posts`)
: _(msg`Mute all ${displayTag} posts`)
}
onPress={() => {
control.close(() => {
if (isMuted) {
resetUpsert()
removeMutedWords(removeableMuteWords)
} else {
resetRemove()
upsertMutedWord([
{
value: tag,
targets: ['tag'],
actorTarget: 'all',
},
])
}
})
}}>
<View
style={[
a.w_full,
a.flex_row,
a.align_center,
a.justify_start,
a.gap_md,
a.px_lg,
a.py_md,
]}>
<Mute size="lg" style={[t.atoms.text_contrast_medium]} />
<Text
numberOfLines={1}
ellipsizeMode="middle"
style={[
a.flex_1,
a.text_md,
a.font_bold,
native({top: 2}),
t.atoms.text_contrast_medium,
]}>
{isMuted ? _(msg`Unmute`) : _(msg`Mute`)}{' '}
<Text style={[a.text_md, a.font_bold, t.atoms.text]}>
{displayTag}
</Text>{' '}
<Trans>posts</Trans>
</Text>
</View>
</Button>
</>
) : null}
</View>
<Button
label={_(msg`Close this dialog`)}
size="small"
variant="ghost"
color="secondary"
onPress={() => control.close()}>
<ButtonText>
<Trans>Cancel</Trans>
</ButtonText>
</Button>
</>
)}
</Dialog.Inner>
)
}
-163
View File
@@ -1,163 +0,0 @@
import React from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {NavigationProp} from '#/lib/routes/types'
import {isInvalidHandle} from '#/lib/strings/handles'
import {enforceLen} from '#/lib/strings/helpers'
import {
usePreferencesQuery,
useRemoveMutedWordsMutation,
useUpsertMutedWordsMutation,
} from '#/state/queries/preferences'
import {EventStopper} from '#/view/com/util/EventStopper'
import {NativeDropdown} from '#/view/com/util/forms/NativeDropdown'
import {web} from '#/alf'
import * as Dialog from '#/components/Dialog'
export function useTagMenuControl(): Dialog.DialogControlProps {
return {
id: '',
// @ts-ignore
ref: null,
open: () => {
throw new Error(`TagMenu controls are only available on native platforms`)
},
close: () => {
throw new Error(`TagMenu controls are only available on native platforms`)
},
}
}
export function TagMenu({
children,
tag,
authorHandle,
}: React.PropsWithChildren<{
/**
* This should be the sanitized tag value from the facet itself, not the
* "display" value with a leading `#`.
*/
tag: string
authorHandle?: string
}>) {
const {_} = useLingui()
const navigation = useNavigation<NavigationProp>()
const {data: preferences} = usePreferencesQuery()
const {mutateAsync: upsertMutedWord, variables: optimisticUpsert} =
useUpsertMutedWordsMutation()
const {mutateAsync: removeMutedWords, variables: optimisticRemove} =
useRemoveMutedWordsMutation()
const isMuted = Boolean(
(preferences?.moderationPrefs.mutedWords?.find(
m => m.value === tag && m.targets.includes('tag'),
) ??
optimisticUpsert?.find(
m => m.value === tag && m.targets.includes('tag'),
)) &&
!optimisticRemove?.find(m => m?.value === tag),
)
const truncatedTag = '#' + enforceLen(tag, 15, true, 'middle')
/*
* Mute word records that exactly match the tag in question.
*/
const removeableMuteWords = React.useMemo(() => {
return (
preferences?.moderationPrefs.mutedWords?.filter(word => {
return word.value === tag
}) || []
)
}, [tag, preferences?.moderationPrefs?.mutedWords])
const dropdownItems = React.useMemo(() => {
return [
{
label: _(msg`See ${truncatedTag} posts`),
onPress() {
navigation.push('Hashtag', {
tag: encodeURIComponent(tag),
})
},
testID: 'tagMenuSearch',
icon: {
ios: {
name: 'magnifyingglass',
},
android: '',
web: 'magnifying-glass',
},
},
authorHandle &&
!isInvalidHandle(authorHandle) && {
label: _(msg`See ${truncatedTag} posts by user`),
onPress() {
navigation.push('Hashtag', {
tag: encodeURIComponent(tag),
author: authorHandle,
})
},
testID: 'tagMenuSearchByUser',
icon: {
ios: {
name: 'magnifyingglass',
},
android: '',
web: ['far', 'user'],
},
},
preferences && {
label: 'separator',
},
preferences && {
label: isMuted
? _(msg`Unmute ${truncatedTag}`)
: _(msg`Mute ${truncatedTag}`),
onPress() {
if (isMuted) {
removeMutedWords(removeableMuteWords)
} else {
upsertMutedWord([
{value: tag, targets: ['tag'], actorTarget: 'all'},
])
}
},
testID: 'tagMenuMute',
icon: {
ios: {
name: 'speaker.slash',
},
android: 'ic_menu_sort_alphabetically',
web: isMuted ? 'eye' : ['far', 'eye-slash'],
},
},
].filter(Boolean)
}, [
_,
authorHandle,
isMuted,
navigation,
preferences,
tag,
truncatedTag,
upsertMutedWord,
removeMutedWords,
removeableMuteWords,
])
return (
<EventStopper>
<NativeDropdown
accessibilityLabel={_(msg`Click here to open tag menu for ${tag}`)}
accessibilityHint=""
// @ts-ignore
items={dropdownItems}
triggerStyle={web({
textAlign: 'left',
})}>
{children}
</NativeDropdown>
</EventStopper>
)
}
+540
View File
@@ -0,0 +1,540 @@
import {useMemo} from 'react'
import {View} from 'react-native'
import {Image} from 'expo-image'
import {LinearGradient} from 'expo-linear-gradient'
import {
AppBskyActorDefs,
AppBskyEmbedVideo,
AppBskyFeedDefs,
AppBskyFeedPost,
ModerationDecision,
} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {sanitizeHandle} from '#/lib/strings/handles'
import {formatCount} from '#/view/com/util/numeric/format'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {VideoFeedSourceContext} from '#/screens/VideoFeed/types'
import {atoms as a, useTheme} from '#/alf'
import {BLUE_HUE} from '#/alf/util/colorGeneration'
import {select} from '#/alf/util/themeSelector'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {EyeSlash_Stroke2_Corner0_Rounded as Eye} from '#/components/icons/EyeSlash'
import {Heart2_Stroke2_Corner0_Rounded as Heart} from '#/components/icons/Heart2'
import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost'
import {Link} from '#/components/Link'
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
import * as Hider from '#/components/moderation/Hider'
import {Text} from '#/components/Typography'
function getBlackColor(t: ReturnType<typeof useTheme>) {
return select(t.name, {
light: t.palette.black,
dark: t.atoms.bg_contrast_25.backgroundColor,
dim: `hsl(${BLUE_HUE}, 28%, 6%)`,
})
}
export function VideoPostCard({
post,
sourceContext,
moderation,
onInteract,
}: {
post: AppBskyFeedDefs.PostView
sourceContext: VideoFeedSourceContext
moderation: ModerationDecision
/**
* Callback for metrics etc
*/
onInteract?: () => void
}) {
const t = useTheme()
const {_, i18n} = useLingui()
const embed = post.embed
const {
state: pressed,
onIn: onPressIn,
onOut: onPressOut,
} = useInteractionState()
const listModUi = moderation.ui('contentList')
const mergedModui = useMemo(() => {
const modui = moderation.ui('contentList')
const mediaModui = moderation.ui('contentMedia')
modui.alerts = [...modui.alerts, ...mediaModui.alerts]
modui.blurs = [...modui.blurs, ...mediaModui.blurs]
modui.filters = [...modui.filters, ...mediaModui.filters]
modui.informs = [...modui.informs, ...mediaModui.informs]
return modui
}, [moderation])
/**
* Filtering should be done at a higher level, such as `PostFeed` or
* `PostFeedVideoGridRow`, but we need to protect here as well.
*/
if (!AppBskyEmbedVideo.isView(embed)) return null
const author = post.author
const text = AppBskyFeedPost.isRecord(post.record) ? post.record?.text : ''
const likeCount = post?.likeCount ?? 0
const repostCount = post?.repostCount ?? 0
const {thumbnail} = embed
const black = getBlackColor(t)
const textAndAuthor = (
<View style={[a.pr_xs, {paddingTop: 6, gap: 4}]}>
{text && (
<Text style={[a.text_md, a.leading_snug]} numberOfLines={2} emoji>
{text}
</Text>
)}
<View style={[a.flex_row, a.gap_xs, a.align_center]}>
<View style={[a.relative, a.rounded_full, {width: 20, height: 20}]}>
<UserAvatar type="user" size={20} avatar={post.author.avatar} />
<MediaInsetBorder />
</View>
<Text
style={[
a.flex_1,
a.text_sm,
a.leading_tight,
t.atoms.text_contrast_medium,
]}
numberOfLines={1}>
{sanitizeHandle(post.author.handle, '@')}
</Text>
</View>
</View>
)
return (
<Link
accessibilityHint={_(msg`Tap to view video in immersive mode.`)}
label={_(msg`Video from ${author.handle}: ${text}`)}
to={{
screen: 'VideoFeed',
params: {
...sourceContext,
initialPostUri: post.uri,
},
}}
onPress={() => {
onInteract?.()
}}
onPressIn={onPressIn}
onPressOut={onPressOut}
style={[
a.flex_col,
{
alignItems: undefined,
justifyContent: undefined,
},
]}>
<Hider.Outer modui={mergedModui}>
<Hider.Mask>
<View
style={[
a.justify_center,
a.rounded_md,
a.overflow_hidden,
{
backgroundColor: black,
aspectRatio: 9 / 16,
},
]}>
<Image
source={{uri: thumbnail}}
style={[a.w_full, a.h_full, {opacity: pressed ? 0.8 : 1}]}
accessibilityIgnoresInvertColors
blurRadius={100}
/>
<MediaInsetBorder />
<View
style={[a.absolute, a.inset_0, a.justify_center, a.align_center]}>
<View
style={[
a.absolute,
a.inset_0,
a.justify_center,
a.align_center,
{
backgroundColor: 'black',
opacity: 0.2,
},
]}
/>
<View style={[a.align_center, a.gap_xs]}>
<Eye size="lg" fill="white" />
<Text style={[a.text_sm, {color: 'white'}]}>
{_(msg`Hidden`)}
</Text>
</View>
</View>
</View>
{listModUi.blur ? (
<VideoPostCardTextPlaceholder author={post.author} />
) : (
textAndAuthor
)}
</Hider.Mask>
<Hider.Content>
<View
style={[
a.justify_center,
a.rounded_md,
a.overflow_hidden,
{
backgroundColor: black,
aspectRatio: 9 / 16,
},
]}>
<Image
source={{uri: thumbnail}}
style={[a.w_full, a.h_full, {opacity: pressed ? 0.8 : 1}]}
accessibilityIgnoresInvertColors
/>
<MediaInsetBorder />
<View style={[a.absolute, a.inset_0]}>
<View
style={[
a.absolute,
a.inset_0,
a.pt_2xl,
{
top: 'auto',
},
]}>
<LinearGradient
colors={[black, 'rgba(0, 0, 0, 0)']}
locations={[0.02, 1]}
start={{x: 0, y: 1}}
end={{x: 0, y: 0}}
style={[a.absolute, a.inset_0, {opacity: 0.9}]}
/>
<View
style={[a.relative, a.z_10, a.p_md, a.flex_row, a.gap_md]}>
{likeCount > 0 && (
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
<Heart size="sm" fill="white" />
<Text style={[a.text_sm, a.font_bold, {color: 'white'}]}>
{formatCount(i18n, likeCount)}
</Text>
</View>
)}
{repostCount > 0 && (
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
<Repost size="sm" fill="white" />
<Text style={[a.text_sm, a.font_bold, {color: 'white'}]}>
{formatCount(i18n, repostCount)}
</Text>
</View>
)}
</View>
</View>
</View>
</View>
{textAndAuthor}
</Hider.Content>
</Hider.Outer>
</Link>
)
}
export function VideoPostCardPlaceholder() {
const t = useTheme()
const black = getBlackColor(t)
return (
<View style={[a.flex_1]}>
<View
style={[
a.rounded_md,
a.overflow_hidden,
{
backgroundColor: black,
aspectRatio: 9 / 16,
},
]}>
<MediaInsetBorder />
</View>
<VideoPostCardTextPlaceholder />
</View>
)
}
export function VideoPostCardTextPlaceholder({
author,
}: {
author?: AppBskyActorDefs.ProfileViewBasic
}) {
const t = useTheme()
return (
<View style={[a.flex_1]}>
<View style={[a.pr_xs, {paddingTop: 8, gap: 6}]}>
<View
style={[
a.w_full,
a.rounded_xs,
t.atoms.bg_contrast_50,
{
height: 14,
},
]}
/>
<View
style={[
a.w_full,
a.rounded_xs,
t.atoms.bg_contrast_50,
{
height: 14,
width: '70%',
},
]}
/>
{author ? (
<View style={[a.flex_row, a.gap_xs, a.align_center]}>
<View style={[a.relative, a.rounded_full, {width: 20, height: 20}]}>
<UserAvatar type="user" size={20} avatar={author.avatar} />
<MediaInsetBorder />
</View>
<Text
style={[
a.flex_1,
a.text_sm,
a.leading_tight,
t.atoms.text_contrast_medium,
]}
numberOfLines={1}>
{sanitizeHandle(author.handle, '@')}
</Text>
</View>
) : (
<View style={[a.flex_row, a.gap_xs, a.align_center]}>
<View
style={[
a.rounded_full,
t.atoms.bg_contrast_50,
{
width: 20,
height: 20,
},
]}
/>
<View
style={[
a.rounded_xs,
t.atoms.bg_contrast_25,
{
height: 12,
width: '75%',
},
]}
/>
</View>
)}
</View>
</View>
)
}
export function CompactVideoPostCard({
post,
sourceContext,
moderation,
onInteract,
}: {
post: AppBskyFeedDefs.PostView
sourceContext: VideoFeedSourceContext
moderation: ModerationDecision
/**
* Callback for metrics etc
*/
onInteract?: () => void
}) {
const t = useTheme()
const {_, i18n} = useLingui()
const embed = post.embed
const {
state: pressed,
onIn: onPressIn,
onOut: onPressOut,
} = useInteractionState()
const mergedModui = useMemo(() => {
const modui = moderation.ui('contentList')
const mediaModui = moderation.ui('contentMedia')
modui.alerts = [...modui.alerts, ...mediaModui.alerts]
modui.blurs = [...modui.blurs, ...mediaModui.blurs]
modui.filters = [...modui.filters, ...mediaModui.filters]
modui.informs = [...modui.informs, ...mediaModui.informs]
return modui
}, [moderation])
/**
* Filtering should be done at a higher level, such as `PostFeed` or
* `PostFeedVideoGridRow`, but we need to protect here as well.
*/
if (!AppBskyEmbedVideo.isView(embed)) return null
const likeCount = post?.likeCount ?? 0
const {thumbnail} = embed
const black = getBlackColor(t)
return (
<Link
label={_(msg`View video`)}
to={{
screen: 'VideoFeed',
params: {
...sourceContext,
initialPostUri: post.uri,
},
}}
onPress={() => {
onInteract?.()
}}
onPressIn={onPressIn}
onPressOut={onPressOut}
style={[
a.flex_col,
{
alignItems: undefined,
justifyContent: undefined,
},
]}>
<Hider.Outer modui={mergedModui}>
<Hider.Mask>
<View
style={[
a.justify_center,
a.rounded_md,
a.overflow_hidden,
{
backgroundColor: black,
aspectRatio: 9 / 16,
},
]}>
<Image
source={{uri: thumbnail}}
style={[a.w_full, a.h_full, {opacity: pressed ? 0.8 : 1}]}
accessibilityIgnoresInvertColors
blurRadius={100}
/>
<MediaInsetBorder />
<View
style={[a.absolute, a.inset_0, a.justify_center, a.align_center]}>
<View
style={[
a.absolute,
a.inset_0,
a.justify_center,
a.align_center,
{
backgroundColor: 'black',
opacity: 0.2,
},
]}
/>
<View style={[a.align_center, a.gap_xs]}>
<Eye size="lg" fill="white" />
<Text style={[a.text_sm, {color: 'white'}]}>
{_(msg`Hidden`)}
</Text>
</View>
</View>
</View>
</Hider.Mask>
<Hider.Content>
<View
style={[
a.justify_center,
a.rounded_md,
a.overflow_hidden,
{
backgroundColor: black,
aspectRatio: 9 / 16,
},
]}>
<Image
source={{uri: thumbnail}}
style={[a.w_full, a.h_full, {opacity: pressed ? 0.8 : 1}]}
accessibilityIgnoresInvertColors
/>
<MediaInsetBorder />
<View style={[a.absolute, a.inset_0]}>
<View style={[a.absolute, a.inset_0, a.p_sm, {bottom: 'auto'}]}>
<View
style={[a.relative, a.rounded_full, {width: 20, height: 20}]}>
<UserAvatar
type="user"
size={20}
avatar={post.author.avatar}
/>
<MediaInsetBorder />
</View>
</View>
<View
style={[
a.absolute,
a.inset_0,
a.pt_2xl,
{
top: 'auto',
},
]}>
<LinearGradient
colors={[black, 'rgba(0, 0, 0, 0)']}
locations={[0.02, 1]}
start={{x: 0, y: 1}}
end={{x: 0, y: 0}}
style={[a.absolute, a.inset_0, {opacity: 0.9}]}
/>
<View
style={[a.relative, a.z_10, a.p_sm, a.flex_row, a.gap_md]}>
{likeCount > 0 && (
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
<Heart size="sm" fill="white" />
<Text style={[a.text_sm, a.font_bold, {color: 'white'}]}>
{formatCount(i18n, likeCount)}
</Text>
</View>
)}
</View>
</View>
</View>
</View>
</Hider.Content>
</Hider.Outer>
</Link>
)
}
export function CompactVideoPostCardPlaceholder() {
const t = useTheme()
const black = getBlackColor(t)
return (
<View style={[a.flex_1]}>
<View
style={[
a.rounded_md,
a.overflow_hidden,
{
backgroundColor: black,
aspectRatio: 9 / 16,
},
]}>
<MediaInsetBorder />
</View>
</View>
)
}
+1
View File
@@ -227,6 +227,7 @@ let ConvoMenu = ({
/> />
{latestReportableMessage ? ( {latestReportableMessage ? (
<ReportDialog <ReportDialog
currentScreen={currentScreen}
params={{ params={{
type: 'convoMessage', type: 'convoMessage',
convoId: convo.id, convoId: convo.id,
+4 -9
View File
@@ -1,6 +1,6 @@
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native' import {StackActions, useNavigation} from '@react-navigation/native'
import {NavigationProp} from '#/lib/routes/types' import {NavigationProp} from '#/lib/routes/types'
import {isNative} from '#/platform/detection' import {isNative} from '#/platform/detection'
@@ -22,15 +22,10 @@ export function LeaveConvoPrompt({
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const {mutate: leaveConvo} = useLeaveConvo(convoId, { const {mutate: leaveConvo} = useLeaveConvo(convoId, {
onSuccess: () => { onMutate: () => {
if (currentScreen === 'conversation') { if (currentScreen === 'conversation') {
navigation.replace( navigation.dispatch(
'Messages', StackActions.replace('Messages', isNative ? {animation: 'pop'} : {}),
isNative
? {
animation: 'pop',
}
: {},
) )
} }
}, },
+1
View File
@@ -138,6 +138,7 @@ export let MessageMenu = ({
</Menu.Root> </Menu.Root>
<ReportDialog <ReportDialog
currentScreen="conversation"
params={{type: 'convoMessage', convoId: convo.convo.id, message}} params={{type: 'convoMessage', convoId: convo.convo.id, message}}
control={reportControl} control={reportControl}
/> />
+170 -22
View File
@@ -1,27 +1,39 @@
import React, {memo, useMemo, useState} from 'react' import React, {memo, useMemo, useState} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import { import {
AppBskyActorDefs,
ChatBskyConvoDefs, ChatBskyConvoDefs,
ComAtprotoModerationCreateReport, ComAtprotoModerationCreateReport,
RichText as RichTextAPI, RichText as RichTextAPI,
} from '@atproto/api' } from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {StackActions, useNavigation} from '@react-navigation/native'
import {useMutation} from '@tanstack/react-query' import {useMutation} from '@tanstack/react-query'
import {ReportOption} from '#/lib/moderation/useReportOptions' import {ReportOption} from '#/lib/moderation/useReportOptions'
import {NavigationProp} from '#/lib/routes/types'
import {isNative} from '#/platform/detection'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {useLeaveConvo} from '#/state/queries/messages/leave-conversation'
import {
useProfileBlockMutationQueue,
useProfileQuery,
} from '#/state/queries/profile'
import {useAgent} from '#/state/session' import {useAgent} from '#/state/session'
import {CharProgress} from '#/view/com/composer/char-progress/CharProgress' import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
import * as Toast from '#/view/com/util/Toast' import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {atoms as a, platform, useBreakpoints, useTheme, web} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
import {Button, ButtonIcon, ButtonText} from '../Button' import {Divider} from '#/components/Divider'
import {Divider} from '../Divider' import * as Toggle from '#/components/forms/Toggle'
import {ChevronLeft_Stroke2_Corner0_Rounded as Chevron} from '../icons/Chevron' import {ChevronLeft_Stroke2_Corner0_Rounded as Chevron} from '#/components/icons/Chevron'
import {Loader} from '../Loader' import {PaperPlane_Stroke2_Corner0_Rounded as SendIcon} from '#/components/icons/PaperPlane'
import {SelectReportOptionView} from '../ReportDialog/SelectReportOptionView' import {Loader} from '#/components/Loader'
import {RichText} from '../RichText' import {SelectReportOptionView} from '#/components/ReportDialog/SelectReportOptionView'
import {Text} from '../Typography' import {RichText} from '#/components/RichText'
import {Text} from '#/components/Typography'
import {MessageItemMetadata} from './MessageItem' import {MessageItemMetadata} from './MessageItem'
type ReportDialogParams = { type ReportDialogParams = {
@@ -33,16 +45,18 @@ type ReportDialogParams = {
let ReportDialog = ({ let ReportDialog = ({
control, control,
params, params,
currentScreen,
}: { }: {
control: Dialog.DialogControlProps control: Dialog.DialogControlProps
params: ReportDialogParams params: ReportDialogParams
currentScreen: 'list' | 'conversation'
}): React.ReactNode => { }): React.ReactNode => {
const {_} = useLingui() const {_} = useLingui()
return ( return (
<Dialog.Outer control={control}> <Dialog.Outer control={control}>
<Dialog.Handle /> <Dialog.Handle />
<Dialog.ScrollableInner label={_(msg`Report this message`)}> <Dialog.ScrollableInner label={_(msg`Report this message`)}>
<DialogInner params={params} /> <DialogInner params={params} currentScreen={currentScreen} />
<Dialog.Close /> <Dialog.Close />
</Dialog.ScrollableInner> </Dialog.ScrollableInner>
</Dialog.Outer> </Dialog.Outer>
@@ -51,14 +65,44 @@ let ReportDialog = ({
ReportDialog = memo(ReportDialog) ReportDialog = memo(ReportDialog)
export {ReportDialog} export {ReportDialog}
function DialogInner({params}: {params: ReportDialogParams}) { function DialogInner({
params,
currentScreen,
}: {
params: ReportDialogParams
currentScreen: 'list' | 'conversation'
}) {
const {data: profile, isError} = useProfileQuery({
did: params.message.sender.did,
})
const [reportOption, setReportOption] = useState<ReportOption | null>(null) const [reportOption, setReportOption] = useState<ReportOption | null>(null)
const [done, setDone] = useState(false)
const control = Dialog.useDialogContext()
return reportOption ? ( return done ? (
profile ? (
<DoneStep
convoId={params.convoId}
currentScreen={currentScreen}
profile={profile}
/>
) : (
<View style={[a.w_full, a.py_5xl, a.align_center]}>
<Loader />
</View>
)
) : reportOption ? (
<SubmitStep <SubmitStep
params={params} params={params}
reportOption={reportOption} reportOption={reportOption}
goBack={() => setReportOption(null)} goBack={() => setReportOption(null)}
onComplete={() => {
if (isError) {
control.close()
} else {
setDone(true)
}
}}
/> />
) : ( ) : (
<ReasonStep params={params} setReportOption={setReportOption} /> <ReasonStep params={params} setReportOption={setReportOption} />
@@ -89,16 +133,17 @@ function SubmitStep({
params, params,
reportOption, reportOption,
goBack, goBack,
onComplete,
}: { }: {
params: ReportDialogParams params: ReportDialogParams
reportOption: ReportOption reportOption: ReportOption
goBack: () => void goBack: () => void
onComplete: () => void
}) { }) {
const {_} = useLingui() const {_} = useLingui()
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const t = useTheme() const t = useTheme()
const [details, setDetails] = useState('') const [details, setDetails] = useState('')
const control = Dialog.useDialogContext()
const agent = useAgent() const agent = useAgent()
const { const {
@@ -124,11 +169,7 @@ function SubmitStep({
await agent.createModerationReport(report) await agent.createModerationReport(report)
} }
}, },
onSuccess: () => { onSuccess: onComplete,
control.close(() => {
Toast.show(_(msg`Thank you. Your report has been sent.`))
})
},
}) })
const copy = useMemo(() => { const copy = useMemo(() => {
@@ -181,13 +222,12 @@ function SubmitStep({
<View style={[a.relative, a.w_full]}> <View style={[a.relative, a.w_full]}>
<Dialog.Input <Dialog.Input
multiline multiline
value={details} defaultValue={details}
onChangeText={setDetails} onChangeText={setDetails}
label="Text field" label={_(msg`Text field`)}
style={{paddingRight: 60}} style={{paddingRight: 60}}
numberOfLines={6} numberOfLines={5}
/> />
<View <View
style={[ style={[
a.absolute, a.absolute,
@@ -231,7 +271,115 @@ function SubmitStep({
<ButtonText> <ButtonText>
<Trans>Send report</Trans> <Trans>Send report</Trans>
</ButtonText> </ButtonText>
{submitting && <ButtonIcon icon={Loader} />} <ButtonIcon icon={submitting ? Loader : SendIcon} />
</Button>
</View>
</View>
)
}
function DoneStep({
convoId,
currentScreen,
profile,
}: {
convoId: string
currentScreen: 'list' | 'conversation'
profile: AppBskyActorDefs.ProfileViewBasic
}) {
const {_} = useLingui()
const navigation = useNavigation<NavigationProp>()
const control = Dialog.useDialogContext()
const {gtMobile} = useBreakpoints()
const t = useTheme()
const [actions, setActions] = useState<string[]>(['block', 'leave'])
const shadow = useProfileShadow(profile)
const [queueBlock] = useProfileBlockMutationQueue(shadow)
const {mutate: leaveConvo} = useLeaveConvo(convoId, {
onMutate: () => {
if (currentScreen === 'conversation') {
navigation.dispatch(
StackActions.replace('Messages', isNative ? {animation: 'pop'} : {}),
)
}
},
onError: () => {
Toast.show(_(msg`Could not leave chat`), 'xmark')
},
})
const onPressPrimaryAction = () => {
control.close(() => {
if (actions.includes('block')) {
queueBlock()
}
if (actions.includes('leave')) {
leaveConvo()
}
})
}
let btnText = _(msg`Done`)
if (actions.includes('leave') && actions.includes('block')) {
btnText = _(msg`Block and Delete`)
} else if (actions.includes('leave')) {
btnText = _(msg`Delete Conversation`)
} else if (actions.includes('block')) {
btnText = _(msg`Block User`)
}
return (
<View style={a.gap_2xl}>
<View style={[a.justify_center, gtMobile ? a.gap_sm : a.gap_xs]}>
<Text style={[a.text_2xl, a.font_bold]}>
<Trans>Report submitted</Trans>
</Text>
<Text style={[a.text_md, t.atoms.text_contrast_medium]}>
<Trans>Our moderation team has recieved your report.</Trans>
</Text>
</View>
<Toggle.Group
label={_(msg`Block and/or delete this conversation`)}
values={actions}
onChange={setActions}>
<View style={[a.gap_md]}>
<Toggle.Item name="block" label={_(msg`Block user`)}>
<Toggle.Checkbox />
<Toggle.LabelText style={[a.text_md]}>
<Trans>Block user</Trans>
</Toggle.LabelText>
</Toggle.Item>
<Toggle.Item name="leave" label={_(msg`Delete conversation`)}>
<Toggle.Checkbox />
<Toggle.LabelText style={[a.text_md]}>
<Trans>Delete conversation</Trans>
</Toggle.LabelText>
</Toggle.Item>
</View>
</Toggle.Group>
<View style={[a.gap_md, web([a.flex_row_reverse])]}>
<Button
label={btnText}
onPress={onPressPrimaryAction}
size="large"
variant="solid"
color={actions.length > 0 ? 'negative' : 'primary'}>
<ButtonText>{btnText}</ButtonText>
</Button>
<Button
label={_(msg`Close`)}
onPress={() => control.close()}
size={platform({native: 'small', web: 'large'})}
variant={platform({
native: 'ghost',
web: 'solid',
})}
color="secondary">
<ButtonText>
<Trans>Close</Trans>
</ButtonText>
</Button> </Button>
</View> </View>
</View> </View>
@@ -0,0 +1,67 @@
import {View} from 'react-native'
import {AppBskyEmbedVideo} from '@atproto/api'
import {logEvent} from '#/lib/statsig/statsig'
import {FeedPostSliceItem} from '#/state/queries/post-feed'
import {VideoFeedSourceContext} from '#/screens/VideoFeed/types'
import {atoms as a, useGutters} from '#/alf'
import * as Grid from '#/components/Grid'
import {
VideoPostCard,
VideoPostCardPlaceholder,
} from '#/components/VideoPostCard'
export function PostFeedVideoGridRow({
items: slices,
sourceContext,
}: {
items: FeedPostSliceItem[]
sourceContext: VideoFeedSourceContext
}) {
const gutters = useGutters(['base', 'base', 0, 'base'])
const posts = slices
.filter(slice => AppBskyEmbedVideo.isView(slice.post.embed))
.map(slice => ({
post: slice.post,
moderation: slice.moderation,
}))
/**
* This should not happen because we should be filtering out posts without
* videos within the `PostFeed` component.
*/
if (posts.length !== slices.length) return null
return (
<View style={[gutters]}>
<View style={[a.flex_row, a.gap_sm]}>
<Grid.Row gap={a.gap_sm.gap}>
{posts.map(post => (
<Grid.Col key={post.post.uri} width={1 / 2}>
<VideoPostCard
post={post.post}
sourceContext={sourceContext}
moderation={post.moderation}
onInteract={() => {
logEvent('videoCard:click', {context: 'feed'})
}}
/>
</Grid.Col>
))}
</Grid.Row>
</View>
</View>
)
}
export function PostFeedVideoGridRowPlaceholder() {
const gutters = useGutters(['base', 'base', 0, 'base'])
return (
<View style={[gutters]}>
<View style={[a.flex_row, a.gap_sm]}>
<VideoPostCardPlaceholder />
<VideoPostCardPlaceholder />
</View>
</View>
)
}
+6
View File
@@ -11,10 +11,12 @@ import React from 'react'
import {formatDistance, Locale} from 'date-fns' import {formatDistance, Locale} from 'date-fns'
import { import {
ca, ca,
da,
de, de,
el, el,
enGB, enGB,
es, es,
eu,
fi, fi,
fr, fr,
gl, gl,
@@ -30,6 +32,7 @@ import {
ptBR, ptBR,
ro, ro,
ru, ru,
sv,
th, th,
tr, tr,
uk, uk,
@@ -50,10 +53,12 @@ const locales: Record<AppLanguage, Locale | undefined> = {
an: undefined, an: undefined,
ast: undefined, ast: undefined,
ca, ca,
da,
de, de,
el, el,
['en-GB']: enGB, ['en-GB']: enGB,
es, es,
eu,
fi, fi,
fr, fr,
ga: undefined, ga: undefined,
@@ -71,6 +76,7 @@ const locales: Record<AppLanguage, Locale | undefined> = {
['pt-BR']: ptBR, ['pt-BR']: ptBR,
ro, ro,
ru, ru,
sv,
th, th,
tr, tr,
uk, uk,
+4 -9
View File
@@ -1,7 +1,5 @@
import React, {useContext} from 'react' import React from 'react'
import {ScrollView, View} from 'react-native' import {ScrollView, View} from 'react-native'
import {DrawerGestureContext} from 'react-native-drawer-layout'
import {Gesture, GestureDetector} from 'react-native-gesture-handler'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@@ -13,6 +11,7 @@ import {
import {useTrendingTopics} from '#/state/queries/trending/useTrendingTopics' import {useTrendingTopics} from '#/state/queries/trending/useTrendingTopics'
import {useTrendingConfig} from '#/state/trending-config' import {useTrendingConfig} from '#/state/trending-config'
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
import {atoms as a, useGutters, useTheme} from '#/alf' import {atoms as a, useGutters, useTheme} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button' import {Button, ButtonIcon} from '#/components/Button'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
@@ -41,13 +40,9 @@ export function Inner() {
setTrendingDisabled(true) setTrendingDisabled(true)
}, [setTrendingDisabled]) }, [setTrendingDisabled])
const drawerGesture = useContext(DrawerGestureContext) ?? Gesture.Native() // noop for web
const trendingScrollGesture =
Gesture.Native().blocksExternalGesture(drawerGesture)
return error || noTopics ? null : ( return error || noTopics ? null : (
<View style={[t.atoms.border_contrast_low, a.border_t]}> <View style={[t.atoms.border_contrast_low, a.border_t]}>
<GestureDetector gesture={trendingScrollGesture}> <BlockDrawerGesture>
<ScrollView <ScrollView
horizontal horizontal
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}
@@ -127,7 +122,7 @@ export function Inner() {
)} )}
</View> </View>
</ScrollView> </ScrollView>
</GestureDetector> </BlockDrawerGesture>
<Prompt.Basic <Prompt.Basic
control={trendingPrompt} control={trendingPrompt}
@@ -0,0 +1,231 @@
import React, {useEffect} from 'react'
import {ScrollView, View} from 'react-native'
import {AppBskyEmbedVideo, AtUri} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import {VIDEO_FEED_URI} from '#/lib/constants'
import {makeCustomFeedLink} from '#/lib/routes/links'
import {logEvent} from '#/lib/statsig/statsig'
import {useTrendingSettingsApi} from '#/state/preferences/trending'
import {usePostFeedQuery} from '#/state/queries/post-feed'
import {RQKEY} from '#/state/queries/post-feed'
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
import {atoms as a, useGutters, useTheme} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending2'
import {Link} from '#/components/Link'
import * as Prompt from '#/components/Prompt'
import {Text} from '#/components/Typography'
import {
CompactVideoPostCard,
CompactVideoPostCardPlaceholder,
} from '#/components/VideoPostCard'
const CARD_WIDTH = 100
const FEED_DESC = `feedgen|${VIDEO_FEED_URI}`
const FEED_PARAMS: {
feedCacheKey: 'discover'
} = {
feedCacheKey: 'discover',
}
export function TrendingVideos() {
const t = useTheme()
const {_} = useLingui()
const gutters = useGutters([0, 'base'])
const {data, isLoading, error} = usePostFeedQuery(FEED_DESC, FEED_PARAMS)
// Refetch on unmount if nothing else is using this query.
const queryClient = useQueryClient()
useEffect(() => {
return () => {
const query = queryClient
.getQueryCache()
.find({queryKey: RQKEY(FEED_DESC, FEED_PARAMS)})
if (query && query.getObserversCount() <= 1) {
query.fetch()
}
}
}, [queryClient])
const {setTrendingVideoDisabled} = useTrendingSettingsApi()
const trendingPrompt = Prompt.usePromptControl()
const onConfirmHide = React.useCallback(() => {
setTrendingVideoDisabled(true)
logEvent('trendingVideos:hide', {context: 'interstitial:discover'})
}, [setTrendingVideoDisabled])
if (error) {
return null
}
return (
<View
style={[
a.pt_lg,
a.pb_lg,
a.border_t,
t.atoms.border_contrast_low,
t.atoms.bg_contrast_25,
]}>
<View
style={[
gutters,
a.pb_sm,
a.flex_row,
a.align_center,
a.justify_between,
]}>
<View style={[a.flex_1, a.flex_row, a.align_center, a.gap_xs]}>
<Graph />
<Text style={[a.text_md, a.font_bold, a.leading_snug]}>
<Trans>Trending Videos</Trans>
</Text>
</View>
<Button
label={_(msg`Dismiss this section`)}
size="tiny"
variant="ghost"
color="secondary"
shape="round"
onPress={() => trendingPrompt.open()}>
<ButtonIcon icon={X} />
</Button>
</View>
<BlockDrawerGesture>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
decelerationRate="fast"
snapToInterval={CARD_WIDTH + a.gap_sm.gap}>
<View
style={[
a.flex_row,
a.gap_sm,
{
paddingLeft: gutters.paddingLeft,
paddingRight: gutters.paddingRight,
},
]}>
{isLoading ? (
Array(10)
.fill(0)
.map((_, i) => (
<View key={i} style={[{width: CARD_WIDTH}]}>
<CompactVideoPostCardPlaceholder />
</View>
))
) : error || !data ? (
<Text>
<Trans>Whoops! Trending videos failed to load.</Trans>
</Text>
) : (
<VideoCards data={data} />
)}
</View>
</ScrollView>
</BlockDrawerGesture>
<Prompt.Basic
control={trendingPrompt}
title={_(msg`Hide trending videos?`)}
description={_(msg`You can update this later from your settings.`)}
confirmButtonCta={_(msg`Hide`)}
onConfirm={onConfirmHide}
/>
</View>
)
}
function VideoCards({
data,
}: {
data: Exclude<ReturnType<typeof usePostFeedQuery>['data'], undefined>
}) {
const t = useTheme()
const {_} = useLingui()
const items = React.useMemo(() => {
return data.pages
.flatMap(page => page.slices)
.map(slice => slice.items[0])
.filter(Boolean)
.filter(item => AppBskyEmbedVideo.isView(item.post.embed))
.slice(0, 8)
}, [data])
const href = React.useMemo(() => {
const urip = new AtUri(VIDEO_FEED_URI)
return makeCustomFeedLink(urip.host, urip.rkey, undefined, 'discover')
}, [])
return (
<>
{items.map(item => (
<View key={item.post.uri} style={[{width: CARD_WIDTH}]}>
<CompactVideoPostCard
post={item.post}
moderation={item.moderation}
sourceContext={{
type: 'feedgen',
uri: VIDEO_FEED_URI,
sourceInterstitial: 'discover',
}}
onInteract={() => {
logEvent('videoCard:click', {
context: 'interstitial:discover',
})
}}
/>
</View>
))}
<View style={[{width: CARD_WIDTH * 2}]}>
<Link
to={href}
label={_(msg`View more`)}
style={[
a.justify_center,
a.align_center,
a.flex_1,
a.rounded_md,
t.atoms.bg,
]}>
{({pressed}) => (
<View
style={[
a.flex_row,
a.align_center,
a.gap_md,
{
opacity: pressed ? 0.6 : 1,
},
]}>
<Text style={[a.text_md]}>
<Trans>View more</Trans>
</Text>
<View
style={[
a.align_center,
a.justify_center,
a.rounded_full,
{
width: 34,
height: 34,
backgroundColor: t.palette.primary_500,
},
]}>
<ButtonIcon icon={ChevronRight} />
</View>
</View>
)}
</Link>
</View>
</>
)
}
+2
View File
@@ -41,6 +41,7 @@ export class FeedViewPostsSlice {
isFallbackMarker: boolean isFallbackMarker: boolean
isOrphan: boolean isOrphan: boolean
rootUri: string rootUri: string
feedPostUri: string
constructor(feedPost: FeedViewPost) { constructor(feedPost: FeedViewPost) {
const {post, reply, reason} = feedPost const {post, reply, reason} = feedPost
@@ -48,6 +49,7 @@ export class FeedViewPostsSlice {
this.isIncompleteThread = false this.isIncompleteThread = false
this.isFallbackMarker = false this.isFallbackMarker = false
this.isOrphan = false this.isOrphan = false
this.feedPostUri = post.uri
if (AppBskyFeedDefs.isPostView(reply?.root)) { if (AppBskyFeedDefs.isPostView(reply?.root)) {
this.rootUri = reply.root.uri this.rootUri = reply.root.uri
} else { } else {
+3 -1
View File
@@ -23,7 +23,9 @@ export class AuthorFeedAPI implements FeedAPI {
get params() { get params() {
const params = {...this._params} const params = {...this._params}
params.includePins = params.filter !== 'posts_with_media' params.includePins =
params.filter === 'posts_with_replies' ||
params.filter === 'posts_and_author_threads'
return params return params
} }
+6 -1
View File
@@ -193,7 +193,12 @@ export async function post(
} }
async function resolveRT(agent: BskyAgent, richtext: RichText) { async function resolveRT(agent: BskyAgent, richtext: RichText) {
let rt = new RichText({text: richtext.text.trimEnd()}, {cleanNewlines: true}) const trimmedText = richtext.text
// Trim leading whitespace-only lines (but don't break ASCII art).
.replace(/^(\s*\n)+/, '')
// Trim any trailing whitespace.
.trimEnd()
let rt = new RichText({text: trimmedText}, {cleanNewlines: true})
await rt.detectFacets(agent) await rt.detectFacets(agent)
rt = shortenLinks(rt) rt = shortenLinks(rt)
+20 -24
View File
@@ -1,27 +1,23 @@
// import {init} from '@bitdrift/react-native' import {init, SessionStrategy} from '@bitdrift/react-native'
// import {Statsig} from 'statsig-react-native-expo' import {Statsig} from 'statsig-react-native-expo'
// export {debug, error, info, warn} from '@bitdrift/react-native' export {debug, error, info, warn} from '@bitdrift/react-native'
// import {initPromise} from './statsig/statsig' import {initPromise} from './statsig/statsig'
// const BITDRIFT_API_KEY = process.env.BITDRIFT_API_KEY const BITDRIFT_API_KEY = process.env.BITDRIFT_API_KEY
// initPromise.then(() => { initPromise.then(() => {
// let isEnabled = false let isEnabled = false
// try { try {
// if (Statsig.checkGate('enable_bitdrift')) { if (Statsig.checkGate('enable_bitdrift')) {
// isEnabled = true isEnabled = true
// } }
// } catch (e) { } catch (e) {
// // Statsig may complain about it being called too early. // Statsig may complain about it being called too early.
// } }
// if (isEnabled && BITDRIFT_API_KEY) { if (isEnabled && BITDRIFT_API_KEY) {
// init(BITDRIFT_API_KEY, {url: 'https://api-bsky.bitdrift.io'}) init(BITDRIFT_API_KEY, SessionStrategy.Activity, {
// } url: 'https://api-bsky.bitdrift.io',
// }) })
}
// TODO: Reenable when the build issue is fixed. })
export function debug(_message: string) {}
export function error(_message: string) {}
export function info(_message: string) {}
export function warn(_message: string) {}
+27
View File
@@ -52,6 +52,8 @@ export const MAX_DM_GRAPHEME_LENGTH = 1000
// but increasing limit per user feedback // but increasing limit per user feedback
export const MAX_ALT_TEXT = 2000 export const MAX_ALT_TEXT = 2000
export const MAX_REPORT_REASON_GRAPHEME_LENGTH = 2000
export function IS_TEST_USER(handle?: string) { export function IS_TEST_USER(handle?: string) {
return handle && handle?.endsWith('.test') return handle && handle?.endsWith('.test')
} }
@@ -63,6 +65,21 @@ export function IS_PROD_SERVICE(url?: string) {
export const PROD_DEFAULT_FEED = (rkey: string) => export const PROD_DEFAULT_FEED = (rkey: string) =>
`at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/${rkey}` `at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/${rkey}`
export const STAGING_DEFAULT_FEED = (rkey: string) =>
`at://did:plc:yofh3kx63drvfljkibw5zuxo/app.bsky.feed.generator/${rkey}`
export const PROD_FEEDS = [
`feedgen|${PROD_DEFAULT_FEED('whats-hot')}`,
`feedgen|${PROD_DEFAULT_FEED('thevids')}`,
]
export const STAGING_FEEDS = [
`feedgen|${STAGING_DEFAULT_FEED('whats-hot')}`,
`feedgen|${STAGING_DEFAULT_FEED('thevids')}`,
]
export const FEEDBACK_FEEDS = [...PROD_FEEDS, ...STAGING_FEEDS]
export const POST_IMG_MAX = { export const POST_IMG_MAX = {
width: 2000, width: 2000,
height: 2000, height: 2000,
@@ -109,6 +126,11 @@ export const BSKY_FEED_OWNER_DIDS = [
export const DISCOVER_FEED_URI = export const DISCOVER_FEED_URI =
'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot' 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot'
export const VIDEO_FEED_URI =
'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/thevids'
export const STAGING_VIDEO_FEED_URI =
'at://did:plc:yofh3kx63drvfljkibw5zuxo/app.bsky.feed.generator/thevids'
export const VIDEO_FEED_URIS = [VIDEO_FEED_URI, STAGING_VIDEO_FEED_URI]
export const DISCOVER_SAVED_FEED = { export const DISCOVER_SAVED_FEED = {
type: 'feed', type: 'feed',
value: DISCOVER_FEED_URI, value: DISCOVER_FEED_URI,
@@ -119,6 +141,11 @@ export const TIMELINE_SAVED_FEED = {
value: 'following', value: 'following',
pinned: true, pinned: true,
} }
export const VIDEO_SAVED_FEED = {
type: 'feed',
value: VIDEO_FEED_URI,
pinned: true,
}
export const RECOMMENDED_SAVED_FEEDS: Pick< export const RECOMMENDED_SAVED_FEEDS: Pick<
AppBskyActorDefs.SavedFeed, AppBskyActorDefs.SavedFeed,
+6 -2
View File
@@ -19,9 +19,13 @@ export function makeProfileLink(
export function makeCustomFeedLink( export function makeCustomFeedLink(
did: string, did: string,
rkey: string, rkey: string,
...segments: string[] segment?: string | undefined,
feedCacheKey?: 'discover' | 'explore' | undefined,
) { ) {
return [`/profile`, did, 'feed', rkey, ...segments].join('/') return (
[`/profile`, did, 'feed', rkey, ...(segment ? [segment] : [])].join('/') +
(feedCacheKey ? `?feedCacheKey=${encodeURIComponent(feedCacheKey)}` : '')
)
} }
export function makeListLink(did: string, rkey: string, ...segments: string[]) { export function makeListLink(did: string, rkey: string, ...segments: string[]) {
+8 -1
View File
@@ -1,6 +1,8 @@
import {NavigationState, PartialState} from '@react-navigation/native' import {NavigationState, PartialState} from '@react-navigation/native'
import type {NativeStackNavigationProp} from '@react-navigation/native-stack' import type {NativeStackNavigationProp} from '@react-navigation/native-stack'
import {VideoFeedSourceContext} from '#/screens/VideoFeed/types'
export type {NativeStackScreenProps} from '@react-navigation/native-stack' export type {NativeStackScreenProps} from '@react-navigation/native-stack'
export type CommonNavigatorParams = { export type CommonNavigatorParams = {
@@ -20,7 +22,11 @@ export type CommonNavigatorParams = {
PostLikedBy: {name: string; rkey: string} PostLikedBy: {name: string; rkey: string}
PostRepostedBy: {name: string; rkey: string} PostRepostedBy: {name: string; rkey: string}
PostQuotes: {name: string; rkey: string} PostQuotes: {name: string; rkey: string}
ProfileFeed: {name: string; rkey: string} ProfileFeed: {
name: string
rkey: string
feedCacheKey?: 'discover' | 'explore' | undefined
}
ProfileFeedLikedBy: {name: string; rkey: string} ProfileFeedLikedBy: {name: string; rkey: string}
ProfileLabelerLikedBy: {name: string} ProfileLabelerLikedBy: {name: string}
Debug: undefined Debug: undefined
@@ -57,6 +63,7 @@ export type CommonNavigatorParams = {
StarterPackShort: {code: string} StarterPackShort: {code: string}
StarterPackWizard: undefined StarterPackWizard: undefined
StarterPackEdit: {rkey?: string} StarterPackEdit: {rkey?: string}
VideoFeed: VideoFeedSourceContext
} }
export type BottomTabNavigatorParams = CommonNavigatorParams & { export type BottomTabNavigatorParams = CommonNavigatorParams & {
+21 -5
View File
@@ -13,7 +13,12 @@ export type LogEvents = {
withPassword: boolean withPassword: boolean
} }
'account:loggedOut': { 'account:loggedOut': {
logContext: 'SwitchAccount' | 'Settings' | 'SignupQueued' | 'Deactivated' logContext:
| 'SwitchAccount'
| 'Settings'
| 'SignupQueued'
| 'Deactivated'
| 'Takendown'
scope: 'current' | 'every' scope: 'current' | 'every'
} }
'notifications:openApp': {} 'notifications:openApp': {}
@@ -131,16 +136,16 @@ export type LogEvents = {
doesPosterFollowLiker: boolean | undefined doesPosterFollowLiker: boolean | undefined
likerClout: number | undefined likerClout: number | undefined
postClout: number | undefined postClout: number | undefined
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
} }
'post:repost': { 'post:repost': {
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
} }
'post:unlike': { 'post:unlike': {
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
} }
'post:unrepost': { 'post:unrepost': {
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
} }
'post:mute': {} 'post:mute': {}
'post:unmute': {} 'post:unmute': {}
@@ -163,6 +168,7 @@ export type LogEvents = {
| 'FeedInterstitial' | 'FeedInterstitial'
| 'ProfileHeaderSuggestedFollows' | 'ProfileHeaderSuggestedFollows'
| 'PostOnboardingFindFollows' | 'PostOnboardingFindFollows'
| 'ImmersiveVideo'
} }
'suggestedUser:follow': { 'suggestedUser:follow': {
logContext: logContext:
@@ -199,6 +205,7 @@ export type LogEvents = {
| 'FeedInterstitial' | 'FeedInterstitial'
| 'ProfileHeaderSuggestedFollows' | 'ProfileHeaderSuggestedFollows'
| 'PostOnboardingFindFollows' | 'PostOnboardingFindFollows'
| 'ImmersiveVideo'
} }
'chat:create': { 'chat:create': {
logContext: 'ProfileHeader' | 'NewChatDialog' | 'SendViaChatDialog' logContext: 'ProfileHeader' | 'NewChatDialog' | 'SendViaChatDialog'
@@ -269,6 +276,15 @@ export type LogEvents = {
'recommendedTopic:click': { 'recommendedTopic:click': {
context: 'explore' context: 'explore'
} }
'trendingVideos:show': {
context: 'settings'
}
'trendingVideos:hide': {
context: 'settings' | 'interstitial:discover' | 'interstitial:explore'
}
'videoCard:click': {
context: 'interstitial:discover' | 'interstitial:explore' | 'feed'
}
'progressGuide:hide': {} 'progressGuide:hide': {}
'progressGuide:followDialog:open': {} 'progressGuide:followDialog:open': {}
+1
View File
@@ -3,6 +3,7 @@ export type Gate =
| 'debug_show_feedcontext' | 'debug_show_feedcontext'
| 'debug_subscriptions' | 'debug_subscriptions'
| 'new_postonboarding' | 'new_postonboarding'
| 'onboarding_add_video_feed'
| 'remove_show_latest_button' | 'remove_show_latest_button'
| 'test_gate_1' | 'test_gate_1'
| 'test_gate_2' | 'test_gate_2'
+2 -4
View File
@@ -5,8 +5,7 @@ import {sha256} from 'js-sha256'
import {Statsig, StatsigProvider} from 'statsig-react-native-expo' import {Statsig, StatsigProvider} from 'statsig-react-native-expo'
import {BUNDLE_DATE, BUNDLE_IDENTIFIER, IS_TESTFLIGHT} from '#/lib/app-info' import {BUNDLE_DATE, BUNDLE_IDENTIFIER, IS_TESTFLIGHT} from '#/lib/app-info'
// TODO: Reenable when the build issue is fixed. import * as bitdrift from '#/lib/bitdrift'
// import * as bitdrift from '#/lib/bitdrift'
import {logger} from '#/logger' import {logger} from '#/logger'
import {isWeb} from '#/platform/detection' import {isWeb} from '#/platform/detection'
import * as persisted from '#/state/persisted' import * as persisted from '#/state/persisted'
@@ -108,8 +107,7 @@ export function logEvent<E extends keyof LogEvents>(
console.groupCollapsed(eventName) console.groupCollapsed(eventName)
console.log(fullMetadata) console.log(fullMetadata)
console.groupEnd() console.groupEnd()
// TODO: Reenable when the build issue is fixed. bitdrift.info(eventName, fullMetadata)
// bitdrift.info(eventName, fullMetadata)
} catch (e) { } catch (e) {
// A log should never interrupt the calling code, whatever happens. // A log should never interrupt the calling code, whatever happens.
logger.error('Failed to log an event', {message: e}) logger.error('Failed to log an event', {message: e})
+6
View File
@@ -161,6 +161,8 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
return AppLanguage.ast return AppLanguage.ast
case 'ca': case 'ca':
return AppLanguage.ca return AppLanguage.ca
case 'da':
return AppLanguage.da
case 'de': case 'de':
return AppLanguage.de return AppLanguage.de
case 'el': case 'el':
@@ -169,6 +171,8 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
return AppLanguage.en_GB return AppLanguage.en_GB
case 'es': case 'es':
return AppLanguage.es return AppLanguage.es
case 'eu':
return AppLanguage.eu
case 'fi': case 'fi':
return AppLanguage.fi return AppLanguage.fi
case 'fr': case 'fr':
@@ -203,6 +207,8 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
return AppLanguage.ro return AppLanguage.ro
case 'ru': case 'ru':
return AppLanguage.ru return AppLanguage.ru
case 'sv':
return AppLanguage.sv
case 'th': case 'th':
return AppLanguage.th return AppLanguage.th
case 'tr': case 'tr':
+27
View File
@@ -14,11 +14,13 @@ import {AppLanguage} from '#/locale/languages'
import {messages as messagesAn} from '#/locale/locales/an/messages' import {messages as messagesAn} from '#/locale/locales/an/messages'
import {messages as messagesAst} from '#/locale/locales/ast/messages' import {messages as messagesAst} from '#/locale/locales/ast/messages'
import {messages as messagesCa} from '#/locale/locales/ca/messages' import {messages as messagesCa} from '#/locale/locales/ca/messages'
import {messages as messagesDa} from '#/locale/locales/da/messages'
import {messages as messagesDe} from '#/locale/locales/de/messages' import {messages as messagesDe} from '#/locale/locales/de/messages'
import {messages as messagesEl} from '#/locale/locales/el/messages' import {messages as messagesEl} from '#/locale/locales/el/messages'
import {messages as messagesEn} from '#/locale/locales/en/messages' import {messages as messagesEn} from '#/locale/locales/en/messages'
import {messages as messagesEn_GB} from '#/locale/locales/en-GB/messages' import {messages as messagesEn_GB} from '#/locale/locales/en-GB/messages'
import {messages as messagesEs} from '#/locale/locales/es/messages' import {messages as messagesEs} from '#/locale/locales/es/messages'
import {messages as messagesEu} from '#/locale/locales/eu/messages'
import {messages as messagesFi} from '#/locale/locales/fi/messages' import {messages as messagesFi} from '#/locale/locales/fi/messages'
import {messages as messagesFr} from '#/locale/locales/fr/messages' import {messages as messagesFr} from '#/locale/locales/fr/messages'
import {messages as messagesGa} from '#/locale/locales/ga/messages' import {messages as messagesGa} from '#/locale/locales/ga/messages'
@@ -36,6 +38,7 @@ import {messages as messagesPl} from '#/locale/locales/pl/messages'
import {messages as messagesPt_BR} from '#/locale/locales/pt-BR/messages' import {messages as messagesPt_BR} from '#/locale/locales/pt-BR/messages'
import {messages as messagesRo} from '#/locale/locales/ro/messages' import {messages as messagesRo} from '#/locale/locales/ro/messages'
import {messages as messagesRu} from '#/locale/locales/ru/messages' import {messages as messagesRu} from '#/locale/locales/ru/messages'
import {messages as messagesSv} from '#/locale/locales/sv/messages'
import {messages as messagesTh} from '#/locale/locales/th/messages' import {messages as messagesTh} from '#/locale/locales/th/messages'
import {messages as messagesTr} from '#/locale/locales/tr/messages' import {messages as messagesTr} from '#/locale/locales/tr/messages'
import {messages as messagesUk} from '#/locale/locales/uk/messages' import {messages as messagesUk} from '#/locale/locales/uk/messages'
@@ -74,6 +77,14 @@ export async function dynamicActivate(locale: AppLanguage) {
]) ])
break break
} }
case AppLanguage.da: {
i18n.loadAndActivate({locale, messages: messagesDa})
await Promise.all([
import('@formatjs/intl-pluralrules/locale-data/da'),
import('@formatjs/intl-numberformat/locale-data/da'),
])
break
}
case AppLanguage.de: { case AppLanguage.de: {
i18n.loadAndActivate({locale, messages: messagesDe}) i18n.loadAndActivate({locale, messages: messagesDe})
await Promise.all([ await Promise.all([
@@ -106,6 +117,14 @@ export async function dynamicActivate(locale: AppLanguage) {
]) ])
break break
} }
case AppLanguage.eu: {
i18n.loadAndActivate({locale, messages: messagesEu})
await Promise.all([
import('@formatjs/intl-pluralrules/locale-data/eu'),
import('@formatjs/intl-numberformat/locale-data/eu'),
])
break
}
case AppLanguage.fi: { case AppLanguage.fi: {
i18n.loadAndActivate({locale, messages: messagesFi}) i18n.loadAndActivate({locale, messages: messagesFi})
await Promise.all([ await Promise.all([
@@ -238,6 +257,14 @@ export async function dynamicActivate(locale: AppLanguage) {
]) ])
break break
} }
case AppLanguage.sv: {
i18n.loadAndActivate({locale, messages: messagesSv})
await Promise.all([
import('@formatjs/intl-pluralrules/locale-data/sv'),
import('@formatjs/intl-numberformat/locale-data/sv'),
])
break
}
case AppLanguage.th: { case AppLanguage.th: {
i18n.loadAndActivate({locale, messages: messagesTh}) i18n.loadAndActivate({locale, messages: messagesTh})
await Promise.all([ await Promise.all([
+12
View File
@@ -24,6 +24,10 @@ export async function dynamicActivate(locale: AppLanguage) {
mod = await import(`./locales/ca/messages`) mod = await import(`./locales/ca/messages`)
break break
} }
case AppLanguage.da: {
mod = await import(`./locales/da/messages`)
break
}
case AppLanguage.de: { case AppLanguage.de: {
mod = await import(`./locales/de/messages`) mod = await import(`./locales/de/messages`)
break break
@@ -40,6 +44,10 @@ export async function dynamicActivate(locale: AppLanguage) {
mod = await import(`./locales/es/messages`) mod = await import(`./locales/es/messages`)
break break
} }
case AppLanguage.eu: {
mod = await import(`./locales/eu/messages`)
break
}
case AppLanguage.fi: { case AppLanguage.fi: {
mod = await import(`./locales/fi/messages`) mod = await import(`./locales/fi/messages`)
break break
@@ -108,6 +116,10 @@ export async function dynamicActivate(locale: AppLanguage) {
mod = await import(`./locales/ru/messages`) mod = await import(`./locales/ru/messages`)
break break
} }
case AppLanguage.sv: {
mod = await import(`./locales/sv/messages`)
break
}
case AppLanguage.th: { case AppLanguage.th: {
mod = await import(`./locales/th/messages`) mod = await import(`./locales/th/messages`)
break break
+6
View File
@@ -9,10 +9,12 @@ export enum AppLanguage {
an = 'an', an = 'an',
ast = 'ast', ast = 'ast',
ca = 'ca', ca = 'ca',
da = 'da',
de = 'de', de = 'de',
el = 'el', el = 'el',
en_GB = 'en-GB', en_GB = 'en-GB',
es = 'es', es = 'es',
eu = 'eu',
fi = 'fi', fi = 'fi',
fr = 'fr', fr = 'fr',
ga = 'ga', ga = 'ga',
@@ -30,6 +32,7 @@ export enum AppLanguage {
pt_BR = 'pt-BR', pt_BR = 'pt-BR',
ro = 'ro', ro = 'ro',
ru = 'ru', ru = 'ru',
sv = 'sv',
th = 'th', th = 'th',
tr = 'tr', tr = 'tr',
uk = 'uk', uk = 'uk',
@@ -49,10 +52,12 @@ export const APP_LANGUAGES: AppLanguageConfig[] = [
{code2: AppLanguage.an, name: 'Aragonés Aragonese'}, {code2: AppLanguage.an, name: 'Aragonés Aragonese'},
{code2: AppLanguage.ast, name: 'Asturianu Asturian'}, {code2: AppLanguage.ast, name: 'Asturianu Asturian'},
{code2: AppLanguage.ca, name: 'Català Catalan'}, {code2: AppLanguage.ca, name: 'Català Catalan'},
{code2: AppLanguage.da, name: 'Dansk Danish'},
{code2: AppLanguage.de, name: 'Deutsch German'}, {code2: AppLanguage.de, name: 'Deutsch German'},
{code2: AppLanguage.el, name: 'Ελληνικά Greek'}, {code2: AppLanguage.el, name: 'Ελληνικά Greek'},
{code2: AppLanguage.en_GB, name: 'English (UK)'}, {code2: AppLanguage.en_GB, name: 'English (UK)'},
{code2: AppLanguage.es, name: 'Español Spanish'}, {code2: AppLanguage.es, name: 'Español Spanish'},
{code2: AppLanguage.eu, name: 'Euskera Basque'},
{code2: AppLanguage.fi, name: 'Suomi Finnish'}, {code2: AppLanguage.fi, name: 'Suomi Finnish'},
{code2: AppLanguage.fr, name: 'Français French'}, {code2: AppLanguage.fr, name: 'Français French'},
{code2: AppLanguage.ga, name: 'Gaeilge Irish'}, {code2: AppLanguage.ga, name: 'Gaeilge Irish'},
@@ -70,6 +75,7 @@ export const APP_LANGUAGES: AppLanguageConfig[] = [
{code2: AppLanguage.pt_BR, name: 'Português (BR) Portuguese (BR)'}, {code2: AppLanguage.pt_BR, name: 'Português (BR) Portuguese (BR)'},
{code2: AppLanguage.ro, name: 'Română Romanian'}, {code2: AppLanguage.ro, name: 'Română Romanian'},
{code2: AppLanguage.ru, name: 'Русский Russian'}, {code2: AppLanguage.ru, name: 'Русский Russian'},
{code2: AppLanguage.sv, name: 'Svenska Swedish'},
{code2: AppLanguage.th, name: 'ภาษาไทย Thai'}, {code2: AppLanguage.th, name: 'ภาษาไทย Thai'},
{code2: AppLanguage.tr, name: 'Türkçe Turkish'}, {code2: AppLanguage.tr, name: 'Türkçe Turkish'},
{code2: AppLanguage.uk, name: 'Українська Ukrainian'}, {code2: AppLanguage.uk, name: 'Українська Ukrainian'},
+5 -5
View File
@@ -1936,7 +1936,7 @@ msgstr "Tanca el diàleg"
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:137 #: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:137
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:173 #: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:173
msgid "Close emoji picker" msgid "Close emoji picker"
msgstr "" msgstr "Tanca el selector d'emojis"
#: src/components/dialogs/GifSelect.tsx:169 #: src/components/dialogs/GifSelect.tsx:169
msgid "Close GIF dialog" msgid "Close GIF dialog"
@@ -2429,7 +2429,7 @@ msgstr "Creat {0}"
#: src/screens/List/ListHiddenScreen.tsx:127 #: src/screens/List/ListHiddenScreen.tsx:127
msgid "Creator has been blocked" msgid "Creator has been blocked"
msgstr "" msgstr "S'ha bloquejat al creador"
#: src/screens/Onboarding/index.tsx:26 #: src/screens/Onboarding/index.tsx:26
#: src/screens/Onboarding/state.ts:99 #: src/screens/Onboarding/state.ts:99
@@ -3049,7 +3049,7 @@ msgstr "Ensenyament"
#: src/screens/List/ListHiddenScreen.tsx:141 #: src/screens/List/ListHiddenScreen.tsx:141
msgid "Either the creator of this list has blocked you or you have blocked the creator." msgid "Either the creator of this list has blocked you or you have blocked the creator."
msgstr "" msgstr "El creador d'aquesta llista t'ha bloquejat o l'has bloquejat tu."
#: src/screens/Settings/AccountSettings.tsx:60 #: src/screens/Settings/AccountSettings.tsx:60
#: src/screens/Signup/StepInfo/index.tsx:170 #: src/screens/Signup/StepInfo/index.tsx:170
@@ -4837,7 +4837,7 @@ msgstr "Llista per <0/>"
#: src/view/com/profile/ProfileSubpageHeader.tsx:154 #: src/view/com/profile/ProfileSubpageHeader.tsx:154
msgid "List by you" msgid "List by you"
msgstr "" msgstr "Llista teva"
#: src/view/screens/ProfileList.tsx:455 #: src/view/screens/ProfileList.tsx:455
msgid "List deleted" msgid "List deleted"
@@ -8675,7 +8675,7 @@ msgstr "Toca per canviar la icona de l'aplicació"
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:138 #: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:138
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:174 #: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:174
msgid "Tap to close the emoji picker" msgid "Tap to close the emoji picker"
msgstr "" msgstr "Toca per a tancar el selector d'emojis"
#: src/components/ProgressGuide/Toast.tsx:156 #: src/components/ProgressGuide/Toast.tsx:156
msgid "Tap to dismiss" msgid "Tap to dismiss"
File diff suppressed because it is too large Load Diff
+115 -115
View File
@@ -1,6 +1,5 @@
msgid "" msgid ""
msgstr "" msgstr "POT-Creation-Date: 2023-12-22 01:56+0530\n"
"POT-Creation-Date: 2023-12-22 01:56+0530\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
@@ -9,13 +8,14 @@ msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: \n" "PO-Revision-Date: \n"
"Last-Translator: brodieavoult\n" "Last-Translator: hogaza\n"
"Language-Team: brodieavoult\n" "Language-Team: brodieavoult\n"
"Plural-Forms: \n" "Plural-Forms:\n"
" \n"
#: src/components/ProgressGuide/FollowDialog.tsx:567 #: src/components/ProgressGuide/FollowDialog.tsx:567
msgid "(active)" msgid "(active)"
msgstr "" msgstr "(activo)"
#: src/screens/Messages/components/ChatListItem.tsx:130 #: src/screens/Messages/components/ChatListItem.tsx:130
msgid "(contains embedded content)" msgid "(contains embedded content)"
@@ -37,11 +37,11 @@ msgstr "{0, plural, one {# día} other {# días}}"
#: src/screens/Profile/ProfileFollowers.tsx:40 #: src/screens/Profile/ProfileFollowers.tsx:40
msgid "{0, plural, one {# follower} other {# followers}}" msgid "{0, plural, one {# follower} other {# followers}}"
msgstr "" msgstr "{0, plural, one {# seguidor} other {# seguidores}}"
#: src/screens/Profile/ProfileFollows.tsx:40 #: src/screens/Profile/ProfileFollows.tsx:40
msgid "{0, plural, one {# following} other {# following}}" msgid "{0, plural, one {# following} other {# following}}"
msgstr "" msgstr "{0, plural, one {# siguiendo} other {# siguiendo}}"
#: src/lib/hooks/useTimeAgo.ts:146 #: src/lib/hooks/useTimeAgo.ts:146
msgid "{0, plural, one {# hour} other {# hours}}" msgid "{0, plural, one {# hour} other {# hours}}"
@@ -65,15 +65,15 @@ msgstr "{0, plural, one {# hora} other {# horas}}"
#: src/components/moderation/LabelsOnMe.tsx:53 #: src/components/moderation/LabelsOnMe.tsx:53
msgid "{0, plural, one {# label has} other {# labels have}} been placed on this account" msgid "{0, plural, one {# label has} other {# labels have}} been placed on this account"
msgstr "" msgstr "Se ha marcado esta cuenta con {0, plural, one {# etiqueta} other {# etiquetas}}"
#: src/components/moderation/LabelsOnMe.tsx:62 #: src/components/moderation/LabelsOnMe.tsx:62
msgid "{0, plural, one {# label has} other {# labels have}} been placed on this content" msgid "{0, plural, one {# label has} other {# labels have}} been placed on this content"
msgstr "" msgstr "Se ha marcado este contenido con {0, plural, one {# etiqueta} other {# etiquetas}}"
#: src/screens/Post/PostLikedBy.tsx:41 #: src/screens/Post/PostLikedBy.tsx:41
msgid "{0, plural, one {# like} other {# likes}}" msgid "{0, plural, one {# like} other {# likes}}"
msgstr "" msgstr "{0, plural, one {# me gusta} other {# me gusta}}"
#: src/lib/hooks/useTimeAgo.ts:136 #: src/lib/hooks/useTimeAgo.ts:136
msgid "{0, plural, one {# minute} other {# minutes}}" msgid "{0, plural, one {# minute} other {# minutes}}"
@@ -85,7 +85,7 @@ msgstr "{0, plural, one {# mes} other {# meses}}"
#: src/screens/Post/PostQuotes.tsx:41 #: src/screens/Post/PostQuotes.tsx:41
msgid "{0, plural, one {# quote} other {# quotes}}" msgid "{0, plural, one {# quote} other {# quotes}}"
msgstr "" msgstr "{0, plural, one {# cita} other {# citas}}"
#: src/screens/Post/PostRepostedBy.tsx:41 #: src/screens/Post/PostRepostedBy.tsx:41
msgid "{0, plural, one {# repost} other {# reposts}}" msgid "{0, plural, one {# repost} other {# reposts}}"
@@ -115,7 +115,7 @@ msgstr "{0, plural, one {siguiendo} other {siguiendo}}"
#: src/view/com/post-thread/PostThreadItem.tsx:447 #: src/view/com/post-thread/PostThreadItem.tsx:447
msgid "{0, plural, one {like} other {likes}}" msgid "{0, plural, one {like} other {likes}}"
msgstr "{0, plural, one {\"me gusta\"} other {\"me gusta\"}}" msgstr "{0, plural, one {me gusta} other {me gusta}}"
#: src/components/FeedCard.tsx:213 #: src/components/FeedCard.tsx:213
#: src/view/com/feeds/FeedSourceCard.tsx:303 #: src/view/com/feeds/FeedSourceCard.tsx:303
@@ -264,11 +264,11 @@ msgstr "{firstAuthorLink} y <0>{additionalAuthorsCount, plural, one {{formattedA
#: src/view/com/notifications/NotificationFeedItem.tsx:327 #: src/view/com/notifications/NotificationFeedItem.tsx:327
msgid "{firstAuthorLink} and <0>{additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}}</0> liked your custom feed" msgid "{firstAuthorLink} and <0>{additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}}</0> liked your custom feed"
msgstr "{firstAuthorLink} y <0>{additionalAuthorsCount, plural, one {{formattedAuthorsCount} más} other {{formattedAuthorsCount} más}}</0> dieron \"me gusta\" a tu feed" msgstr "A {firstAuthorLink} y <0>{additionalAuthorsCount, plural, one {{formattedAuthorsCount} más} other {{formattedAuthorsCount} más}}</0> les gusta tu feed"
#: src/view/com/notifications/NotificationFeedItem.tsx:223 #: src/view/com/notifications/NotificationFeedItem.tsx:223
msgid "{firstAuthorLink} and <0>{additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}}</0> liked your post" msgid "{firstAuthorLink} and <0>{additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}}</0> liked your post"
msgstr "{firstAuthorLink} y <0>{additionalAuthorsCount, plural, one {{formattedAuthorsCount} más} other {{formattedAuthorsCount} más}}</0> dieron \"me gusta\" a tu publicación" msgstr "A {firstAuthorLink} y <0>{additionalAuthorsCount, plural, one {{formattedAuthorsCount} más} other {{formattedAuthorsCount} más}}</0> les gusta tu publicación"
#: src/view/com/notifications/NotificationFeedItem.tsx:247 #: src/view/com/notifications/NotificationFeedItem.tsx:247
msgid "{firstAuthorLink} and <0>{additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}}</0> reposted your post" msgid "{firstAuthorLink} and <0>{additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}}</0> reposted your post"
@@ -288,11 +288,11 @@ msgstr "{firstAuthorLink} te siguió de vuelta"
#: src/view/com/notifications/NotificationFeedItem.tsx:339 #: src/view/com/notifications/NotificationFeedItem.tsx:339
msgid "{firstAuthorLink} liked your custom feed" msgid "{firstAuthorLink} liked your custom feed"
msgstr "{firstAuthorLink} dio \"me gusta\" a tu feed personalizado" msgstr "A {firstAuthorLink} le gusta tu feed personalizado"
#: src/view/com/notifications/NotificationFeedItem.tsx:235 #: src/view/com/notifications/NotificationFeedItem.tsx:235
msgid "{firstAuthorLink} liked your post" msgid "{firstAuthorLink} liked your post"
msgstr "{firstAuthorLink} dio \"me gusta\" a tu publicación" msgstr "A {firstAuthorLink} le gusta tu publicación"
#: src/view/com/notifications/NotificationFeedItem.tsx:259 #: src/view/com/notifications/NotificationFeedItem.tsx:259
msgid "{firstAuthorLink} reposted your post" msgid "{firstAuthorLink} reposted your post"
@@ -308,11 +308,11 @@ msgstr "{firstAuthorName} y {formattedAuthorsCount} más te siguieron"
#: src/view/com/notifications/NotificationFeedItem.tsx:320 #: src/view/com/notifications/NotificationFeedItem.tsx:320
msgid "{firstAuthorName} and {additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}} liked your custom feed" msgid "{firstAuthorName} and {additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}} liked your custom feed"
msgstr "{firstAuthorName} y {formattedAuthorsCount} más dieron \"me gusta\" a tu feed personalizado" msgstr "A {firstAuthorName} y {formattedAuthorsCount} más les gusta tu feed personalizado"
#: src/view/com/notifications/NotificationFeedItem.tsx:216 #: src/view/com/notifications/NotificationFeedItem.tsx:216
msgid "{firstAuthorName} and {additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}} liked your post" msgid "{firstAuthorName} and {additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}} liked your post"
msgstr "{firstAuthorName} y {formattedAuthorsCount} más dieron \"me gusta\" a tu publicación" msgstr "A {firstAuthorName} y {formattedAuthorsCount} les gusta tu publicación"
#: src/view/com/notifications/NotificationFeedItem.tsx:240 #: src/view/com/notifications/NotificationFeedItem.tsx:240
msgid "{firstAuthorName} and {additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}} reposted your post" msgid "{firstAuthorName} and {additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}} reposted your post"
@@ -332,11 +332,11 @@ msgstr "{firstAuthorName} te siguió de vuelta"
#: src/view/com/notifications/NotificationFeedItem.tsx:325 #: src/view/com/notifications/NotificationFeedItem.tsx:325
msgid "{firstAuthorName} liked your custom feed" msgid "{firstAuthorName} liked your custom feed"
msgstr "{firstAuthorName} dio \"me gusta\" a tu feed personalizado" msgstr "A {firstAuthorName} le gusta tu feed personalizado"
#: src/view/com/notifications/NotificationFeedItem.tsx:221 #: src/view/com/notifications/NotificationFeedItem.tsx:221
msgid "{firstAuthorName} liked your post" msgid "{firstAuthorName} liked your post"
msgstr "{firstAuthorName} dio \"me gusta\" a tu publicación" msgstr "A {firstAuthorName} le gusta tu publicación"
#: src/view/com/notifications/NotificationFeedItem.tsx:245 #: src/view/com/notifications/NotificationFeedItem.tsx:245
msgid "{firstAuthorName} reposted your post" msgid "{firstAuthorName} reposted your post"
@@ -363,7 +363,7 @@ msgstr "No se puede enviar un mensaje a {handle}"
#: src/view/shell/bottom-bar/BottomBarWeb.tsx:260 #: src/view/shell/bottom-bar/BottomBarWeb.tsx:260
msgid "{notificationCount} unread items" msgid "{notificationCount} unread items"
msgstr "" msgstr "{notificationCount} elementos sin leer"
#: src/view/shell/Drawer.tsx:448 #: src/view/shell/Drawer.tsx:448
msgid "{numUnreadNotifications} unread" msgid "{numUnreadNotifications} unread"
@@ -532,16 +532,16 @@ msgstr "Cuenta seguida"
#: src/view/com/profile/ProfileMenu.tsx:108 #: src/view/com/profile/ProfileMenu.tsx:108
msgid "Account muted" msgid "Account muted"
msgstr "Cuenta muteada" msgstr "Cuenta silenciada"
#: src/components/moderation/ModerationDetailsDialog.tsx:101 #: src/components/moderation/ModerationDetailsDialog.tsx:101
#: src/lib/moderation/useModerationCauseDescription.ts:96 #: src/lib/moderation/useModerationCauseDescription.ts:96
msgid "Account Muted" msgid "Account Muted"
msgstr "Cuenta muteada" msgstr "Cuenta silenciada"
#: src/components/moderation/ModerationDetailsDialog.tsx:87 #: src/components/moderation/ModerationDetailsDialog.tsx:87
msgid "Account Muted by List" msgid "Account Muted by List"
msgstr "Cuenta muteada por lista" msgstr "Cuenta silenciada por lista"
#: src/screens/Settings/Settings.tsx:428 #: src/screens/Settings/Settings.tsx:428
msgid "Account options" msgid "Account options"
@@ -645,7 +645,7 @@ msgstr "Agregar nueva publicación"
#: src/view/screens/ProfileList.tsx:903 #: src/view/screens/ProfileList.tsx:903
#: src/view/screens/ProfileList.tsx:921 #: src/view/screens/ProfileList.tsx:921
msgid "Add people" msgid "Add people"
msgstr "" msgstr "Añadir personas"
#: src/screens/StarterPack/Wizard/index.tsx:197 #: src/screens/StarterPack/Wizard/index.tsx:197
#~ msgid "Add people to your starter pack that you think others will enjoy following" #~ msgid "Add people to your starter pack that you think others will enjoy following"
@@ -710,7 +710,7 @@ msgstr "El contenido para adultos solo se puede habilitar a través de la web en
#: src/components/moderation/LabelPreference.tsx:242 #: src/components/moderation/LabelPreference.tsx:242
msgid "Adult content is disabled." msgid "Adult content is disabled."
msgstr "El contenido adulto esta desactivado." msgstr "El contenido adulto está desactivado."
#: src/view/com/composer/labels/LabelsBtn.tsx:139 #: src/view/com/composer/labels/LabelsBtn.tsx:139
#: src/view/com/composer/labels/LabelsBtn.tsx:197 #: src/view/com/composer/labels/LabelsBtn.tsx:197
@@ -727,7 +727,7 @@ msgstr "Avanzado"
#: src/view/screens/Notifications.tsx:86 #: src/view/screens/Notifications.tsx:86
msgid "All" msgid "All"
msgstr "" msgstr "Todas"
#: src/screens/StarterPack/StarterPackScreen.tsx:381 #: src/screens/StarterPack/StarterPackScreen.tsx:381
msgid "All accounts have been followed!" msgid "All accounts have been followed!"
@@ -754,7 +754,7 @@ msgstr "Permitir nuevos mensajes de"
#: src/components/dialogs/PostInteractionSettingsDialog.tsx:314 #: src/components/dialogs/PostInteractionSettingsDialog.tsx:314
msgid "Allow quote posts" msgid "Allow quote posts"
msgstr "" msgstr "Permitir citas"
#: src/components/dialogs/PostInteractionSettingsDialog.tsx:359 #: src/components/dialogs/PostInteractionSettingsDialog.tsx:359
msgid "Allow replies from:" msgid "Allow replies from:"
@@ -933,7 +933,7 @@ msgstr "Cualquiera puede interactuar"
#: src/screens/Settings/AppIconSettings/SettingsListItem.tsx:18 #: src/screens/Settings/AppIconSettings/SettingsListItem.tsx:18
#: src/screens/Settings/AppIconSettings/SettingsListItem.tsx:23 #: src/screens/Settings/AppIconSettings/SettingsListItem.tsx:23
msgid "App Icon" msgid "App Icon"
msgstr "" msgstr "Icono de la app"
#: src/screens/Settings/LanguageSettings.tsx:80 #: src/screens/Settings/LanguageSettings.tsx:80
msgid "App Language" msgid "App Language"
@@ -1165,7 +1165,7 @@ msgstr "Antes de contactar a otro usuario, debes verificar tu correo electrónic
#: src/screens/Search/components/ExploreTrendingTopics.tsx:74 #: src/screens/Search/components/ExploreTrendingTopics.tsx:74
msgid "BETA" msgid "BETA"
msgstr "" msgstr "BETA"
#: src/components/dialogs/BirthDateSettings.tsx:106 #: src/components/dialogs/BirthDateSettings.tsx:106
#: src/screens/Settings/AccountSettings.tsx:109 #: src/screens/Settings/AccountSettings.tsx:109
@@ -1289,11 +1289,11 @@ msgstr "Bluesky no mostrará tu perfil o posts a usuarios que no hayan iniciado
#: src/screens/Settings/AppIconSettings/index.tsx:99 #: src/screens/Settings/AppIconSettings/index.tsx:99
msgid "Bluesky+" msgid "Bluesky+"
msgstr "" msgstr "Bluesky+"
#: src/screens/Settings/AppIconSettings/index.tsx:102 #: src/screens/Settings/AppIconSettings/index.tsx:102
msgid "Bluesky+ icons" msgid "Bluesky+ icons"
msgstr "" msgstr "Iconos Bluesky+"
#: src/lib/moderation/useLabelBehaviorDescription.ts:53 #: src/lib/moderation/useLabelBehaviorDescription.ts:53
msgid "Blur images" msgid "Blur images"
@@ -1339,15 +1339,15 @@ msgstr "Explorar otros feeds"
#: src/components/TrendingTopics.tsx:176 #: src/components/TrendingTopics.tsx:176
msgid "Browse posts about {displayName}" msgid "Browse posts about {displayName}"
msgstr "" msgstr "Explorar posts sobre {displayName}"
#: src/components/TrendingTopics.tsx:184 #: src/components/TrendingTopics.tsx:184
msgid "Browse posts tagged with {displayName}" msgid "Browse posts tagged with {displayName}"
msgstr "" msgstr "Explorar posts etiquetados con {displayName}"
#: src/components/TrendingTopics.tsx:222 #: src/components/TrendingTopics.tsx:222
msgid "Browse topic {displayName}" msgid "Browse topic {displayName}"
msgstr "" msgstr "Explorar el tema {displayName}"
#: src/view/com/auth/SplashScreen.web.tsx:168 #: src/view/com/auth/SplashScreen.web.tsx:168
msgid "Business" msgid "Business"
@@ -1371,7 +1371,7 @@ msgstr "Por {0}"
#: src/screens/Profile/components/ProfileFeedHeader.tsx:447 #: src/screens/Profile/components/ProfileFeedHeader.tsx:447
msgid "By <0>{0}</0>" msgid "By <0>{0}</0>"
msgstr "" msgstr "Por <0>{0}</0>"
#: src/screens/Signup/StepInfo/Policies.tsx:80 #: src/screens/Signup/StepInfo/Policies.tsx:80
#~ msgid "By creating an account you agree to the {els}." #~ msgid "By creating an account you agree to the {els}."
@@ -1504,12 +1504,12 @@ msgstr "Cambiar"
#: src/screens/Settings/AppIconSettings/index.tsx:39 #: src/screens/Settings/AppIconSettings/index.tsx:39
msgid "Change app icon" msgid "Change app icon"
msgstr "" msgstr "Cambiar icono de la app"
#: src/screens/Settings/AppIconSettings/index.tsx:38 #: src/screens/Settings/AppIconSettings/index.tsx:38
#: src/screens/Settings/AppIconSettings/index.tsx:221 #: src/screens/Settings/AppIconSettings/index.tsx:221
msgid "Change app icon to \"{0}\"" msgid "Change app icon to \"{0}\""
msgstr "" msgstr "Cambiar icono de la app a \"{0}\""
#: src/screens/Settings/AccountSettings.tsx:97 #: src/screens/Settings/AccountSettings.tsx:97
#: src/screens/Settings/AccountSettings.tsx:101 #: src/screens/Settings/AccountSettings.tsx:101
@@ -1578,7 +1578,7 @@ msgstr "Configuración de chat"
#: src/components/dms/ConvoMenu.tsx:84 #: src/components/dms/ConvoMenu.tsx:84
msgid "Chat unmuted" msgid "Chat unmuted"
msgstr "Chat demuteado" msgstr "Chat no silenciado"
#: src/screens/SignupQueued.tsx:78 #: src/screens/SignupQueued.tsx:78
#: src/screens/SignupQueued.tsx:82 #: src/screens/SignupQueued.tsx:82
@@ -1649,7 +1649,7 @@ msgstr "Elige este color como tu avatar"
#: src/view/screens/Feeds.tsx:728 #: src/view/screens/Feeds.tsx:728
#: src/view/screens/Search/Explore.tsx:411 #: src/view/screens/Search/Explore.tsx:411
msgid "Choose your own timeline! Feeds built by the community help you find content you love." msgid "Choose your own timeline! Feeds built by the community help you find content you love."
msgstr "" msgstr "Elige tu propia cronología: encuentra el contenido que más te gusta gracias a los feeds de la comunidad."
#: src/screens/Signup/StepInfo/index.tsx:201 #: src/screens/Signup/StepInfo/index.tsx:201
msgid "Choose your password" msgid "Choose your password"
@@ -1761,11 +1761,11 @@ msgstr "Cerrar ventana"
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:137 #: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:137
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:173 #: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:173
msgid "Close emoji picker" msgid "Close emoji picker"
msgstr "" msgstr "Cerrar el selector de emoji"
#: src/components/dialogs/GifSelect.tsx:169 #: src/components/dialogs/GifSelect.tsx:169
msgid "Close GIF dialog" msgid "Close GIF dialog"
msgstr "Cerrar ventana de GIF" msgstr "Cerrar la ventana de GIF"
#: src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx:31 #: src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx:31
msgid "Close image" msgid "Close image"
@@ -1934,7 +1934,7 @@ msgstr "Contactar a soporte"
#: src/screens/Settings/ContentAndMediaSettings.tsx:49 #: src/screens/Settings/ContentAndMediaSettings.tsx:49
msgid "Content & Media" msgid "Content & Media"
msgstr "" msgstr "Contenido y medios"
#: src/screens/Settings/AccessibilitySettings.tsx:109 #: src/screens/Settings/AccessibilitySettings.tsx:109
#: src/screens/Settings/Settings.tsx:175 #: src/screens/Settings/Settings.tsx:175
@@ -2212,7 +2212,7 @@ msgstr "Creado {0}"
#: src/screens/List/ListHiddenScreen.tsx:127 #: src/screens/List/ListHiddenScreen.tsx:127
msgid "Creator has been blocked" msgid "Creator has been blocked"
msgstr "" msgstr "Se ha bloqueado al creador"
#: src/screens/Onboarding/index.tsx:26 #: src/screens/Onboarding/index.tsx:26
#: src/screens/Onboarding/state.ts:99 #: src/screens/Onboarding/state.ts:99
@@ -2286,7 +2286,7 @@ msgstr "Por Defecto"
#: src/screens/Settings/AppIconSettings/index.tsx:75 #: src/screens/Settings/AppIconSettings/index.tsx:75
msgid "Default icons" msgid "Default icons"
msgstr "" msgstr "Iconos por defecto"
#: src/components/dms/MessageMenu.tsx:151 #: src/components/dms/MessageMenu.tsx:151
#: src/screens/Settings/AppPasswords.tsx:212 #: src/screens/Settings/AppPasswords.tsx:212
@@ -2720,7 +2720,7 @@ msgstr "Editar los detalles de la lista"
#: src/view/com/modals/CreateOrEditList.tsx:230 #: src/view/com/modals/CreateOrEditList.tsx:230
msgid "Edit Moderation List" msgid "Edit Moderation List"
msgstr "Editar lista de Moderación" msgstr "Editar lista de moderación"
#: src/Navigation.tsx:296 #: src/Navigation.tsx:296
#: src/view/screens/Feeds.tsx:515 #: src/view/screens/Feeds.tsx:515
@@ -2792,7 +2792,7 @@ msgstr "Educación"
#: src/screens/List/ListHiddenScreen.tsx:141 #: src/screens/List/ListHiddenScreen.tsx:141
msgid "Either the creator of this list has blocked you or you have blocked the creator." msgid "Either the creator of this list has blocked you or you have blocked the creator."
msgstr "" msgstr "La persona que creó esta lista te ha bloqueado, o tú la has bloqueado a ella."
#: src/screens/Settings/AccountSettings.tsx:60 #: src/screens/Settings/AccountSettings.tsx:60
#: src/screens/Signup/StepInfo/index.tsx:170 #: src/screens/Signup/StepInfo/index.tsx:170
@@ -2910,7 +2910,7 @@ msgstr "Habilitar solo esta fuente"
#: src/screens/Settings/ContentAndMediaSettings.tsx:122 #: src/screens/Settings/ContentAndMediaSettings.tsx:122
#: src/screens/Settings/ContentAndMediaSettings.tsx:136 #: src/screens/Settings/ContentAndMediaSettings.tsx:136
msgid "Enable trending topics" msgid "Enable trending topics"
msgstr "" msgstr "Activar las tendencias"
#: src/screens/Messages/Settings.tsx:130 #: src/screens/Messages/Settings.tsx:130
#: src/screens/Messages/Settings.tsx:133 #: src/screens/Messages/Settings.tsx:133
@@ -3261,7 +3261,7 @@ msgstr "Feed por {0}"
#: src/screens/Profile/components/ProfileFeedHeader.tsx:353 #: src/screens/Profile/components/ProfileFeedHeader.tsx:353
msgid "Feed menu" msgid "Feed menu"
msgstr "" msgstr "Menú de feed"
#: src/view/screens/Feeds.tsx:709 #: src/view/screens/Feeds.tsx:709
#~ msgid "Feed offline" #~ msgid "Feed offline"
@@ -3308,7 +3308,7 @@ msgstr "¡Feeds actualizados!"
#: src/screens/Search/components/ExploreRecommendations.tsx:54 #: src/screens/Search/components/ExploreRecommendations.tsx:54
msgid "Feeds we think you might like." msgid "Feeds we think you might like."
msgstr "" msgstr "Creemos que estos estos feeds te gustarán."
#: src/view/com/modals/ChangeHandle.tsx:468 #: src/view/com/modals/ChangeHandle.tsx:468
#~ msgid "File Contents" #~ msgid "File Contents"
@@ -3340,7 +3340,7 @@ msgstr "Buscar cuentas para seguir"
#: src/components/ProgressGuide/FollowDialog.tsx:88 #: src/components/ProgressGuide/FollowDialog.tsx:88
#: src/components/ProgressGuide/FollowDialog.tsx:417 #: src/components/ProgressGuide/FollowDialog.tsx:417
msgid "Find people to follow" msgid "Find people to follow"
msgstr "" msgstr "Buscar a gente para seguir"
#: src/view/screens/Search/Search.tsx:595 #: src/view/screens/Search/Search.tsx:595
msgid "Find posts and users on Bluesky" msgid "Find posts and users on Bluesky"
@@ -3405,7 +3405,7 @@ msgstr "Seguir {name}"
#: src/components/ProgressGuide/List.tsx:52 #: src/components/ProgressGuide/List.tsx:52
#: src/state/shell/progress-guide.tsx:229 #: src/state/shell/progress-guide.tsx:229
msgid "Follow 10 accounts" msgid "Follow 10 accounts"
msgstr "" msgstr "Sigue 10 cuentas"
#: src/components/ProgressGuide/List.tsx:69 #: src/components/ProgressGuide/List.tsx:69
msgid "Follow 7 accounts" msgid "Follow 7 accounts"
@@ -3856,13 +3856,13 @@ msgstr "¿Ocultar esta respuesta?"
#: src/screens/Search/components/ExploreTrendingTopics.tsx:83 #: src/screens/Search/components/ExploreTrendingTopics.tsx:83
#: src/view/shell/desktop/SidebarTrendingTopics.tsx:62 #: src/view/shell/desktop/SidebarTrendingTopics.tsx:62
msgid "Hide trending topics" msgid "Hide trending topics"
msgstr "" msgstr "Ocultar las tendencias"
#: src/components/interstitials/Trending.tsx:99 #: src/components/interstitials/Trending.tsx:99
#: src/screens/Search/components/ExploreTrendingTopics.tsx:135 #: src/screens/Search/components/ExploreTrendingTopics.tsx:135
#: src/view/shell/desktop/SidebarTrendingTopics.tsx:108 #: src/view/shell/desktop/SidebarTrendingTopics.tsx:108
msgid "Hide trending topics?" msgid "Hide trending topics?"
msgstr "" msgstr "¿Quieres ocultar las tendencias?"
#: src/view/com/notifications/NotificationFeedItem.tsx:592 #: src/view/com/notifications/NotificationFeedItem.tsx:592
msgid "Hide user list" msgid "Hide user list"
@@ -4322,11 +4322,11 @@ msgstr "Claro"
#: src/screens/Profile/components/ProfileFeedHeader.tsx:514 #: src/screens/Profile/components/ProfileFeedHeader.tsx:514
msgid "Like" msgid "Like"
msgstr "" msgstr "Me gusta"
#: src/view/com/util/post-ctrls/PostCtrls.tsx:311 #: src/view/com/util/post-ctrls/PostCtrls.tsx:311
msgid "Like ({0, plural, one {# like} other {# likes}})" msgid "Like ({0, plural, one {# like} other {# likes}})"
msgstr "" msgstr "Like ({0, plural, one {# me gusta} other {# me gusta}})"
#: src/components/ProgressGuide/List.tsx:63 #: src/components/ProgressGuide/List.tsx:63
msgid "Like 10 posts" msgid "Like 10 posts"
@@ -4339,7 +4339,7 @@ msgstr "Dale \"me gusta\" a 10 publicaciones para entrenar el feed de Descubrimi
#: src/screens/Profile/components/ProfileFeedHeader.tsx:501 #: src/screens/Profile/components/ProfileFeedHeader.tsx:501
msgid "Like feed" msgid "Like feed"
msgstr "" msgstr "Me gusta este feed"
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:275 #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:275
msgid "Like this feed" msgid "Like this feed"
@@ -4349,18 +4349,18 @@ msgstr "Dar \"me gusta\" a este feed"
#: src/Navigation.tsx:236 #: src/Navigation.tsx:236
#: src/Navigation.tsx:241 #: src/Navigation.tsx:241
msgid "Liked by" msgid "Liked by"
msgstr "Le ha gustado a" msgstr "Le gusta a"
#: src/screens/Post/PostLikedBy.tsx:38 #: src/screens/Post/PostLikedBy.tsx:38
#: src/screens/Profile/ProfileLabelerLikedBy.tsx:29 #: src/screens/Profile/ProfileLabelerLikedBy.tsx:29
#: src/view/screens/ProfileFeedLikedBy.tsx:30 #: src/view/screens/ProfileFeedLikedBy.tsx:30
msgid "Liked By" msgid "Liked By"
msgstr "Le ha gustado a" msgstr "Le gusta a"
#: src/components/FeedCard.tsx:213 #: src/components/FeedCard.tsx:213
#: src/view/com/feeds/FeedSourceCard.tsx:303 #: src/view/com/feeds/FeedSourceCard.tsx:303
msgid "Liked by {0, plural, one {# user} other {# users}}" msgid "Liked by {0, plural, one {# user} other {# users}}"
msgstr "" msgstr "Le gusta a {0, plural, one {# usuario} other {# usuarios}}"
#: src/view/com/feeds/FeedSourceCard.tsx:268 #: src/view/com/feeds/FeedSourceCard.tsx:268
#~ msgid "Liked by {0} {1}" #~ msgid "Liked by {0} {1}"
@@ -4375,7 +4375,7 @@ msgstr ""
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:295 #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:295
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:309 #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:309
msgid "Liked by {likeCount, plural, one {# user} other {# users}}" msgid "Liked by {likeCount, plural, one {# user} other {# users}}"
msgstr "" msgstr "Le gusta a {likeCount, plural, one {# usuario} other {# usuarios}}"
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:287 #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:287
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:301 #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:301
@@ -4393,7 +4393,7 @@ msgstr ""
#: src/view/screens/Profile.tsx:234 #: src/view/screens/Profile.tsx:234
msgid "Likes" msgid "Likes"
msgstr "\"Me gusta\"" msgstr "Me gusta"
#: src/view/com/post-thread/PostThreadItem.tsx:217 #: src/view/com/post-thread/PostThreadItem.tsx:217
msgid "Likes on this post" msgid "Likes on this post"
@@ -4402,7 +4402,7 @@ msgstr "\"Me gusta\" en este post"
#: src/view/com/post-thread/PostThread.tsx:626 #: src/view/com/post-thread/PostThread.tsx:626
#: src/view/com/post-thread/PostThread.tsx:631 #: src/view/com/post-thread/PostThread.tsx:631
msgid "Linear" msgid "Linear"
msgstr "" msgstr "Lineal"
#: src/Navigation.tsx:198 #: src/Navigation.tsx:198
msgid "List" msgid "List"
@@ -4423,11 +4423,11 @@ msgstr "Lista por {0}"
#: src/view/com/profile/ProfileSubpageHeader.tsx:156 #: src/view/com/profile/ProfileSubpageHeader.tsx:156
msgid "List by <0/>" msgid "List by <0/>"
msgstr "" msgstr "Lista de {0}"
#: src/view/com/profile/ProfileSubpageHeader.tsx:154 #: src/view/com/profile/ProfileSubpageHeader.tsx:154
msgid "List by you" msgid "List by you"
msgstr "" msgstr "Lista tuya"
#: src/view/screens/ProfileList.tsx:455 #: src/view/screens/ProfileList.tsx:455
msgid "List deleted" msgid "List deleted"
@@ -4523,7 +4523,7 @@ msgstr "Acceder a una cuenta que no está en la lista"
#: src/view/shell/desktop/RightNav.tsx:121 #: src/view/shell/desktop/RightNav.tsx:121
msgid "Logo by @sawaratsuki.bsky.social" msgid "Logo by @sawaratsuki.bsky.social"
msgstr "" msgstr "Logo de @sawaratsuki.bsky.social</0>"
#: src/view/shell/desktop/RightNav.tsx:103 #: src/view/shell/desktop/RightNav.tsx:103
#~ msgid "Logo by <0/>" #~ msgid "Logo by <0/>"
@@ -4532,7 +4532,7 @@ msgstr ""
#: src/view/shell/desktop/RightNav.tsx:118 #: src/view/shell/desktop/RightNav.tsx:118
#: src/view/shell/Drawer.tsx:629 #: src/view/shell/Drawer.tsx:629
msgid "Logo by <0>@sawaratsuki.bsky.social</0>" msgid "Logo by <0>@sawaratsuki.bsky.social</0>"
msgstr "Logo por <0>@sawaratsuki.bsky.social</0>" msgstr "Logo de <0>@sawaratsuki.bsky.social</0>"
#: src/components/RichText.tsx:219 #: src/components/RichText.tsx:219
msgid "Long press to open tag menu for #{tag}" msgid "Long press to open tag menu for #{tag}"
@@ -4540,7 +4540,7 @@ msgstr "Mantén presionado para abrir el menú de etiquetas para #{tag}"
#: src/screens/Login/SetNewPasswordForm.tsx:110 #: src/screens/Login/SetNewPasswordForm.tsx:110
msgid "Looks like XXXXX-XXXXX" msgid "Looks like XXXXX-XXXXX"
msgstr "Es como XXXXX-XXXXX" msgstr "Es algo así: XXXXX-XXXXX"
#: src/screens/Feeds/NoSavedFeedsOfAnyType.tsx:39 #: src/screens/Feeds/NoSavedFeedsOfAnyType.tsx:39
msgid "Looks like you haven't saved any feeds! Use our recommendations or browse more below." msgid "Looks like you haven't saved any feeds! Use our recommendations or browse more below."
@@ -4598,7 +4598,7 @@ msgstr "Usuarios mencionados"
#: src/view/screens/Notifications.tsx:99 #: src/view/screens/Notifications.tsx:99
msgid "Mentions" msgid "Mentions"
msgstr "" msgstr "Menciones"
#: src/components/Menu/index.tsx:96 #: src/components/Menu/index.tsx:96
#: src/view/screens/Search/Search.tsx:863 #: src/view/screens/Search/Search.tsx:863
@@ -4972,7 +4972,7 @@ msgstr "Nuevo nombre de usuario"
#: src/view/screens/Lists.tsx:66 #: src/view/screens/Lists.tsx:66
#: src/view/screens/ModerationModlists.tsx:66 #: src/view/screens/ModerationModlists.tsx:66
msgid "New list" msgid "New list"
msgstr "" msgstr "Nueva lista"
#: src/components/dms/NewMessagesPill.tsx:92 #: src/components/dms/NewMessagesPill.tsx:92
msgid "New messages" msgid "New messages"
@@ -5394,31 +5394,31 @@ msgstr "Abrir el creador de avatares"
#: src/screens/Settings/AccountSettings.tsx:127 #: src/screens/Settings/AccountSettings.tsx:127
msgid "Open change handle dialog" msgid "Open change handle dialog"
msgstr "Abrir diálogo de cambio de nombre de usuario" msgstr "Abrir el diálogo de cambio de nombre de usuario"
#: src/screens/Messages/components/ChatListItem.tsx:272 #: src/screens/Messages/components/ChatListItem.tsx:272
#: src/screens/Messages/components/ChatListItem.tsx:273 #: src/screens/Messages/components/ChatListItem.tsx:273
msgid "Open conversation options" msgid "Open conversation options"
msgstr "Abrir opciones de conversación" msgstr "Abrir las opciones de conversación"
#: src/components/Layout/Header/index.tsx:145 #: src/components/Layout/Header/index.tsx:145
msgid "Open drawer menu" msgid "Open drawer menu"
msgstr "" msgstr "Abrir el menú lateral"
#: src/screens/Messages/components/MessageInput.web.tsx:181 #: src/screens/Messages/components/MessageInput.web.tsx:181
#: src/view/com/composer/Composer.tsx:1228 #: src/view/com/composer/Composer.tsx:1228
#: src/view/com/composer/Composer.tsx:1229 #: src/view/com/composer/Composer.tsx:1229
msgid "Open emoji picker" msgid "Open emoji picker"
msgstr "Abrir selector de emoji" msgstr "Abrir el selector de emoji"
#: src/screens/Profile/components/ProfileFeedHeader.tsx:190 #: src/screens/Profile/components/ProfileFeedHeader.tsx:190
msgid "Open feed info screen" msgid "Open feed info screen"
msgstr "" msgstr "Abrir la pantalla de información del feed"
#: src/screens/Profile/components/ProfileFeedHeader.tsx:290 #: src/screens/Profile/components/ProfileFeedHeader.tsx:290
#: src/screens/Profile/components/ProfileFeedHeader.tsx:295 #: src/screens/Profile/components/ProfileFeedHeader.tsx:295
msgid "Open feed options menu" msgid "Open feed options menu"
msgstr "Abrir menú de opciones del feed" msgstr "Abrir el menú de opciones del feed"
#: src/screens/Settings/Settings.tsx:208 #: src/screens/Settings/Settings.tsx:208
msgid "Open helpdesk in browser" msgid "Open helpdesk in browser"
@@ -5745,7 +5745,7 @@ msgstr "Imágenes destinadas a adultos."
#: src/screens/Profile/components/ProfileFeedHeader.tsx:519 #: src/screens/Profile/components/ProfileFeedHeader.tsx:519
#: src/screens/Profile/components/ProfileFeedHeader.tsx:526 #: src/screens/Profile/components/ProfileFeedHeader.tsx:526
msgid "Pin feed" msgid "Pin feed"
msgstr "" msgstr "Anclar el feed"
#: src/view/screens/ProfileList.tsx:685 #: src/view/screens/ProfileList.tsx:685
msgid "Pin to home" msgid "Pin to home"
@@ -5767,7 +5767,7 @@ msgstr "Anclado"
#: src/screens/Profile/components/ProfileFeedHeader.tsx:162 #: src/screens/Profile/components/ProfileFeedHeader.tsx:162
#: src/screens/Profile/components/ProfileFeedHeader.tsx:174 #: src/screens/Profile/components/ProfileFeedHeader.tsx:174
msgid "Pinned {0} to Home" msgid "Pinned {0} to Home"
msgstr "" msgstr "{0} anclados en inicio"
#: src/view/screens/SavedFeeds.tsx:123 #: src/view/screens/SavedFeeds.tsx:123
msgid "Pinned Feeds" msgid "Pinned Feeds"
@@ -6097,11 +6097,11 @@ msgstr "Público"
#: src/view/com/lists/MyLists.tsx:77 #: src/view/com/lists/MyLists.tsx:77
msgid "Public, sharable lists of users to mute or block in bulk." msgid "Public, sharable lists of users to mute or block in bulk."
msgstr "" msgstr "Listas públicas y compartibles para bloquear o silenciar a usuarios en grupo."
#: src/view/com/lists/MyLists.tsx:72 #: src/view/com/lists/MyLists.tsx:72
msgid "Public, sharable lists which can be used to drive feeds." msgid "Public, sharable lists which can be used to drive feeds."
msgstr "" msgstr "Listas públicas y compartibles que se pueden usar para impulsar feeds."
#: src/view/screens/ModerationModlists.tsx:75 #: src/view/screens/ModerationModlists.tsx:75
#~ msgid "Public, shareable lists of users to mute or block in bulk." #~ msgid "Public, shareable lists of users to mute or block in bulk."
@@ -6230,7 +6230,7 @@ msgstr "Búsquedas Recientes"
#: src/screens/Search/components/ExploreRecommendations.tsx:50 #: src/screens/Search/components/ExploreRecommendations.tsx:50
msgid "Recommended" msgid "Recommended"
msgstr "" msgstr "Recomendaciones"
#: src/screens/Messages/components/MessageListError.tsx:20 #: src/screens/Messages/components/MessageListError.tsx:20
msgid "Reconnect" msgid "Reconnect"
@@ -6426,7 +6426,7 @@ msgstr "Responder"
#: src/view/com/util/post-ctrls/PostCtrls.tsx:263 #: src/view/com/util/post-ctrls/PostCtrls.tsx:263
msgid "Reply ({0, plural, one {# reply} other {# replies}})" msgid "Reply ({0, plural, one {# reply} other {# replies}})"
msgstr "" msgstr "Responder ({0, plural, one {# respuesta} other {# respuestas}})"
#: src/view/screens/PreferencesFollowingFeed.tsx:142 #: src/view/screens/PreferencesFollowingFeed.tsx:142
#~ msgid "Reply Filters" #~ msgid "Reply Filters"
@@ -6452,7 +6452,7 @@ msgstr "La configuración de respuestas es elegida por el autor del hilo"
#: src/view/com/post-thread/PostThread.tsx:648 #: src/view/com/post-thread/PostThread.tsx:648
msgid "Reply sorting" msgid "Reply sorting"
msgstr "" msgstr "Orden de las respuestas"
#: src/view/com/post/Post.tsx:204 #: src/view/com/post/Post.tsx:204
#: src/view/com/posts/PostFeedItem.tsx:553 #: src/view/com/posts/PostFeedItem.tsx:553
@@ -6576,7 +6576,7 @@ msgstr "Republicar"
#: src/view/com/util/post-ctrls/RepostButton.tsx:74 #: src/view/com/util/post-ctrls/RepostButton.tsx:74
msgid "Repost ({0, plural, one {# repost} other {# reposts}})" msgid "Repost ({0, plural, one {# repost} other {# reposts}})"
msgstr "" msgstr "Republicar ({0, plural, one {# repost} other {# reposts}})"
#: src/screens/StarterPack/StarterPackScreen.tsx:548 #: src/screens/StarterPack/StarterPackScreen.tsx:548
#: src/view/com/util/post-ctrls/RepostButton.tsx:148 #: src/view/com/util/post-ctrls/RepostButton.tsx:148
@@ -6860,15 +6860,15 @@ msgstr "Buscar"
#: src/components/ProgressGuide/FollowDialog.tsx:745 #: src/components/ProgressGuide/FollowDialog.tsx:745
msgid "Search by name or interest" msgid "Search by name or interest"
msgstr "" msgstr "Buscar por nombre o interés"
#: src/view/screens/Feeds.tsx:441 #: src/view/screens/Feeds.tsx:441
msgid "Search feeds" msgid "Search feeds"
msgstr "" msgstr "Buscar feeds"
#: src/components/ProgressGuide/FollowDialog.tsx:575 #: src/components/ProgressGuide/FollowDialog.tsx:575
msgid "Search for \"{interestsDisplayName}\"{activeText}" msgid "Search for \"{interestsDisplayName}\"{activeText}"
msgstr "" msgstr "Buscar \"{interestsDisplayName}\"{activeText}"
#: src/view/shell/desktop/Search.tsx:201 #: src/view/shell/desktop/Search.tsx:201
msgid "Search for \"{query}\"" msgid "Search for \"{query}\""
@@ -7148,7 +7148,7 @@ msgstr "Dirección del servidor"
#: src/screens/Settings/AppIconSettings/index.tsx:178 #: src/screens/Settings/AppIconSettings/index.tsx:178
msgid "Set app icon to {0}" msgid "Set app icon to {0}"
msgstr "" msgstr "Cambiar el icono de la app a {0}"
#: src/screens/Moderation/index.tsx:290 #: src/screens/Moderation/index.tsx:290
msgid "Set birthdate" msgid "Set birthdate"
@@ -7305,7 +7305,7 @@ msgstr "Compartir código QR"
#: src/screens/Profile/components/ProfileFeedHeader.tsx:470 #: src/screens/Profile/components/ProfileFeedHeader.tsx:470
msgid "Share this feed" msgid "Share this feed"
msgstr "" msgstr "Compartir este feed"
#: src/screens/StarterPack/StarterPackScreen.tsx:416 #: src/screens/StarterPack/StarterPackScreen.tsx:416
msgid "Share this starter pack" msgid "Share this starter pack"
@@ -7436,11 +7436,11 @@ msgstr "Mostrar respuestas"
#: src/view/com/post-thread/PostThread.tsx:622 #: src/view/com/post-thread/PostThread.tsx:622
msgid "Show replies as" msgid "Show replies as"
msgstr "" msgstr "Mostrar las respuestas como"
#: src/screens/Settings/ThreadPreferences.tsx:151 #: src/screens/Settings/ThreadPreferences.tsx:151
msgid "Show replies as threaded" msgid "Show replies as threaded"
msgstr "" msgstr "Mostrar las respuestas como hilos"
#: src/screens/Settings/ThreadPreferences.tsx:126 #: src/screens/Settings/ThreadPreferences.tsx:126
msgid "Show replies by people you follow before all other replies" msgid "Show replies by people you follow before all other replies"
@@ -7570,7 +7570,7 @@ msgstr "¿Cerrar sesión?"
#: src/view/shell/NavSignupCard.tsx:47 #: src/view/shell/NavSignupCard.tsx:47
#: src/view/shell/NavSignupCard.tsx:52 #: src/view/shell/NavSignupCard.tsx:52
msgid "Create account" msgid "Create account"
msgstr "Crear cuenta" msgstr "Crear una cuenta"
#: src/view/shell/NavSignupCard.tsx:47 #: src/view/shell/NavSignupCard.tsx:47
#~ msgid "Sign up or sign in to join the conversation" #~ msgid "Sign up or sign in to join the conversation"
@@ -7655,7 +7655,7 @@ msgstr "¡Se produjo un error!"
#: src/screens/Profile/components/ProfileFeedHeader.tsx:538 #: src/screens/Profile/components/ProfileFeedHeader.tsx:538
msgid "Something wrong? Let us know." msgid "Something wrong? Let us know."
msgstr "" msgstr "¿Algo va mal? Avísanos."
#: src/App.native.tsx:122 #: src/App.native.tsx:122
#: src/App.web.tsx:97 #: src/App.web.tsx:97
@@ -7719,12 +7719,12 @@ msgstr "Iniciar un nuevo chat"
#: src/view/screens/ProfileList.tsx:817 #: src/view/screens/ProfileList.tsx:817
#: src/view/screens/ProfileList.tsx:938 #: src/view/screens/ProfileList.tsx:938
msgid "Start adding people" msgid "Start adding people"
msgstr "" msgstr "Empieza a añadir personas"
#: src/view/screens/ProfileList.tsx:824 #: src/view/screens/ProfileList.tsx:824
#: src/view/screens/ProfileList.tsx:945 #: src/view/screens/ProfileList.tsx:945
msgid "Start adding people!" msgid "Start adding people!"
msgstr "" msgstr "¡Empieza a añadir personas!"
#: src/components/dms/dialogs/SearchablePeopleList.tsx:353 #: src/components/dms/dialogs/SearchablePeopleList.tsx:353
msgid "Start chat with {displayName}" msgid "Start chat with {displayName}"
@@ -7750,7 +7750,7 @@ msgstr "Paquete de inicio de {0}"
#: src/view/com/profile/ProfileSubpageHeader.tsx:182 #: src/view/com/profile/ProfileSubpageHeader.tsx:182
msgid "Starter pack by <0/>" msgid "Starter pack by <0/>"
msgstr "" msgstr "Paquete de inicio de <0/>"
#: src/components/StarterPack/StarterPackCard.tsx:80 #: src/components/StarterPack/StarterPackCard.tsx:80
#: src/view/com/profile/ProfileSubpageHeader.tsx:180 #: src/view/com/profile/ProfileSubpageHeader.tsx:180
@@ -7902,12 +7902,12 @@ msgstr "Solo etiquetas"
#: src/screens/Settings/AppIconSettings/index.tsx:216 #: src/screens/Settings/AppIconSettings/index.tsx:216
msgid "Tap to change app icon" msgid "Tap to change app icon"
msgstr "" msgstr "Toca para cambiar el icono de la app"
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:138 #: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:138
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:174 #: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:174
msgid "Tap to close the emoji picker" msgid "Tap to close the emoji picker"
msgstr "" msgstr "Toca para cerrar el selector de emoji"
#: src/components/ProgressGuide/Toast.tsx:156 #: src/components/ProgressGuide/Toast.tsx:156
msgid "Tap to dismiss" msgid "Tap to dismiss"
@@ -7936,11 +7936,11 @@ msgstr "Toca para ver la imagen completa"
#: src/state/shell/progress-guide.tsx:233 #: src/state/shell/progress-guide.tsx:233
msgid "Task complete - 10 follows!" msgid "Task complete - 10 follows!"
msgstr "" msgstr "¡Tarea completada: 10 cuentas seguidas!"
#: src/state/shell/progress-guide.tsx:223 #: src/state/shell/progress-guide.tsx:223
msgid "Task complete - 10 likes!" msgid "Task complete - 10 likes!"
msgstr "¡Tarea completada - 10 \"me gusta\"!" msgstr "¡Tarea completada: 10 me gusta!"
#: src/components/ProgressGuide/List.tsx:64 #: src/components/ProgressGuide/List.tsx:64
msgid "Teach our algorithm what you like" msgid "Teach our algorithm what you like"
@@ -8051,7 +8051,7 @@ msgstr "La cuenta podrá interactuar contigo tras desbloquearla."
#: src/screens/Settings/AppIconSettings/index.tsx:41 #: src/screens/Settings/AppIconSettings/index.tsx:41
#: src/screens/Settings/AppIconSettings/index.tsx:222 #: src/screens/Settings/AppIconSettings/index.tsx:222
msgid "The app will be restarted" msgid "The app will be restarted"
msgstr "" msgstr "Se reiniciará la app"
#: src/components/moderation/ModerationDetailsDialog.tsx:127 #: src/components/moderation/ModerationDetailsDialog.tsx:127
#~ msgid "the author" #~ msgid "the author"
@@ -8417,7 +8417,7 @@ msgstr "Esta lista - creada por <0>{0}</0> - contiene posibles violaciones de la
#: src/view/screens/ProfileList.tsx:933 #: src/view/screens/ProfileList.tsx:933
msgid "This list is empty." msgid "This list is empty."
msgstr "" msgstr "Esta lista está vacía"
#: src/screens/Profile/ErrorState.tsx:40 #: src/screens/Profile/ErrorState.tsx:40
msgid "This moderation service is unavailable. See below for more details. If this issue persists, contact us." msgid "This moderation service is unavailable. See below for more details. If this issue persists, contact us."
@@ -8524,7 +8524,7 @@ msgstr "Esto eliminará tu publicación de esta cita para todos los usuarios y l
#: src/view/com/post-thread/PostThread.tsx:606 #: src/view/com/post-thread/PostThread.tsx:606
#: src/view/com/post-thread/PostThread.tsx:609 #: src/view/com/post-thread/PostThread.tsx:609
msgid "Thread options" msgid "Thread options"
msgstr "" msgstr "Opciones de hilos"
#: src/screens/Settings/ContentAndMediaSettings.tsx:66 #: src/screens/Settings/ContentAndMediaSettings.tsx:66
#: src/screens/Settings/ContentAndMediaSettings.tsx:69 #: src/screens/Settings/ContentAndMediaSettings.tsx:69
@@ -8542,7 +8542,7 @@ msgstr "Preferencias de hilos"
#: src/view/com/post-thread/PostThread.tsx:636 #: src/view/com/post-thread/PostThread.tsx:636
#: src/view/com/post-thread/PostThread.tsx:641 #: src/view/com/post-thread/PostThread.tsx:641
msgid "Threaded" msgid "Threaded"
msgstr "" msgstr "En hilos"
#: src/screens/Settings/ThreadPreferences.tsx:142 #: src/screens/Settings/ThreadPreferences.tsx:142
msgid "Threaded mode" msgid "Threaded mode"
@@ -8604,7 +8604,7 @@ msgstr "Top"
#: src/Navigation.tsx:383 #: src/Navigation.tsx:383
msgid "Topic" msgid "Topic"
msgstr "" msgstr "Tema"
#: src/view/com/modals/EditImage.tsx:272 #: src/view/com/modals/EditImage.tsx:272
#~ msgid "Transformations" #~ msgid "Transformations"
@@ -8622,7 +8622,7 @@ msgstr "Traducir"
#: src/screens/Search/components/ExploreTrendingTopics.tsx:69 #: src/screens/Search/components/ExploreTrendingTopics.tsx:69
#: src/view/shell/desktop/SidebarTrendingTopics.tsx:59 #: src/view/shell/desktop/SidebarTrendingTopics.tsx:59
msgid "Trending" msgid "Trending"
msgstr "" msgstr "Tendencias"
#: src/view/com/util/error/ErrorScreen.tsx:103 #: src/view/com/util/error/ErrorScreen.tsx:103
msgctxt "action" msgctxt "action"
@@ -8716,7 +8716,7 @@ msgstr "Deshacer repost"
#: src/view/com/util/post-ctrls/RepostButton.tsx:68 #: src/view/com/util/post-ctrls/RepostButton.tsx:68
msgid "Undo repost ({0, plural, one {# repost} other {# reposts}})" msgid "Undo repost ({0, plural, one {# repost} other {# reposts}})"
msgstr "" msgstr "Deshacer republicación ({0, plural, one {# repost} other {# reposts}})"
#: src/view/com/profile/FollowButton.tsx:60 #: src/view/com/profile/FollowButton.tsx:60
msgctxt "action" msgctxt "action"
@@ -8742,7 +8742,7 @@ msgstr "No me gusta"
#: src/view/com/util/post-ctrls/PostCtrls.tsx:305 #: src/view/com/util/post-ctrls/PostCtrls.tsx:305
msgid "Unlike ({0, plural, one {# like} other {# likes}})" msgid "Unlike ({0, plural, one {# like} other {# likes}})"
msgstr "" msgstr "No me gusta ({0, plural, one {# me gusta} other {# me gusta}})"
#: src/view/screens/ProfileFeed.tsx:576 #: src/view/screens/ProfileFeed.tsx:576
#~ msgid "Unlike this feed" #~ msgid "Unlike this feed"
@@ -8800,7 +8800,7 @@ msgstr "Desfijar"
#: src/screens/Profile/components/ProfileFeedHeader.tsx:519 #: src/screens/Profile/components/ProfileFeedHeader.tsx:519
#: src/screens/Profile/components/ProfileFeedHeader.tsx:526 #: src/screens/Profile/components/ProfileFeedHeader.tsx:526
msgid "Unpin feed" msgid "Unpin feed"
msgstr "" msgstr "Desfijar feed"
#: src/screens/Profile/components/ProfileFeedHeader.tsx:309 #: src/screens/Profile/components/ProfileFeedHeader.tsx:309
#: src/screens/Profile/components/ProfileFeedHeader.tsx:311 #: src/screens/Profile/components/ProfileFeedHeader.tsx:311
@@ -8818,7 +8818,7 @@ msgstr "Desfijar lista de moderación"
#: src/screens/Profile/components/ProfileFeedHeader.tsx:164 #: src/screens/Profile/components/ProfileFeedHeader.tsx:164
msgid "Unpinned {0} from Home" msgid "Unpinned {0} from Home"
msgstr "" msgstr "{0} desfijado de inicio"
#: src/view/screens/ProfileList.tsx:352 #: src/view/screens/ProfileList.tsx:352
msgid "Unpinned from your feeds" msgid "Unpinned from your feeds"
@@ -9284,7 +9284,7 @@ msgstr "No pudimos encontrar resultados para ese hashtag."
#: src/screens/Topic.tsx:178 #: src/screens/Topic.tsx:178
msgid "We couldn't find any results for that topic." msgid "We couldn't find any results for that topic."
msgstr "" msgstr "No hemos encontrado resultados para ese tema."
#: src/screens/Messages/Conversation.tsx:103 #: src/screens/Messages/Conversation.tsx:103
msgid "We couldn't load this conversation" msgid "We couldn't load this conversation"
@@ -9398,7 +9398,7 @@ msgstr "¿Cómo quieres llamar a tu paquete de inicio?"
#: src/screens/Search/components/ExploreTrendingTopics.tsx:79 #: src/screens/Search/components/ExploreTrendingTopics.tsx:79
msgid "What people are posting about." msgid "What people are posting about."
msgstr "" msgstr "Los temas del momento."
#: src/view/com/auth/SplashScreen.tsx:38 #: src/view/com/auth/SplashScreen.tsx:38
#: src/view/com/auth/SplashScreen.web.tsx:99 #: src/view/com/auth/SplashScreen.web.tsx:99
@@ -9586,7 +9586,7 @@ msgstr "Puedes reactivar tu cuenta para continuar iniciando sesión. Tu perfil y
#: src/screens/Search/components/ExploreTrendingTopics.tsx:136 #: src/screens/Search/components/ExploreTrendingTopics.tsx:136
#: src/view/shell/desktop/SidebarTrendingTopics.tsx:109 #: src/view/shell/desktop/SidebarTrendingTopics.tsx:109
msgid "You can update this later from your settings." msgid "You can update this later from your settings."
msgstr "" msgstr "Puedes cambiar esta opción en Ajustes."
#: src/view/com/profile/ProfileFollowers.tsx:94 #: src/view/com/profile/ProfileFollowers.tsx:94
msgid "You do not have any followers." msgid "You do not have any followers."
@@ -9832,7 +9832,7 @@ msgstr "Has elegido ocultar una palabra o etiqueta dentro de esta publicación."
#: src/state/shell/progress-guide.tsx:234 #: src/state/shell/progress-guide.tsx:234
msgid "You've found some people to follow" msgid "You've found some people to follow"
msgstr "" msgstr "Has encontrado algunas personas a las que seguir"
#: src/view/com/posts/FollowingEndOfFeed.tsx:44 #: src/view/com/posts/FollowingEndOfFeed.tsx:44
msgid "You've reached the end of your feed! Find some more accounts to follow." msgid "You've reached the end of your feed! Find some more accounts to follow."
@@ -9880,7 +9880,7 @@ msgstr "Tu elección será guardada. Puedes cambiar esto en los ajustes luego."
#: src/screens/Settings/components/ChangeHandleDialog.tsx:496 #: src/screens/Settings/components/ChangeHandleDialog.tsx:496
msgid "Your current handle <0>{0}</0> will automatically remain reserved for you. You can switch back to it at any time from this account." msgid "Your current handle <0>{0}</0> will automatically remain reserved for you. You can switch back to it at any time from this account."
msgstr "" msgstr "Tu nombre de usuario actual, <0>{0}</0>, quedará automáticamente reservado para ti. Puedes volver a él cuando quieras desde esta cuenta."
#: src/screens/Onboarding/StepFollowingFeed.tsx:62 #: src/screens/Onboarding/StepFollowingFeed.tsx:62
#~ msgid "Your default feed is \"Following\"" #~ msgid "Your default feed is \"Following\""
File diff suppressed because it is too large Load Diff
+8 -8
View File
@@ -1437,7 +1437,7 @@ msgstr "Sulje valintaikkuna"
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:137 #: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:137
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:173 #: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:173
msgid "Close emoji picker" msgid "Close emoji picker"
msgstr "" msgstr "Sulje emojinvalitsin"
#: src/components/dialogs/GifSelect.tsx:169 #: src/components/dialogs/GifSelect.tsx:169
msgid "Close GIF dialog" msgid "Close GIF dialog"
@@ -1823,7 +1823,7 @@ msgstr "Luotu {0}"
#: src/screens/List/ListHiddenScreen.tsx:127 #: src/screens/List/ListHiddenScreen.tsx:127
msgid "Creator has been blocked" msgid "Creator has been blocked"
msgstr "" msgstr "Tekijä on estetty"
#: src/screens/Onboarding/index.tsx:26 #: src/screens/Onboarding/index.tsx:26
#: src/screens/Onboarding/state.ts:99 #: src/screens/Onboarding/state.ts:99
@@ -2321,7 +2321,7 @@ msgstr "Koulutus"
#: src/screens/List/ListHiddenScreen.tsx:141 #: src/screens/List/ListHiddenScreen.tsx:141
msgid "Either the creator of this list has blocked you or you have blocked the creator." msgid "Either the creator of this list has blocked you or you have blocked the creator."
msgstr "" msgstr "Joko listan tekijä on estänyt sinut tai sitten sinä olet estänyt listan tekijän."
#: src/screens/Settings/AccountSettings.tsx:60 #: src/screens/Settings/AccountSettings.tsx:60
#: src/screens/Signup/StepInfo/index.tsx:170 #: src/screens/Signup/StepInfo/index.tsx:170
@@ -3410,7 +3410,7 @@ msgstr "Virheellinen tai ei-tuettu julkaisutietue"
#: src/screens/Login/LoginForm.tsx:90 #: src/screens/Login/LoginForm.tsx:90
#: src/screens/Login/LoginForm.tsx:149 #: src/screens/Login/LoginForm.tsx:149
msgid "Incorrect username or password" msgid "Incorrect username or password"
msgstr "Virheellinen käyttäjätunnus tai salasana" msgstr "Väärä käyttäjätunnus tai salasana"
#: src/components/intents/VerifyEmailIntentDialog.tsx:91 #: src/components/intents/VerifyEmailIntentDialog.tsx:91
msgid "Invalid Verification Code" msgid "Invalid Verification Code"
@@ -4549,7 +4549,7 @@ msgstr "Hups!"
#: src/screens/Onboarding/StepFinished.tsx:260 #: src/screens/Onboarding/StepFinished.tsx:260
msgid "Open" msgid "Open"
msgstr "Avaa" msgstr "Avoin"
#: src/view/com/posts/AviFollowButton.tsx:86 #: src/view/com/posts/AviFollowButton.tsx:86
msgid "Open {name} profile shortcut menu" msgid "Open {name} profile shortcut menu"
@@ -4576,7 +4576,7 @@ msgstr "Avaa alavalikko"
#: src/view/com/composer/Composer.tsx:1228 #: src/view/com/composer/Composer.tsx:1228
#: src/view/com/composer/Composer.tsx:1229 #: src/view/com/composer/Composer.tsx:1229
msgid "Open emoji picker" msgid "Open emoji picker"
msgstr "Avaa emoji-valitsin" msgstr "Avaa emojinvalitsin"
#: src/screens/Profile/components/ProfileFeedHeader.tsx:190 #: src/screens/Profile/components/ProfileFeedHeader.tsx:190
msgid "Open feed info screen" msgid "Open feed info screen"
@@ -6283,7 +6283,7 @@ msgstr "Kirjaudutaanko ulos?"
#: src/view/shell/NavSignupCard.tsx:47 #: src/view/shell/NavSignupCard.tsx:47
#: src/view/shell/NavSignupCard.tsx:52 #: src/view/shell/NavSignupCard.tsx:52
msgid "Create account" msgid "Create account"
msgstr "Rekisteröidy" msgstr "Luo tili"
#: src/components/moderation/ScreenHider.tsx:91 #: src/components/moderation/ScreenHider.tsx:91
#: src/lib/moderation/useGlobalLabelStrings.ts:28 #: src/lib/moderation/useGlobalLabelStrings.ts:28
@@ -6543,7 +6543,7 @@ msgstr "Napauta vaihtaaksesi sovelluskuvakkeen"
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:138 #: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:138
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:174 #: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:174
msgid "Tap to close the emoji picker" msgid "Tap to close the emoji picker"
msgstr "" msgstr "Napauta sulkeaksesi emojinvalitsimen"
#: src/components/ProgressGuide/Toast.tsx:156 #: src/components/ProgressGuide/Toast.tsx:156
msgid "Tap to dismiss" msgid "Tap to dismiss"
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
+1 -1
View File
@@ -8062,7 +8062,7 @@ msgstr "Вы включили игнорирование этого пользо
#: src/screens/Messages/ChatList.tsx:190 #: src/screens/Messages/ChatList.tsx:190
msgid "You have no conversations yet. Start one!" msgid "You have no conversations yet. Start one!"
msgstr "У вас ещё нет бесед. Начните одну!" msgstr "У вас ещё нет бесед. Создайте первую!"
#: src/view/com/feeds/ProfileFeedgens.tsx:143 #: src/view/com/feeds/ProfileFeedgens.tsx:143
msgid "You have no feeds." msgid "You have no feeds."
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+88 -88
View File
@@ -8,14 +8,14 @@ msgstr ""
"Language: vi\n" "Language: vi\n"
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2024-11-20T08:45:59.857Z\n" "PO-Revision-Date: 2025-01-22T08:45:59.857Z\n"
"Last-Translator: vnphanquang\n" "Last-Translator: vinhphm\n"
"Language-Team: Translators in PR #2524, ManhNguyen98, vinhphm, vnphanquang\n" "Language-Team: Translators in PR #2524, ManhNguyen98, vinhphm, vnphanquang\n"
"Plural-Forms: \n" "Plural-Forms: \n"
#: src/components/ProgressGuide/FollowDialog.tsx:567 #: src/components/ProgressGuide/FollowDialog.tsx:567
msgid "(active)" msgid "(active)"
msgstr "" msgstr "(hoạt động)"
#: src/screens/Messages/components/ChatListItem.tsx:130 #: src/screens/Messages/components/ChatListItem.tsx:130
msgid "(contains embedded content)" msgid "(contains embedded content)"
@@ -32,11 +32,11 @@ msgstr "{0, plural, one {# ngày} other {# ngày}}"
#: src/screens/Profile/ProfileFollowers.tsx:40 #: src/screens/Profile/ProfileFollowers.tsx:40
msgid "{0, plural, one {# follower} other {# followers}}" msgid "{0, plural, one {# follower} other {# followers}}"
msgstr "" msgstr "{0, plural, one {# người theo dõi} other {# người theo dõi}}"
#: src/screens/Profile/ProfileFollows.tsx:40 #: src/screens/Profile/ProfileFollows.tsx:40
msgid "{0, plural, one {# following} other {# following}}" msgid "{0, plural, one {# following} other {# following}}"
msgstr "" msgstr "{0, plural, one {# đang theo dõi} other {# đang theo dõi}}"
#: src/lib/hooks/useTimeAgo.ts:146 #: src/lib/hooks/useTimeAgo.ts:146
msgid "{0, plural, one {# hour} other {# hours}}" msgid "{0, plural, one {# hour} other {# hours}}"
@@ -52,15 +52,15 @@ msgstr "{0, plural, one {# giờ} other {# giờ}}"
#: src/components/moderation/LabelsOnMe.tsx:53 #: src/components/moderation/LabelsOnMe.tsx:53
msgid "{0, plural, one {# label has} other {# labels have}} been placed on this account" msgid "{0, plural, one {# label has} other {# labels have}} been placed on this account"
msgstr "" msgstr "{0, plural, one {# nhãn} other {# nhãn}} được gắn lên tài khoản này"
#: src/components/moderation/LabelsOnMe.tsx:62 #: src/components/moderation/LabelsOnMe.tsx:62
msgid "{0, plural, one {# label has} other {# labels have}} been placed on this content" msgid "{0, plural, one {# label has} other {# labels have}} been placed on this content"
msgstr "" msgstr "{0, plural, one {# nhãn} other {# nhãn}} được gắn lên nội dung này"
#: src/screens/Post/PostLikedBy.tsx:41 #: src/screens/Post/PostLikedBy.tsx:41
msgid "{0, plural, one {# like} other {# likes}}" msgid "{0, plural, one {# like} other {# likes}}"
msgstr "" msgstr "{0, plural, one {# lượt thích} other {# lượt thích}}"
#: src/lib/hooks/useTimeAgo.ts:136 #: src/lib/hooks/useTimeAgo.ts:136
msgid "{0, plural, one {# minute} other {# minutes}}" msgid "{0, plural, one {# minute} other {# minutes}}"
@@ -72,7 +72,7 @@ msgstr "{0, plural, one {# tháng} other {# tháng}}"
#: src/screens/Post/PostQuotes.tsx:41 #: src/screens/Post/PostQuotes.tsx:41
msgid "{0, plural, one {# quote} other {# quotes}}" msgid "{0, plural, one {# quote} other {# quotes}}"
msgstr "" msgstr "{0, plural, one {# trích dẫn} other {# trích dẫn}}"
#: src/screens/Post/PostRepostedBy.tsx:41 #: src/screens/Post/PostRepostedBy.tsx:41
msgid "{0, plural, one {# repost} other {# reposts}}" msgid "{0, plural, one {# repost} other {# reposts}}"
@@ -322,7 +322,7 @@ msgstr "không thể nhắn tin cho {handle}"
#: src/view/shell/bottom-bar/BottomBarWeb.tsx:260 #: src/view/shell/bottom-bar/BottomBarWeb.tsx:260
msgid "{notificationCount} unread items" msgid "{notificationCount} unread items"
msgstr "" msgstr "{notificationCount} mục chưa đọc"
#: src/view/shell/Drawer.tsx:448 #: src/view/shell/Drawer.tsx:448
msgid "{numUnreadNotifications} unread" msgid "{numUnreadNotifications} unread"
@@ -555,7 +555,7 @@ msgstr "Thêm bài mới"
#: src/view/screens/ProfileList.tsx:903 #: src/view/screens/ProfileList.tsx:903
#: src/view/screens/ProfileList.tsx:921 #: src/view/screens/ProfileList.tsx:921
msgid "Add people" msgid "Add people"
msgstr "" msgstr "Thêm người"
#: src/screens/Home/NoFeedsPinned.tsx:99 #: src/screens/Home/NoFeedsPinned.tsx:99
msgid "Add recommended feeds" msgid "Add recommended feeds"
@@ -629,7 +629,7 @@ msgstr "Nâng cao"
#: src/view/screens/Notifications.tsx:86 #: src/view/screens/Notifications.tsx:86
msgid "All" msgid "All"
msgstr "" msgstr "Tất cả"
#: src/screens/StarterPack/StarterPackScreen.tsx:381 #: src/screens/StarterPack/StarterPackScreen.tsx:381
msgid "All accounts have been followed!" msgid "All accounts have been followed!"
@@ -651,7 +651,7 @@ msgstr "Cho phép tin nhắn mới từ"
#: src/components/dialogs/PostInteractionSettingsDialog.tsx:314 #: src/components/dialogs/PostInteractionSettingsDialog.tsx:314
msgid "Allow quote posts" msgid "Allow quote posts"
msgstr "" msgstr "Cho phép trích dẫn bài đăng"
#: src/components/dialogs/PostInteractionSettingsDialog.tsx:359 #: src/components/dialogs/PostInteractionSettingsDialog.tsx:359
msgid "Allow replies from:" msgid "Allow replies from:"
@@ -998,7 +998,7 @@ msgstr "Bạn cần xác minh email trước khi có thể nhắn tin với ngư
#: src/screens/Search/components/ExploreTrendingTopics.tsx:74 #: src/screens/Search/components/ExploreTrendingTopics.tsx:74
msgid "BETA" msgid "BETA"
msgstr "" msgstr "Thử nghiệm"
#: src/components/dialogs/BirthDateSettings.tsx:106 #: src/components/dialogs/BirthDateSettings.tsx:106
#: src/screens/Settings/AccountSettings.tsx:109 #: src/screens/Settings/AccountSettings.tsx:109
@@ -1114,7 +1114,7 @@ msgstr ""
#: src/screens/Settings/AppIconSettings/index.tsx:102 #: src/screens/Settings/AppIconSettings/index.tsx:102
msgid "Bluesky+ icons" msgid "Bluesky+ icons"
msgstr "" msgstr "Biểu tượng Bluesky+"
#: src/lib/moderation/useLabelBehaviorDescription.ts:53 #: src/lib/moderation/useLabelBehaviorDescription.ts:53
msgid "Blur images" msgid "Blur images"
@@ -1156,15 +1156,15 @@ msgstr "Xem các bảng tin khác"
#: src/components/TrendingTopics.tsx:176 #: src/components/TrendingTopics.tsx:176
msgid "Browse posts about {displayName}" msgid "Browse posts about {displayName}"
msgstr "" msgstr "Tìm bài đăng về {displayName}"
#: src/components/TrendingTopics.tsx:184 #: src/components/TrendingTopics.tsx:184
msgid "Browse posts tagged with {displayName}" msgid "Browse posts tagged with {displayName}"
msgstr "" msgstr "Tìm bài đăng được gắn thẻ với {displayName}"
#: src/components/TrendingTopics.tsx:222 #: src/components/TrendingTopics.tsx:222
msgid "Browse topic {displayName}" msgid "Browse topic {displayName}"
msgstr "" msgstr "Tìm chủ đề {displayName}"
#: src/view/com/auth/SplashScreen.web.tsx:168 #: src/view/com/auth/SplashScreen.web.tsx:168
msgid "Business" msgid "Business"
@@ -1184,7 +1184,7 @@ msgstr "Bởi {0}"
#: src/screens/Profile/components/ProfileFeedHeader.tsx:447 #: src/screens/Profile/components/ProfileFeedHeader.tsx:447
msgid "By <0>{0}</0>" msgid "By <0>{0}</0>"
msgstr "" msgstr "Bởi <0>{0}</0>"
#: src/screens/Signup/StepInfo/Policies.tsx:81 #: src/screens/Signup/StepInfo/Policies.tsx:81
msgid "By creating an account you agree to the <0>Privacy Policy</0>." msgid "By creating an account you agree to the <0>Privacy Policy</0>."
@@ -1296,7 +1296,7 @@ msgstr "Thay đổi"
#: src/screens/Settings/AppIconSettings/index.tsx:39 #: src/screens/Settings/AppIconSettings/index.tsx:39
msgid "Change app icon" msgid "Change app icon"
msgstr "" msgstr "Thay đổi biểu tượng ứng dụng"
#: src/screens/Settings/AppIconSettings/index.tsx:38 #: src/screens/Settings/AppIconSettings/index.tsx:38
#: src/screens/Settings/AppIconSettings/index.tsx:221 #: src/screens/Settings/AppIconSettings/index.tsx:221
@@ -1412,7 +1412,7 @@ msgstr "Chọn màu này làm hình đại diện của bạn"
#: src/view/screens/Feeds.tsx:728 #: src/view/screens/Feeds.tsx:728
#: src/view/screens/Search/Explore.tsx:411 #: src/view/screens/Search/Explore.tsx:411
msgid "Choose your own timeline! Feeds built by the community help you find content you love." msgid "Choose your own timeline! Feeds built by the community help you find content you love."
msgstr "" msgstr "Chọn bảng tin của riêng bạn! Các bảng tin được xây dựng bởi cộng đồng giúp bạn tìm nội dung bạn yêu thích."
#: src/screens/Signup/StepInfo/index.tsx:201 #: src/screens/Signup/StepInfo/index.tsx:201
msgid "Choose your password" msgid "Choose your password"
@@ -1468,7 +1468,7 @@ msgstr "Khí hậu"
#: src/components/dms/ChatEmptyPill.tsx:39 #: src/components/dms/ChatEmptyPill.tsx:39
msgid "Clip 🐴 clop 🐴" msgid "Clip 🐴 clop 🐴"
msgstr "Cộp 🐴 cộp 🐴" msgstr "Lộp 🐴 cộp 🐴"
#: src/components/dialogs/GifSelect.tsx:281 #: src/components/dialogs/GifSelect.tsx:281
#: src/components/dialogs/VerifyEmailDialog.tsx:289 #: src/components/dialogs/VerifyEmailDialog.tsx:289
@@ -1504,7 +1504,7 @@ msgstr "Đóng hộp thoại"
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:137 #: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:137
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:173 #: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:173
msgid "Close emoji picker" msgid "Close emoji picker"
msgstr "" msgstr "Đóng bảng chọn emoji"
#: src/components/dialogs/GifSelect.tsx:169 #: src/components/dialogs/GifSelect.tsx:169
msgid "Close GIF dialog" msgid "Close GIF dialog"
@@ -1657,7 +1657,7 @@ msgstr "Liên hệ hỗ trợ"
#: src/screens/Settings/ContentAndMediaSettings.tsx:49 #: src/screens/Settings/ContentAndMediaSettings.tsx:49
msgid "Content & Media" msgid "Content & Media"
msgstr "" msgstr "Nội dung & phương tiện"
#: src/screens/Settings/AccessibilitySettings.tsx:109 #: src/screens/Settings/AccessibilitySettings.tsx:109
#: src/screens/Settings/Settings.tsx:175 #: src/screens/Settings/Settings.tsx:175
@@ -1890,7 +1890,7 @@ msgstr "Đã tạo {0}"
#: src/screens/List/ListHiddenScreen.tsx:127 #: src/screens/List/ListHiddenScreen.tsx:127
msgid "Creator has been blocked" msgid "Creator has been blocked"
msgstr "" msgstr "Người tạo đã bị chặn"
#: src/screens/Onboarding/index.tsx:26 #: src/screens/Onboarding/index.tsx:26
#: src/screens/Onboarding/state.ts:99 #: src/screens/Onboarding/state.ts:99
@@ -1948,7 +1948,7 @@ msgstr "Mặc định"
#: src/screens/Settings/AppIconSettings/index.tsx:75 #: src/screens/Settings/AppIconSettings/index.tsx:75
msgid "Default icons" msgid "Default icons"
msgstr "" msgstr "Biểu tượng mặc định"
#: src/components/dms/MessageMenu.tsx:151 #: src/components/dms/MessageMenu.tsx:151
#: src/screens/Settings/AppPasswords.tsx:212 #: src/screens/Settings/AppPasswords.tsx:212
@@ -2246,7 +2246,7 @@ msgstr "Thả vào để thêm hình ảnh"
#: src/components/dialogs/MutedWords.tsx:153 #: src/components/dialogs/MutedWords.tsx:153
msgid "Duration:" msgid "Duration:"
msgstr "Thời gian:" msgstr "Thời hạn:"
#: src/screens/Settings/components/ChangeHandleDialog.tsx:213 #: src/screens/Settings/components/ChangeHandleDialog.tsx:213
msgid "e.g. alice" msgid "e.g. alice"
@@ -2393,7 +2393,7 @@ msgstr "Giáo dục"
#: src/screens/List/ListHiddenScreen.tsx:141 #: src/screens/List/ListHiddenScreen.tsx:141
msgid "Either the creator of this list has blocked you or you have blocked the creator." msgid "Either the creator of this list has blocked you or you have blocked the creator."
msgstr "" msgstr "Người tạo danh sách này đã chặn bạn hoặc bạn đã chặn người đó."
#: src/screens/Settings/AccountSettings.tsx:60 #: src/screens/Settings/AccountSettings.tsx:60
#: src/screens/Signup/StepInfo/index.tsx:170 #: src/screens/Signup/StepInfo/index.tsx:170
@@ -2494,7 +2494,7 @@ msgstr "Chỉ bật nguồn này"
#: src/screens/Settings/ContentAndMediaSettings.tsx:122 #: src/screens/Settings/ContentAndMediaSettings.tsx:122
#: src/screens/Settings/ContentAndMediaSettings.tsx:136 #: src/screens/Settings/ContentAndMediaSettings.tsx:136
msgid "Enable trending topics" msgid "Enable trending topics"
msgstr "" msgstr "Mở chủ đề nổi trội"
#: src/screens/Messages/Settings.tsx:130 #: src/screens/Messages/Settings.tsx:130
#: src/screens/Messages/Settings.tsx:133 #: src/screens/Messages/Settings.tsx:133
@@ -2808,7 +2808,7 @@ msgstr "Bảng tin bởi {0}"
#: src/screens/Profile/components/ProfileFeedHeader.tsx:353 #: src/screens/Profile/components/ProfileFeedHeader.tsx:353
msgid "Feed menu" msgid "Feed menu"
msgstr "" msgstr "Menu bảng tin"
#: src/components/StarterPack/Wizard/WizardListCard.tsx:54 #: src/components/StarterPack/Wizard/WizardListCard.tsx:54
msgid "Feed toggle" msgid "Feed toggle"
@@ -2847,7 +2847,7 @@ msgstr "Đã cập nhật bảng tin"
#: src/screens/Search/components/ExploreRecommendations.tsx:54 #: src/screens/Search/components/ExploreRecommendations.tsx:54
msgid "Feeds we think you might like." msgid "Feeds we think you might like."
msgstr "" msgstr "Bảng tin mà bạn có thể thích."
#: src/screens/Settings/components/ExportCarDialog.tsx:43 #: src/screens/Settings/components/ExportCarDialog.tsx:43
msgid "File saved successfully!" msgid "File saved successfully!"
@@ -2871,7 +2871,7 @@ msgstr "Tìm tài khoản đề theo dõi"
#: src/components/ProgressGuide/FollowDialog.tsx:88 #: src/components/ProgressGuide/FollowDialog.tsx:88
#: src/components/ProgressGuide/FollowDialog.tsx:417 #: src/components/ProgressGuide/FollowDialog.tsx:417
msgid "Find people to follow" msgid "Find people to follow"
msgstr "" msgstr "Tìm người để theo dõi"
#: src/view/screens/Search/Search.tsx:595 #: src/view/screens/Search/Search.tsx:595
msgid "Find posts and users on Bluesky" msgid "Find posts and users on Bluesky"
@@ -2915,7 +2915,7 @@ msgstr "Theo dõi {name}"
#: src/components/ProgressGuide/List.tsx:52 #: src/components/ProgressGuide/List.tsx:52
#: src/state/shell/progress-guide.tsx:229 #: src/state/shell/progress-guide.tsx:229
msgid "Follow 10 accounts" msgid "Follow 10 accounts"
msgstr "" msgstr "Theo dõi 10 tài khoản"
#: src/components/ProgressGuide/List.tsx:69 #: src/components/ProgressGuide/List.tsx:69
msgid "Follow 7 accounts" msgid "Follow 7 accounts"
@@ -3289,13 +3289,13 @@ msgstr "Ẩn trả lời này?"
#: src/screens/Search/components/ExploreTrendingTopics.tsx:83 #: src/screens/Search/components/ExploreTrendingTopics.tsx:83
#: src/view/shell/desktop/SidebarTrendingTopics.tsx:62 #: src/view/shell/desktop/SidebarTrendingTopics.tsx:62
msgid "Hide trending topics" msgid "Hide trending topics"
msgstr "" msgstr "Ẩn chủ đề nổi trội"
#: src/components/interstitials/Trending.tsx:99 #: src/components/interstitials/Trending.tsx:99
#: src/screens/Search/components/ExploreTrendingTopics.tsx:135 #: src/screens/Search/components/ExploreTrendingTopics.tsx:135
#: src/view/shell/desktop/SidebarTrendingTopics.tsx:108 #: src/view/shell/desktop/SidebarTrendingTopics.tsx:108
msgid "Hide trending topics?" msgid "Hide trending topics?"
msgstr "" msgstr "Ẩn chủ đề nổi trội?"
#: src/view/com/notifications/NotificationFeedItem.tsx:592 #: src/view/com/notifications/NotificationFeedItem.tsx:592
msgid "Hide user list" msgid "Hide user list"
@@ -3356,7 +3356,7 @@ msgstr "Nhà cung cấp lưu trữ"
#: src/view/com/post-thread/PostThread.tsx:652 #: src/view/com/post-thread/PostThread.tsx:652
#: src/view/com/post-thread/PostThread.tsx:657 #: src/view/com/post-thread/PostThread.tsx:657
msgid "Hot replies first" msgid "Hot replies first"
msgstr "Tr lời nổi bật trước" msgstr "Tr lời nổi bật trước"
#: src/view/com/modals/InAppBrowserConsent.tsx:41 #: src/view/com/modals/InAppBrowserConsent.tsx:41
msgid "How should we open this link?" msgid "How should we open this link?"
@@ -3405,7 +3405,7 @@ msgstr "Danh sách không thể được khôi phục sau khi đã bị xóa."
#: src/screens/Settings/components/ChangeHandleDialog.tsx:250 #: src/screens/Settings/components/ChangeHandleDialog.tsx:250
msgid "If you have your own domain, you can use that as your handle. This lets you self-verify your identity. <0>Learn more here.</0>" msgid "If you have your own domain, you can use that as your handle. This lets you self-verify your identity. <0>Learn more here.</0>"
msgstr "" msgstr "Nếu bạn có tên miền riêng, bạn có thể sử dụng nó làm tên người dùng. Điều này giúp bạn tự xác minh danh tính của mình. <0>Tìm hiểu thêm tại đây.</0>"
#: src/view/com/util/forms/PostDropdownBtnMenuItems.tsx:658 #: src/view/com/util/forms/PostDropdownBtnMenuItems.tsx:658
msgid "If you remove this post, you won't be able to recover it." msgid "If you remove this post, you won't be able to recover it."
@@ -3703,11 +3703,11 @@ msgstr "Sáng"
#: src/screens/Profile/components/ProfileFeedHeader.tsx:514 #: src/screens/Profile/components/ProfileFeedHeader.tsx:514
msgid "Like" msgid "Like"
msgstr "" msgstr "Thích"
#: src/view/com/util/post-ctrls/PostCtrls.tsx:311 #: src/view/com/util/post-ctrls/PostCtrls.tsx:311
msgid "Like ({0, plural, one {# like} other {# likes}})" msgid "Like ({0, plural, one {# like} other {# likes}})"
msgstr "" msgstr "Thích ({0, plural, one {# lượt thích} other {# lượt thích}})"
#: src/components/ProgressGuide/List.tsx:63 #: src/components/ProgressGuide/List.tsx:63
msgid "Like 10 posts" msgid "Like 10 posts"
@@ -3720,7 +3720,7 @@ msgstr "Thích 10 bài để dạy cho bảng tin Khám phá"
#: src/screens/Profile/components/ProfileFeedHeader.tsx:501 #: src/screens/Profile/components/ProfileFeedHeader.tsx:501
msgid "Like feed" msgid "Like feed"
msgstr "" msgstr "Thích bảng tin"
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:275 #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:275
msgid "Like this feed" msgid "Like this feed"
@@ -3741,14 +3741,14 @@ msgstr "Thích bởi"
#: src/components/FeedCard.tsx:213 #: src/components/FeedCard.tsx:213
#: src/view/com/feeds/FeedSourceCard.tsx:303 #: src/view/com/feeds/FeedSourceCard.tsx:303
msgid "Liked by {0, plural, one {# user} other {# users}}" msgid "Liked by {0, plural, one {# user} other {# users}}"
msgstr "" msgstr "Thích bởi {0, plural, one {# người dùng} other {# người dùng}}"
#: src/components/LabelingServiceCard/index.tsx:96 #: src/components/LabelingServiceCard/index.tsx:96
#: src/screens/Profile/components/ProfileFeedHeader.tsx:489 #: src/screens/Profile/components/ProfileFeedHeader.tsx:489
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:295 #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:295
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:309 #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:309
msgid "Liked by {likeCount, plural, one {# user} other {# users}}" msgid "Liked by {likeCount, plural, one {# user} other {# users}}"
msgstr "" msgstr "Thích bởi {likeCount, plural, one {# người dùng} other {# người dùng}}"
#: src/view/screens/Profile.tsx:234 #: src/view/screens/Profile.tsx:234
msgid "Likes" msgid "Likes"
@@ -3761,7 +3761,7 @@ msgstr "Lượt thích cho bài đăng này"
#: src/view/com/post-thread/PostThread.tsx:626 #: src/view/com/post-thread/PostThread.tsx:626
#: src/view/com/post-thread/PostThread.tsx:631 #: src/view/com/post-thread/PostThread.tsx:631
msgid "Linear" msgid "Linear"
msgstr "" msgstr "Ngang hàng"
#: src/Navigation.tsx:198 #: src/Navigation.tsx:198
msgid "List" msgid "List"
@@ -3782,11 +3782,11 @@ msgstr "Danh sách bởi {0}"
#: src/view/com/profile/ProfileSubpageHeader.tsx:156 #: src/view/com/profile/ProfileSubpageHeader.tsx:156
msgid "List by <0/>" msgid "List by <0/>"
msgstr "" msgstr "Danh sách tạo bởi <0/>"
#: src/view/com/profile/ProfileSubpageHeader.tsx:154 #: src/view/com/profile/ProfileSubpageHeader.tsx:154
msgid "List by you" msgid "List by you"
msgstr "" msgstr "Danh sách của bạn"
#: src/view/screens/ProfileList.tsx:455 #: src/view/screens/ProfileList.tsx:455
msgid "List deleted" msgid "List deleted"
@@ -3882,7 +3882,7 @@ msgstr "Đăng nhập vào tải khoản chưa được liệt kê"
#: src/view/shell/desktop/RightNav.tsx:121 #: src/view/shell/desktop/RightNav.tsx:121
msgid "Logo by @sawaratsuki.bsky.social" msgid "Logo by @sawaratsuki.bsky.social"
msgstr "" msgstr "Logo bởi @sawaratsuki.bsky.social"
#: src/view/shell/desktop/RightNav.tsx:103 #: src/view/shell/desktop/RightNav.tsx:103
#~ msgid "Logo by <0/>" #~ msgid "Logo by <0/>"
@@ -3953,7 +3953,7 @@ msgstr "Người dùng được đề cập"
#: src/view/screens/Notifications.tsx:99 #: src/view/screens/Notifications.tsx:99
msgid "Mentions" msgid "Mentions"
msgstr "" msgstr "Nhắc đến"
#: src/components/Menu/index.tsx:96 #: src/components/Menu/index.tsx:96
#: src/view/screens/Search/Search.tsx:863 #: src/view/screens/Search/Search.tsx:863
@@ -4282,7 +4282,7 @@ msgstr "Tên người dùng mới"
#: src/view/screens/Lists.tsx:66 #: src/view/screens/Lists.tsx:66
#: src/view/screens/ModerationModlists.tsx:66 #: src/view/screens/ModerationModlists.tsx:66
msgid "New list" msgid "New list"
msgstr "" msgstr "Danh sách mới"
#: src/components/dms/NewMessagesPill.tsx:92 #: src/components/dms/NewMessagesPill.tsx:92
msgid "New messages" msgid "New messages"
@@ -4668,7 +4668,7 @@ msgstr "Mở tùy chọn hội thoại"
#: src/components/Layout/Header/index.tsx:145 #: src/components/Layout/Header/index.tsx:145
msgid "Open drawer menu" msgid "Open drawer menu"
msgstr "" msgstr "Mở trình đơn"
#: src/screens/Messages/components/MessageInput.web.tsx:181 #: src/screens/Messages/components/MessageInput.web.tsx:181
#: src/view/com/composer/Composer.tsx:1228 #: src/view/com/composer/Composer.tsx:1228
@@ -4678,7 +4678,7 @@ msgstr "Mở trình chọn biểu tượng cảm xúc"
#: src/screens/Profile/components/ProfileFeedHeader.tsx:190 #: src/screens/Profile/components/ProfileFeedHeader.tsx:190
msgid "Open feed info screen" msgid "Open feed info screen"
msgstr "" msgstr "Mở màn hình thông tin bảng tin"
#: src/screens/Profile/components/ProfileFeedHeader.tsx:290 #: src/screens/Profile/components/ProfileFeedHeader.tsx:290
#: src/screens/Profile/components/ProfileFeedHeader.tsx:295 #: src/screens/Profile/components/ProfileFeedHeader.tsx:295
@@ -4912,7 +4912,7 @@ msgstr "Hình ảnh cho người lớn"
#: src/screens/Profile/components/ProfileFeedHeader.tsx:519 #: src/screens/Profile/components/ProfileFeedHeader.tsx:519
#: src/screens/Profile/components/ProfileFeedHeader.tsx:526 #: src/screens/Profile/components/ProfileFeedHeader.tsx:526
msgid "Pin feed" msgid "Pin feed"
msgstr "" msgstr "Ghim bảng tin"
#: src/view/screens/ProfileList.tsx:685 #: src/view/screens/ProfileList.tsx:685
msgid "Pin to home" msgid "Pin to home"
@@ -4934,7 +4934,7 @@ msgstr "Đã ghim"
#: src/screens/Profile/components/ProfileFeedHeader.tsx:162 #: src/screens/Profile/components/ProfileFeedHeader.tsx:162
#: src/screens/Profile/components/ProfileFeedHeader.tsx:174 #: src/screens/Profile/components/ProfileFeedHeader.tsx:174
msgid "Pinned {0} to Home" msgid "Pinned {0} to Home"
msgstr "" msgstr "Đã ghim {0} vào trang chủ"
#: src/view/screens/SavedFeeds.tsx:123 #: src/view/screens/SavedFeeds.tsx:123
msgid "Pinned Feeds" msgid "Pinned Feeds"
@@ -5226,11 +5226,11 @@ msgstr "Công khai"
#: src/view/com/lists/MyLists.tsx:77 #: src/view/com/lists/MyLists.tsx:77
msgid "Public, sharable lists of users to mute or block in bulk." msgid "Public, sharable lists of users to mute or block in bulk."
msgstr "" msgstr "Danh sách công khai, chia sẻ được của người dùng để cho việc ẩn hoặc chặn hàng loạt."
#: src/view/com/lists/MyLists.tsx:72 #: src/view/com/lists/MyLists.tsx:72
msgid "Public, sharable lists which can be used to drive feeds." msgid "Public, sharable lists which can be used to drive feeds."
msgstr "" msgstr "Danh sách công khai và chia sẻ được có thể thúc đẩy bảng tin."
#: src/view/screens/ModerationModlists.tsx:75 #: src/view/screens/ModerationModlists.tsx:75
#~ msgid "Public, shareable lists of users to mute or block in bulk." #~ msgid "Public, shareable lists of users to mute or block in bulk."
@@ -5334,7 +5334,7 @@ msgstr "Tìm kiếm gần đây"
#: src/screens/Search/components/ExploreRecommendations.tsx:50 #: src/screens/Search/components/ExploreRecommendations.tsx:50
msgid "Recommended" msgid "Recommended"
msgstr "" msgstr "Đề xuất"
#: src/screens/Messages/components/MessageListError.tsx:20 #: src/screens/Messages/components/MessageListError.tsx:20
msgid "Reconnect" msgid "Reconnect"
@@ -5506,7 +5506,7 @@ msgstr "Trả lời"
#: src/view/com/util/post-ctrls/PostCtrls.tsx:263 #: src/view/com/util/post-ctrls/PostCtrls.tsx:263
msgid "Reply ({0, plural, one {# reply} other {# replies}})" msgid "Reply ({0, plural, one {# reply} other {# replies}})"
msgstr "" msgstr "Trả lời ({0, plural, one {# trả lời} other {# trả lời}})"
#: src/components/moderation/ModerationDetailsDialog.tsx:114 #: src/components/moderation/ModerationDetailsDialog.tsx:114
#: src/lib/moderation/useModerationCauseDescription.ts:123 #: src/lib/moderation/useModerationCauseDescription.ts:123
@@ -5528,7 +5528,7 @@ msgstr "Cài đặt trả lời được chọn bởi tác giả của thảo lu
#: src/view/com/post-thread/PostThread.tsx:648 #: src/view/com/post-thread/PostThread.tsx:648
msgid "Reply sorting" msgid "Reply sorting"
msgstr "" msgstr "Sắp xếp trả lời"
#: src/view/com/post/Post.tsx:204 #: src/view/com/post/Post.tsx:204
#: src/view/com/posts/PostFeedItem.tsx:553 #: src/view/com/posts/PostFeedItem.tsx:553
@@ -5647,7 +5647,7 @@ msgstr "Đăng lại"
#: src/view/com/util/post-ctrls/RepostButton.tsx:74 #: src/view/com/util/post-ctrls/RepostButton.tsx:74
msgid "Repost ({0, plural, one {# repost} other {# reposts}})" msgid "Repost ({0, plural, one {# repost} other {# reposts}})"
msgstr "" msgstr "Đăng lại ({0, plural, one {# đăng lại} other {# đăng lại}})"
#: src/screens/StarterPack/StarterPackScreen.tsx:548 #: src/screens/StarterPack/StarterPackScreen.tsx:548
#: src/view/com/util/post-ctrls/RepostButton.tsx:148 #: src/view/com/util/post-ctrls/RepostButton.tsx:148
@@ -5890,15 +5890,15 @@ msgstr "Tìm kiếm"
#: src/components/ProgressGuide/FollowDialog.tsx:745 #: src/components/ProgressGuide/FollowDialog.tsx:745
msgid "Search by name or interest" msgid "Search by name or interest"
msgstr "" msgstr "Tìm theo tên hoặc sở thích"
#: src/view/screens/Feeds.tsx:441 #: src/view/screens/Feeds.tsx:441
msgid "Search feeds" msgid "Search feeds"
msgstr "" msgstr "Tìm bảng tin"
#: src/components/ProgressGuide/FollowDialog.tsx:575 #: src/components/ProgressGuide/FollowDialog.tsx:575
msgid "Search for \"{interestsDisplayName}\"{activeText}" msgid "Search for \"{interestsDisplayName}\"{activeText}"
msgstr "" msgstr "Tìm kiếm \"{interestsDisplayName}\"{activeText}"
#: src/view/shell/desktop/Search.tsx:201 #: src/view/shell/desktop/Search.tsx:201
msgid "Search for \"{query}\"" msgid "Search for \"{query}\""
@@ -5961,7 +5961,7 @@ msgstr "Xem hướng dẫn này"
#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/Scrubber.tsx:194 #: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/Scrubber.tsx:194
msgid "Seek slider. Use the arrow keys to seek forwards and backwards, and space to play/pause" msgid "Seek slider. Use the arrow keys to seek forwards and backwards, and space to play/pause"
msgstr "" msgstr "Thanh trượt. Sử dụng các phím mũi tên để tìm kiếm về phía trước và phía sau, và phím cách để phát / tạm dừng"
#: src/screens/Onboarding/StepProfile/AvatarCreatorItems.tsx:67 #: src/screens/Onboarding/StepProfile/AvatarCreatorItems.tsx:67
msgid "Select a color" msgid "Select a color"
@@ -6125,7 +6125,7 @@ msgstr "Địa chỉ máy chủ"
#: src/screens/Settings/AppIconSettings/index.tsx:178 #: src/screens/Settings/AppIconSettings/index.tsx:178
msgid "Set app icon to {0}" msgid "Set app icon to {0}"
msgstr "" msgstr "Đặt biểu tượng ứng dụng thành {0}"
#: src/screens/Moderation/index.tsx:290 #: src/screens/Moderation/index.tsx:290
msgid "Set birthdate" msgid "Set birthdate"
@@ -6218,7 +6218,7 @@ msgstr "Chia sẻ mã QR"
#: src/screens/Profile/components/ProfileFeedHeader.tsx:470 #: src/screens/Profile/components/ProfileFeedHeader.tsx:470
msgid "Share this feed" msgid "Share this feed"
msgstr "" msgstr "Chia sẻ bảng tin này"
#: src/screens/StarterPack/StarterPackScreen.tsx:416 #: src/screens/StarterPack/StarterPackScreen.tsx:416
msgid "Share this starter pack" msgid "Share this starter pack"
@@ -6313,11 +6313,11 @@ msgstr "Hiển thị trả lời"
#: src/view/com/post-thread/PostThread.tsx:622 #: src/view/com/post-thread/PostThread.tsx:622
msgid "Show replies as" msgid "Show replies as"
msgstr "" msgstr "Hiển thị trả lời dưới dạng"
#: src/screens/Settings/ThreadPreferences.tsx:151 #: src/screens/Settings/ThreadPreferences.tsx:151
msgid "Show replies as threaded" msgid "Show replies as threaded"
msgstr "" msgstr "Hiển thị trả lời dưới dạng phân luồng"
#: src/screens/Settings/ThreadPreferences.tsx:126 #: src/screens/Settings/ThreadPreferences.tsx:126
msgid "Show replies by people you follow before all other replies" msgid "Show replies by people you follow before all other replies"
@@ -6479,7 +6479,7 @@ msgstr "Có gì đó không đúng!"
#: src/screens/Profile/components/ProfileFeedHeader.tsx:538 #: src/screens/Profile/components/ProfileFeedHeader.tsx:538
msgid "Something wrong? Let us know." msgid "Something wrong? Let us know."
msgstr "" msgstr "Có gì không đúng? Hãy cho chúng tôi biết."
#: src/App.native.tsx:122 #: src/App.native.tsx:122
#: src/App.web.tsx:97 #: src/App.web.tsx:97
@@ -6523,12 +6523,12 @@ msgstr "Bắt đầu cuộc trò chuyện mới"
#: src/view/screens/ProfileList.tsx:817 #: src/view/screens/ProfileList.tsx:817
#: src/view/screens/ProfileList.tsx:938 #: src/view/screens/ProfileList.tsx:938
msgid "Start adding people" msgid "Start adding people"
msgstr "" msgstr "Bắt đầu thêm người"
#: src/view/screens/ProfileList.tsx:824 #: src/view/screens/ProfileList.tsx:824
#: src/view/screens/ProfileList.tsx:945 #: src/view/screens/ProfileList.tsx:945
msgid "Start adding people!" msgid "Start adding people!"
msgstr "" msgstr "Bắt đầu thêm người!"
#: src/components/dms/dialogs/SearchablePeopleList.tsx:353 #: src/components/dms/dialogs/SearchablePeopleList.tsx:353
msgid "Start chat with {displayName}" msgid "Start chat with {displayName}"
@@ -6546,7 +6546,7 @@ msgstr "Gói khởi đầu bởi {0}"
#: src/view/com/profile/ProfileSubpageHeader.tsx:182 #: src/view/com/profile/ProfileSubpageHeader.tsx:182
msgid "Starter pack by <0/>" msgid "Starter pack by <0/>"
msgstr "" msgstr "Gói khởi đầu bởi <0/>"
#: src/components/StarterPack/StarterPackCard.tsx:80 #: src/components/StarterPack/StarterPackCard.tsx:80
#: src/view/com/profile/ProfileSubpageHeader.tsx:180 #: src/view/com/profile/ProfileSubpageHeader.tsx:180
@@ -6670,7 +6670,7 @@ msgstr "Chạm để thay đổi biểu tượng ứng dụng"
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:138 #: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:138
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:174 #: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:174
msgid "Tap to close the emoji picker" msgid "Tap to close the emoji picker"
msgstr "" msgstr "Chạm để đóng bảng chọn biểu tượng cảm xúc"
#: src/components/ProgressGuide/Toast.tsx:156 #: src/components/ProgressGuide/Toast.tsx:156
msgid "Tap to dismiss" msgid "Tap to dismiss"
@@ -6695,7 +6695,7 @@ msgstr "Nhấn để xem ảnh đầy đủ"
#: src/state/shell/progress-guide.tsx:233 #: src/state/shell/progress-guide.tsx:233
msgid "Task complete - 10 follows!" msgid "Task complete - 10 follows!"
msgstr "" msgstr "Hoàn thành nhiệm vụ - 10 người theo dõi!"
#: src/state/shell/progress-guide.tsx:223 #: src/state/shell/progress-guide.tsx:223
msgid "Task complete - 10 likes!" msgid "Task complete - 10 likes!"
@@ -7099,7 +7099,7 @@ msgstr "Danh sách này - tạo bởi <0>{0}</0> - có thể chứa các vi ph
#: src/view/screens/ProfileList.tsx:933 #: src/view/screens/ProfileList.tsx:933
msgid "This list is empty." msgid "This list is empty."
msgstr "" msgstr "Danh sách này đang trống."
#: src/screens/Profile/ErrorState.tsx:40 #: src/screens/Profile/ErrorState.tsx:40
msgid "This moderation service is unavailable. See below for more details. If this issue persists, contact us." msgid "This moderation service is unavailable. See below for more details. If this issue persists, contact us."
@@ -7190,7 +7190,7 @@ msgstr "Tác vụ này sẽ xóa bài đăng của bạn khỏi bài đăng trí
#: src/view/com/post-thread/PostThread.tsx:606 #: src/view/com/post-thread/PostThread.tsx:606
#: src/view/com/post-thread/PostThread.tsx:609 #: src/view/com/post-thread/PostThread.tsx:609
msgid "Thread options" msgid "Thread options"
msgstr "" msgstr "Tùy chọn thảo luận"
#: src/screens/Settings/ContentAndMediaSettings.tsx:66 #: src/screens/Settings/ContentAndMediaSettings.tsx:66
#: src/screens/Settings/ContentAndMediaSettings.tsx:69 #: src/screens/Settings/ContentAndMediaSettings.tsx:69
@@ -7204,7 +7204,7 @@ msgstr "Cài đặt thảo luận"
#: src/view/com/post-thread/PostThread.tsx:636 #: src/view/com/post-thread/PostThread.tsx:636
#: src/view/com/post-thread/PostThread.tsx:641 #: src/view/com/post-thread/PostThread.tsx:641
msgid "Threaded" msgid "Threaded"
msgstr "" msgstr "Phân luồng"
#: src/screens/Settings/ThreadPreferences.tsx:142 #: src/screens/Settings/ThreadPreferences.tsx:142
msgid "Threaded mode" msgid "Threaded mode"
@@ -7254,7 +7254,7 @@ msgstr "Hàng đầu"
#: src/Navigation.tsx:383 #: src/Navigation.tsx:383
msgid "Topic" msgid "Topic"
msgstr "" msgstr "Chủ đề"
#: src/components/dms/MessageMenu.tsx:103 #: src/components/dms/MessageMenu.tsx:103
#: src/components/dms/MessageMenu.tsx:105 #: src/components/dms/MessageMenu.tsx:105
@@ -7268,7 +7268,7 @@ msgstr "Dịch"
#: src/screens/Search/components/ExploreTrendingTopics.tsx:69 #: src/screens/Search/components/ExploreTrendingTopics.tsx:69
#: src/view/shell/desktop/SidebarTrendingTopics.tsx:59 #: src/view/shell/desktop/SidebarTrendingTopics.tsx:59
msgid "Trending" msgid "Trending"
msgstr "" msgstr "Chủ đề nổi trội"
#: src/view/com/util/error/ErrorScreen.tsx:103 #: src/view/com/util/error/ErrorScreen.tsx:103
msgctxt "action" msgctxt "action"
@@ -7293,7 +7293,7 @@ msgstr "Gõ tin nhắn của bạn ở đây"
#: src/screens/Settings/components/ChangeHandleDialog.tsx:415 #: src/screens/Settings/components/ChangeHandleDialog.tsx:415
msgid "Type:" msgid "Type:"
msgstr "Type:" msgstr "Loại:"
#: src/view/screens/ProfileList.tsx:590 #: src/view/screens/ProfileList.tsx:590
msgid "Un-block list" msgid "Un-block list"
@@ -7358,7 +7358,7 @@ msgstr "Bỏ chặn bài đăng lại"
#: src/view/com/util/post-ctrls/RepostButton.tsx:68 #: src/view/com/util/post-ctrls/RepostButton.tsx:68
msgid "Undo repost ({0, plural, one {# repost} other {# reposts}})" msgid "Undo repost ({0, plural, one {# repost} other {# reposts}})"
msgstr "" msgstr "Huỷ đăng lại ({0, plural, one {# đăng lại} other {# đăng lại}})"
#: src/view/com/profile/FollowButton.tsx:60 #: src/view/com/profile/FollowButton.tsx:60
msgctxt "action" msgctxt "action"
@@ -7376,11 +7376,11 @@ msgstr "Bỏ theo dõi tài khoản"
#: src/screens/Profile/components/ProfileFeedHeader.tsx:514 #: src/screens/Profile/components/ProfileFeedHeader.tsx:514
msgid "Unlike" msgid "Unlike"
msgstr "" msgstr "Bỏ thích"
#: src/view/com/util/post-ctrls/PostCtrls.tsx:305 #: src/view/com/util/post-ctrls/PostCtrls.tsx:305
msgid "Unlike ({0, plural, one {# like} other {# likes}})" msgid "Unlike ({0, plural, one {# like} other {# likes}})"
msgstr "" msgstr "Bỏ thích ({0, plural, one {# thích} other {# thích}})"
#: src/view/screens/ProfileFeed.tsx:576 #: src/view/screens/ProfileFeed.tsx:576
#~ msgid "Unlike this feed" #~ msgid "Unlike this feed"
@@ -7430,7 +7430,7 @@ msgstr "Bỏ ghim"
#: src/screens/Profile/components/ProfileFeedHeader.tsx:519 #: src/screens/Profile/components/ProfileFeedHeader.tsx:519
#: src/screens/Profile/components/ProfileFeedHeader.tsx:526 #: src/screens/Profile/components/ProfileFeedHeader.tsx:526
msgid "Unpin feed" msgid "Unpin feed"
msgstr "" msgstr "Bỏ ghim bảng tin"
#: src/screens/Profile/components/ProfileFeedHeader.tsx:309 #: src/screens/Profile/components/ProfileFeedHeader.tsx:309
#: src/screens/Profile/components/ProfileFeedHeader.tsx:311 #: src/screens/Profile/components/ProfileFeedHeader.tsx:311
@@ -7448,7 +7448,7 @@ msgstr "Bỏ ghim khỏi danh sách kiểm duyệt"
#: src/screens/Profile/components/ProfileFeedHeader.tsx:164 #: src/screens/Profile/components/ProfileFeedHeader.tsx:164
msgid "Unpinned {0} from Home" msgid "Unpinned {0} from Home"
msgstr "" msgstr "Đã bỏ ghim {0} khỏi Trang chủ"
#: src/view/screens/ProfileList.tsx:352 #: src/view/screens/ProfileList.tsx:352
msgid "Unpinned from your feeds" msgid "Unpinned from your feeds"
@@ -7854,7 +7854,7 @@ msgstr "Không tìm thấy nội dung nào cho hashtag."
#: src/screens/Topic.tsx:178 #: src/screens/Topic.tsx:178
msgid "We couldn't find any results for that topic." msgid "We couldn't find any results for that topic."
msgstr "" msgstr "Chúng tôi không tìm thấy kết quả nào cho chủ đề đó."
#: src/screens/Messages/Conversation.tsx:103 #: src/screens/Messages/Conversation.tsx:103
msgid "We couldn't load this conversation" msgid "We couldn't load this conversation"
@@ -7952,7 +7952,7 @@ msgstr "Bạn muốn gọi gói khởi đầu của mình là gì?"
#: src/screens/Search/components/ExploreTrendingTopics.tsx:79 #: src/screens/Search/components/ExploreTrendingTopics.tsx:79
msgid "What people are posting about." msgid "What people are posting about."
msgstr "" msgstr "Chủ đề mà mọi người đang bàn luận."
#: src/view/com/auth/SplashScreen.tsx:38 #: src/view/com/auth/SplashScreen.tsx:38
#: src/view/com/auth/SplashScreen.web.tsx:99 #: src/view/com/auth/SplashScreen.web.tsx:99
@@ -8107,7 +8107,7 @@ msgstr "Bạn có thể kích hoạt lại tài khoản của mình để tiếp
#: src/screens/Search/components/ExploreTrendingTopics.tsx:136 #: src/screens/Search/components/ExploreTrendingTopics.tsx:136
#: src/view/shell/desktop/SidebarTrendingTopics.tsx:109 #: src/view/shell/desktop/SidebarTrendingTopics.tsx:109
msgid "You can update this later from your settings." msgid "You can update this later from your settings."
msgstr "" msgstr "Bạn có thể cập nhật lại sau trong phần cài đặt."
#: src/view/com/profile/ProfileFollowers.tsx:94 #: src/view/com/profile/ProfileFollowers.tsx:94
msgid "You do not have any followers." msgid "You do not have any followers."
@@ -8325,7 +8325,7 @@ msgstr "Bạn đã chọn ẩn một từ hoặc thẻ trong bài đăng này."
#: src/state/shell/progress-guide.tsx:234 #: src/state/shell/progress-guide.tsx:234
msgid "You've found some people to follow" msgid "You've found some people to follow"
msgstr "" msgstr "Bạn đã tìm thấy một số người để theo dõi"
#: src/view/com/posts/FollowingEndOfFeed.tsx:44 #: src/view/com/posts/FollowingEndOfFeed.tsx:44
msgid "You've reached the end of your feed! Find some more accounts to follow." msgid "You've reached the end of your feed! Find some more accounts to follow."
@@ -8373,7 +8373,7 @@ msgstr "Lựa chọn của bạn sẽ được lưu, nhưng có thể được t
#: src/screens/Settings/components/ChangeHandleDialog.tsx:496 #: src/screens/Settings/components/ChangeHandleDialog.tsx:496
msgid "Your current handle <0>{0}</0> will automatically remain reserved for you. You can switch back to it at any time from this account." msgid "Your current handle <0>{0}</0> will automatically remain reserved for you. You can switch back to it at any time from this account."
msgstr "" msgstr "Tên người dùng hiện tại của bạn <0>{0}</0> sẽ tự động được giữ lại cho bạn. Bạn có thể chuyển lại vào bất kỳ lúc nào từ tài khoản này."
#: src/screens/Login/ForgotPasswordForm.tsx:51 #: src/screens/Login/ForgotPasswordForm.tsx:51
#: src/screens/Signup/state.ts:203 #: src/screens/Signup/state.ts:203
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
+1 -13
View File
@@ -1,4 +1,4 @@
import {GestureResponderEvent, Linking} from 'react-native' import {Linking} from 'react-native'
import {isNative, isWeb} from './detection' import {isNative, isWeb} from './detection'
@@ -24,15 +24,3 @@ export function clearHash() {
window.location.hash = '' window.location.hash = ''
} }
} }
export function shouldClickOpenNewTab(e: GestureResponderEvent) {
/**
* A `GestureResponderEvent`, but cast to `any` to avoid using a bunch
* of @ts-ignore below.
*/
const event = e as any
const isMiddleClick = isWeb && event.button === 1
const isMetaKey =
isWeb && (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey)
return isMetaKey || isMiddleClick
}
+1
View File
@@ -64,4 +64,5 @@ export const router = new Router({
StarterPack: '/starter-pack/:name/:rkey', StarterPack: '/starter-pack/:name/:rkey',
StarterPackShort: '/starter-pack-short/:code', StarterPackShort: '/starter-pack-short/:code',
StarterPackWizard: '/starter-pack/create', StarterPackWizard: '/starter-pack/create',
VideoFeed: '/video-feed',
}) })
+10 -2
View File
@@ -14,6 +14,7 @@ import {logger} from '#/logger'
import {isNative} from '#/platform/detection' import {isNative} from '#/platform/detection'
import {MESSAGE_SCREEN_POLL_INTERVAL} from '#/state/messages/convo/const' import {MESSAGE_SCREEN_POLL_INTERVAL} from '#/state/messages/convo/const'
import {useMessagesEventBus} from '#/state/messages/events' import {useMessagesEventBus} from '#/state/messages/events'
import {useLeftConvos} from '#/state/queries/messages/leave-conversation'
import {useListConvosQuery} from '#/state/queries/messages/list-conversations' import {useListConvosQuery} from '#/state/queries/messages/list-conversations'
import {List} from '#/view/com/util/List' import {List} from '#/view/com/util/List'
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
@@ -94,12 +95,19 @@ export function MessagesScreen({navigation, route}: Props) {
useRefreshOnFocus(refetch) useRefreshOnFocus(refetch)
const leftConvos = useLeftConvos()
const conversations = useMemo(() => { const conversations = useMemo(() => {
if (data?.pages) { if (data?.pages) {
return data.pages.flatMap(page => page.convos) return (
data.pages
.flatMap(page => page.convos)
// filter out convos that are actively being left
.filter(convo => !leftConvos.includes(convo.id))
)
} }
return [] return []
}, [data]) }, [data, leftConvos])
const onRefresh = useCallback(async () => { const onRefresh = useCallback(async () => {
setIsPTRing(true) setIsPTRing(true)
+7
View File
@@ -12,6 +12,7 @@ import {
BSKY_APP_ACCOUNT_DID, BSKY_APP_ACCOUNT_DID,
DISCOVER_SAVED_FEED, DISCOVER_SAVED_FEED,
TIMELINE_SAVED_FEED, TIMELINE_SAVED_FEED,
VIDEO_SAVED_FEED,
} from '#/lib/constants' } from '#/lib/constants'
import {useRequestNotificationsPermission} from '#/lib/notifications/notifications' import {useRequestNotificationsPermission} from '#/lib/notifications/notifications'
import {logEvent, useGate} from '#/lib/statsig/statsig' import {logEvent, useGate} from '#/lib/statsig/statsig'
@@ -111,6 +112,12 @@ export function StepFinished() {
id: TID.nextStr(), id: TID.nextStr(),
}, },
] ]
if (gate('onboarding_add_video_feed')) {
feedsToSave.push({
...VIDEO_SAVED_FEED,
id: TID.nextStr(),
})
}
// Any starter pack feeds will be pinned _after_ the defaults // Any starter pack feeds will be pinned _after_ the defaults
if (starterPack && starterPack.feeds?.length) { if (starterPack && starterPack.feeds?.length) {
+32 -3
View File
@@ -1,12 +1,14 @@
import React, {useCallback, useMemo} from 'react' import React, {useCallback, useMemo} from 'react'
import {StyleSheet, View} from 'react-native' import {StyleSheet, View} from 'react-native'
import {useAnimatedRef} from 'react-native-reanimated' import {useAnimatedRef} from 'react-native-reanimated'
import {AppBskyFeedDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useIsFocused, useNavigation} from '@react-navigation/native' import {useIsFocused, useNavigation} from '@react-navigation/native'
import {NativeStackScreenProps} from '@react-navigation/native-stack' import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {VIDEO_FEED_URIS} from '#/lib/constants'
import {usePalette} from '#/lib/hooks/usePalette' import {usePalette} from '#/lib/hooks/usePalette'
import {useSetTitle} from '#/lib/hooks/useSetTitle' import {useSetTitle} from '#/lib/hooks/useSetTitle'
import {ComposeIcon2} from '#/lib/icons' import {ComposeIcon2} from '#/lib/icons'
@@ -18,7 +20,7 @@ import {isNative} from '#/platform/detection'
import {listenSoftReset} from '#/state/events' import {listenSoftReset} from '#/state/events'
import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback' import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
import {FeedSourceFeedInfo, useFeedSourceInfoQuery} from '#/state/queries/feed' import {FeedSourceFeedInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
import {FeedDescriptor} from '#/state/queries/post-feed' import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed'
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
import { import {
usePreferencesQuery, usePreferencesQuery,
@@ -46,6 +48,11 @@ type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFeed'>
export function ProfileFeedScreen(props: Props) { export function ProfileFeedScreen(props: Props) {
const {rkey, name: handleOrDid} = props.route.params const {rkey, name: handleOrDid} = props.route.params
const feedParams: FeedParams | undefined = props.route.params.feedCacheKey
? {
feedCacheKey: props.route.params.feedCacheKey,
}
: undefined
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
@@ -96,7 +103,10 @@ export function ProfileFeedScreen(props: Props) {
return resolvedUri ? ( return resolvedUri ? (
<Layout.Screen> <Layout.Screen>
<ProfileFeedScreenIntermediate feedUri={resolvedUri.uri} /> <ProfileFeedScreenIntermediate
feedUri={resolvedUri.uri}
feedParams={feedParams}
/>
</Layout.Screen> </Layout.Screen>
) : ( ) : (
<Layout.Screen> <Layout.Screen>
@@ -108,7 +118,13 @@ export function ProfileFeedScreen(props: Props) {
) )
} }
function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) { function ProfileFeedScreenIntermediate({
feedUri,
feedParams,
}: {
feedUri: string
feedParams: FeedParams | undefined
}) {
const {data: preferences} = usePreferencesQuery() const {data: preferences} = usePreferencesQuery()
const {data: info} = useFeedSourceInfoQuery({uri: feedUri}) const {data: info} = useFeedSourceInfoQuery({uri: feedUri})
@@ -125,15 +141,18 @@ function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) {
<ProfileFeedScreenInner <ProfileFeedScreenInner
preferences={preferences} preferences={preferences}
feedInfo={info as FeedSourceFeedInfo} feedInfo={info as FeedSourceFeedInfo}
feedParams={feedParams}
/> />
) )
} }
export function ProfileFeedScreenInner({ export function ProfileFeedScreenInner({
feedInfo, feedInfo,
feedParams,
}: { }: {
preferences: UsePreferencesQueryResponse preferences: UsePreferencesQueryResponse
feedInfo: FeedSourceFeedInfo feedInfo: FeedSourceFeedInfo
feedParams: FeedParams | undefined
}) { }) {
const {_} = useLingui() const {_} = useLingui()
const {hasSession} = useSession() const {hasSession} = useSession()
@@ -170,6 +189,14 @@ export function ProfileFeedScreenInner({
return <EmptyState icon="hashtag" message={_(msg`This feed is empty.`)} /> return <EmptyState icon="hashtag" message={_(msg`This feed is empty.`)} />
}, [_]) }, [_])
const isVideoFeed = React.useMemo(() => {
const isBskyVideoFeed = VIDEO_FEED_URIS.includes(feedInfo.uri)
const feedIsVideoMode =
feedInfo.contentMode === AppBskyFeedDefs.CONTENTMODEVIDEO
const _isVideoFeed = isBskyVideoFeed || feedIsVideoMode
return isNative && _isVideoFeed
}, [feedInfo])
return ( return (
<> <>
<ProfileFeedHeader info={feedInfo} /> <ProfileFeedHeader info={feedInfo} />
@@ -177,12 +204,14 @@ export function ProfileFeedScreenInner({
<FeedFeedbackProvider value={feedFeedback}> <FeedFeedbackProvider value={feedFeedback}>
<PostFeed <PostFeed
feed={feed} feed={feed}
feedParams={feedParams}
pollInterval={60e3} pollInterval={60e3}
disablePoll={hasNew} disablePoll={hasNew}
onHasNew={setHasNew} onHasNew={setHasNew}
scrollElRef={scrollElRef} scrollElRef={scrollElRef}
onScrolledDownChange={setIsScrolledDown} onScrolledDownChange={setIsScrolledDown}
renderEmptyState={renderPostsEmpty} renderEmptyState={renderPostsEmpty}
isVideoFeed={isVideoFeed}
/> />
</FeedFeedbackProvider> </FeedFeedbackProvider>
+3 -1
View File
@@ -45,6 +45,7 @@ export const ProfileFeedSection = React.forwardRef<
const [hasNew, setHasNew] = React.useState(false) const [hasNew, setHasNew] = React.useState(false)
const [isScrolledDown, setIsScrolledDown] = React.useState(false) const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const shouldUseAdjustedNumToRender = feed.endsWith('posts_and_author_threads') const shouldUseAdjustedNumToRender = feed.endsWith('posts_and_author_threads')
const isVideoFeed = isNative && feed.endsWith('posts_with_video')
const adjustedInitialNumToRender = useInitialNumToRender({ const adjustedInitialNumToRender = useInitialNumToRender({
screenHeightOffset: headerHeight, screenHeightOffset: headerHeight,
}) })
@@ -84,11 +85,12 @@ export const ProfileFeedSection = React.forwardRef<
renderEmptyState={renderPostsEmpty} renderEmptyState={renderPostsEmpty}
headerOffset={headerHeight} headerOffset={headerHeight}
progressViewOffset={ios(0)} progressViewOffset={ios(0)}
renderEndOfFeed={ProfileEndOfFeed} renderEndOfFeed={isVideoFeed ? undefined : ProfileEndOfFeed}
ignoreFilterFor={ignoreFilterFor} ignoreFilterFor={ignoreFilterFor}
initialNumToRender={ initialNumToRender={
shouldUseAdjustedNumToRender ? adjustedInitialNumToRender : undefined shouldUseAdjustedNumToRender ? adjustedInitialNumToRender : undefined
} }
isVideoFeed={isVideoFeed}
/> />
{(isScrolledDown || hasNew) && ( {(isScrolledDown || hasNew) && (
<LoadLatestBtn <LoadLatestBtn
@@ -0,0 +1,271 @@
import React from 'react'
import {ScrollView, View} from 'react-native'
import {AppBskyEmbedVideo, AtUri} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
import {VIDEO_FEED_URI} from '#/lib/constants'
import {makeCustomFeedLink} from '#/lib/routes/links'
import {logEvent} from '#/lib/statsig/statsig'
import {isWeb} from '#/platform/detection'
import {useSavedFeeds} from '#/state/queries/feed'
import {RQKEY, usePostFeedQuery} from '#/state/queries/post-feed'
import {useAddSavedFeedsMutation} from '#/state/queries/preferences'
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
import {atoms as a, tokens, useGutters, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {GradientFill} from '#/components/GradientFill'
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
import {Pin_Stroke2_Corner0_Rounded as Pin} from '#/components/icons/Pin'
import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending2'
import {Link} from '#/components/Link'
import {Text} from '#/components/Typography'
import {
CompactVideoPostCard,
CompactVideoPostCardPlaceholder,
} from '#/components/VideoPostCard'
const CARD_WIDTH = 100
const FEED_DESC = `feedgen|${VIDEO_FEED_URI}`
const FEED_PARAMS: {
feedCacheKey: 'explore'
} = {
feedCacheKey: 'explore',
}
export function ExploreTrendingVideos() {
const t = useTheme()
const {_} = useLingui()
const gutters = useGutters([0, 'base'])
const {data, isLoading, error} = usePostFeedQuery(FEED_DESC, FEED_PARAMS)
// Refetch on tab change if nothing else is using this query.
const queryClient = useQueryClient()
useFocusEffect(() => {
return () => {
const query = queryClient
.getQueryCache()
.find({queryKey: RQKEY(FEED_DESC, FEED_PARAMS)})
if (query && query.getObserversCount() <= 1) {
query.fetch()
}
}
})
const {data: saved} = useSavedFeeds()
const isSavedAlready = React.useMemo(() => {
return !!saved?.feeds?.some(info => info.config.value === VIDEO_FEED_URI)
}, [saved])
const {mutateAsync: addSavedFeeds, isPending: isPinPending} =
useAddSavedFeedsMutation()
const pinFeed = React.useCallback(
(e: any) => {
e.preventDefault()
addSavedFeeds([
{
type: 'feed',
value: VIDEO_FEED_URI,
pinned: true,
},
])
// prevent navigation
return false
},
[addSavedFeeds],
)
if (error) {
return null
}
return (
<View style={[a.pb_xl]}>
<View
style={[
a.flex_row,
isWeb
? [a.px_lg, a.py_lg, a.pt_2xl, a.gap_md]
: [a.p_lg, a.pt_xl, a.gap_md],
a.border_b,
t.atoms.border_contrast_low,
]}>
<View style={[a.flex_1, a.gap_sm]}>
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
<Graph
size="lg"
fill={t.palette.primary_500}
style={{marginLeft: -2}}
/>
<Text style={[a.text_2xl, a.font_heavy, t.atoms.text]}>
<Trans>Trending Videos</Trans>
</Text>
<View style={[a.py_xs, a.px_sm, a.rounded_sm, a.overflow_hidden]}>
<GradientFill gradient={tokens.gradients.primary} />
<Text style={[a.text_sm, a.font_heavy, {color: 'white'}]}>
<Trans>BETA</Trans>
</Text>
</View>
</View>
<Text style={[t.atoms.text_contrast_high, a.leading_snug]}>
<Trans>Popular videos in your network.</Trans>
</Text>
</View>
</View>
<BlockDrawerGesture>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
decelerationRate="fast"
snapToInterval={CARD_WIDTH + tokens.space.sm}>
<View
style={[
a.pt_lg,
a.flex_row,
a.gap_sm,
{
paddingLeft: gutters.paddingLeft,
paddingRight: gutters.paddingRight,
},
]}>
{isLoading ? (
Array(10)
.fill(0)
.map((_, i) => (
<View key={i} style={[{width: CARD_WIDTH}]}>
<CompactVideoPostCardPlaceholder />
</View>
))
) : error || !data ? (
<Text>
<Trans>Whoops! Trending videos failed to load.</Trans>
</Text>
) : (
<VideoCards data={data} />
)}
</View>
</ScrollView>
</BlockDrawerGesture>
{!isSavedAlready && (
<View
style={[
gutters,
a.pt_lg,
a.flex_row,
a.align_center,
a.justify_between,
a.gap_xl,
]}>
<Text style={[a.flex_1, a.text_sm, a.leading_snug]}>
<Trans>
Pin the trending videos feed to your home screen for easy access
</Trans>
</Text>
<Button
disabled={isPinPending}
label={_(msg`Pin`)}
size="small"
variant="outline"
color="secondary"
onPress={pinFeed}>
<ButtonText>{_(msg`Pin`)}</ButtonText>
<ButtonIcon icon={Pin} position="right" />
</Button>
</View>
)}
</View>
)
}
function VideoCards({
data,
}: {
data: Exclude<ReturnType<typeof usePostFeedQuery>['data'], undefined>
}) {
const t = useTheme()
const {_} = useLingui()
const items = React.useMemo(() => {
return data.pages
.flatMap(page => page.slices)
.map(slice => slice.items[0])
.filter(Boolean)
.filter(item => AppBskyEmbedVideo.isView(item.post.embed))
.slice(0, 8)
}, [data])
const href = React.useMemo(() => {
const urip = new AtUri(VIDEO_FEED_URI)
return makeCustomFeedLink(urip.host, urip.rkey, undefined, 'explore')
}, [])
return (
<>
{items.map(item => (
<View key={item.post.uri} style={[{width: CARD_WIDTH}]}>
<CompactVideoPostCard
post={item.post}
moderation={item.moderation}
sourceContext={{
type: 'feedgen',
uri: VIDEO_FEED_URI,
sourceInterstitial: 'explore',
}}
onInteract={() => {
logEvent('videoCard:click', {
context: 'interstitial:explore',
})
}}
/>
</View>
))}
<View style={[{width: CARD_WIDTH * 2}]}>
<Link
to={href}
label={_(msg`View more`)}
style={[
a.justify_center,
a.align_center,
a.flex_1,
a.rounded_md,
t.atoms.bg_contrast_25,
]}>
{({pressed}) => (
<View
style={[
a.flex_row,
a.align_center,
a.gap_md,
{
opacity: pressed ? 0.6 : 1,
},
]}>
<Text style={[a.text_md]}>
<Trans>View more</Trans>
</Text>
<View
style={[
a.align_center,
a.justify_center,
a.rounded_full,
{
width: 34,
height: 34,
backgroundColor: t.palette.primary_500,
},
]}>
<ButtonIcon icon={ChevronRight} />
</View>
</View>
)}
</Link>
</View>
</>
)
}
@@ -37,8 +37,9 @@ export function ContentAndMediaSettingsScreen({}: Props) {
const inAppBrowserPref = useInAppBrowser() const inAppBrowserPref = useInAppBrowser()
const setUseInAppBrowser = useSetInAppBrowser() const setUseInAppBrowser = useSetInAppBrowser()
const {enabled: trendingEnabled} = useTrendingConfig() const {enabled: trendingEnabled} = useTrendingConfig()
const {trendingDisabled} = useTrendingSettings() const {trendingDisabled, trendingVideoDisabled} = useTrendingSettings()
const {setTrendingDisabled} = useTrendingSettingsApi() const {setTrendingDisabled, setTrendingVideoDisabled} =
useTrendingSettingsApi()
return ( return (
<Layout.Screen> <Layout.Screen>
@@ -138,6 +139,27 @@ export function ContentAndMediaSettingsScreen({}: Props) {
<Toggle.Platform /> <Toggle.Platform />
</SettingsList.Item> </SettingsList.Item>
</Toggle.Item> </Toggle.Item>
<Toggle.Item
name="show_trending_videos"
label={_(msg`Enable trending videos in your Discover feed.`)}
value={!trendingVideoDisabled}
onChange={value => {
const hide = Boolean(!value)
if (hide) {
logEvent('trendingVideos:hide', {context: 'settings'})
} else {
logEvent('trendingVideos:show', {context: 'settings'})
}
setTrendingVideoDisabled(hide)
}}>
<SettingsList.Item>
<SettingsList.ItemIcon icon={Graph} />
<SettingsList.ItemText>
<Trans>Enable trending videos in your Discover feed</Trans>
</SettingsList.ItemText>
<Toggle.Platform />
</SettingsList.Item>
</Toggle.Item>
</> </>
)} )}
</SettingsList.Container> </SettingsList.Container>
+1 -2
View File
@@ -80,9 +80,8 @@ export function StepCaptcha() {
<View <View
style={[ style={[
a.w_full, a.w_full,
a.pb_xl,
a.overflow_hidden, a.overflow_hidden,
{minHeight: 500}, {minHeight: 510},
completed && [a.align_center, a.justify_center], completed && [a.align_center, a.justify_center],
]}> ]}>
{!completed ? ( {!completed ? (
+21 -24
View File
@@ -85,6 +85,21 @@ export function SignupQueued() {
</Button> </Button>
) )
const logoutBtn = (
<Button
variant="ghost"
size="large"
color="primary"
label={_(msg`Log out`)}
onPress={() => logoutCurrentAccount('SignupQueued')}>
<ButtonText>
<Trans>Log out</Trans>
</ButtonText>
</Button>
)
const webLayout = isWeb && gtMobile
return ( return (
<Modal <Modal
visible visible
@@ -108,7 +123,7 @@ export function SignupQueued() {
<Logo width={120} /> <Logo width={120} />
</View> </View>
<Text style={[a.text_4xl, a.font_bold, a.pb_sm]}> <Text style={[a.text_4xl, a.font_heavy, a.pb_sm]}>
<Trans>You're in line</Trans> <Trans>You're in line</Trans>
</Text> </Text>
<P style={[t.atoms.text_contrast_medium]}> <P style={[t.atoms.text_contrast_medium]}>
@@ -153,7 +168,7 @@ export function SignupQueued() {
</P> </P>
</View> </View>
{isWeb && gtMobile && ( {webLayout && (
<View <View
style={[ style={[
a.w_full, a.w_full,
@@ -162,15 +177,7 @@ export function SignupQueued() {
a.pt_5xl, a.pt_5xl,
{paddingBottom: 200}, {paddingBottom: 200},
]}> ]}>
<Button {logoutBtn}
variant="ghost"
size="large"
label={_(msg`Log out`)}
onPress={() => logoutCurrentAccount('SignupQueued')}>
<ButtonText style={[{color: t.palette.primary_500}]}>
<Trans>Log out</Trans>
</ButtonText>
</Button>
{checkBtn} {checkBtn}
</View> </View>
)} )}
@@ -178,27 +185,17 @@ export function SignupQueued() {
</View> </View>
</ScrollView> </ScrollView>
{(!isWeb || !gtMobile) && ( {!webLayout && (
<View <View
style={[ style={[
a.align_center, a.align_center,
t.atoms.bg, t.atoms.bg,
gtMobile ? a.px_5xl : a.px_xl, gtMobile ? a.px_5xl : a.px_xl,
{ {paddingBottom: Math.max(insets.bottom, a.pb_5xl.paddingBottom)},
paddingBottom: Math.max(insets.bottom, a.pb_5xl.paddingBottom),
},
]}> ]}>
<View style={[a.w_full, a.gap_sm, {maxWidth: COL_WIDTH}]}> <View style={[a.w_full, a.gap_sm, {maxWidth: COL_WIDTH}]}>
{checkBtn} {checkBtn}
<Button {logoutBtn}
variant="ghost"
size="large"
label={_(msg`Log out`)}
onPress={() => logoutCurrentAccount('SignupQueued')}>
<ButtonText style={[{color: t.palette.primary_500}]}>
<Trans>Log out</Trans>
</ButtonText>
</Button>
</View> </View>
</View> </View>
)} )}
+263
View File
@@ -0,0 +1,263 @@
import {useMemo, useState} from 'react'
import {Modal, View} from 'react-native'
import {KeyboardAwareScrollView} from 'react-native-keyboard-controller'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {StatusBar} from 'expo-status-bar'
import {ComAtprotoAdminDefs, ComAtprotoModerationDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useMutation} from '@tanstack/react-query'
import Graphemer from 'graphemer'
import {MAX_REPORT_REASON_GRAPHEME_LENGTH} from '#/lib/constants'
import {useEnableKeyboardController} from '#/lib/hooks/useEnableKeyboardController'
import {cleanError} from '#/lib/strings/errors'
import {isIOS, isWeb} from '#/platform/detection'
import {useAgent, useSession, useSessionApi} from '#/state/session'
import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
import {Logo} from '#/view/icons/Logo'
import {atoms as a, native, useBreakpoints, useTheme, web} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as TextField from '#/components/forms/TextField'
import {InlineLinkText} from '#/components/Link'
import {Loader} from '#/components/Loader'
import {P, Text} from '#/components/Typography'
const COL_WIDTH = 400
export function Takendown() {
const {_} = useLingui()
const t = useTheme()
const insets = useSafeAreaInsets()
const {gtMobile} = useBreakpoints()
const {currentAccount} = useSession()
const {logoutCurrentAccount} = useSessionApi()
const agent = useAgent()
const [isAppealling, setIsAppealling] = useState(false)
const [reason, setReason] = useState('')
const graphemer = useMemo(() => new Graphemer(), [])
const reasonGraphemeLength = useMemo(() => {
return graphemer.countGraphemes(reason)
}, [graphemer, reason])
const {
mutate: submitAppeal,
isPending,
isSuccess,
error,
} = useMutation({
mutationFn: async (appealText: string) => {
if (!currentAccount) throw new Error('No session')
await agent.com.atproto.moderation.createReport({
reasonType: ComAtprotoModerationDefs.REASONAPPEAL,
subject: {
$type: 'com.atproto.admin.defs#repoRef',
did: currentAccount.did,
} satisfies ComAtprotoAdminDefs.RepoRef,
reason: appealText,
})
},
onSuccess: () => setReason(''),
})
const primaryBtn =
isAppealling && !isSuccess ? (
<Button
variant="solid"
color="primary"
size="large"
label={_(msg`Submit appeal`)}
onPress={() => submitAppeal(reason)}
disabled={
isPending || reasonGraphemeLength > MAX_REPORT_REASON_GRAPHEME_LENGTH
}>
<ButtonText>
<Trans>Submit Appeal</Trans>
</ButtonText>
{isPending && <ButtonIcon icon={Loader} />}
</Button>
) : (
<Button
variant="solid"
size="large"
color="secondary_inverted"
label={_(msg`Log out`)}
onPress={() => logoutCurrentAccount('Takendown')}>
<ButtonText>
<Trans>Log Out</Trans>
</ButtonText>
</Button>
)
const secondaryBtn = isAppealling ? (
!isSuccess && (
<Button
variant="ghost"
size="large"
color="secondary"
label={_(msg`Cancel`)}
onPress={() => setIsAppealling(false)}>
<ButtonText>
<Trans>Cancel</Trans>
</ButtonText>
</Button>
)
) : (
<Button
variant="ghost"
size="large"
color="secondary"
label={_(msg`Appeal suspension`)}
onPress={() => setIsAppealling(true)}>
<ButtonText>
<Trans>Appeal Suspension</Trans>
</ButtonText>
</Button>
)
const webLayout = isWeb && gtMobile
useEnableKeyboardController(true)
return (
<Modal
visible
animationType={native('slide')}
presentationStyle="formSheet"
style={[web(a.util_screen_outer)]}>
{isIOS && <StatusBar style="light" />}
<KeyboardAwareScrollView style={[a.flex_1, t.atoms.bg]} centerContent>
<View
style={[
a.flex_row,
a.justify_center,
gtMobile ? a.pt_4xl : [a.px_xl, a.pt_4xl],
]}>
<View style={[a.flex_1, {maxWidth: COL_WIDTH, minHeight: COL_WIDTH}]}>
<View style={[a.pb_xl]}>
<Logo width={64} />
</View>
<Text style={[a.text_4xl, a.font_heavy, a.pb_md]}>
{isAppealling ? (
<Trans>Appeal suspension</Trans>
) : (
<Trans>Your account has been suspended</Trans>
)}
</Text>
{isAppealling ? (
<View style={[a.relative, a.w_full, a.mt_xl]}>
{isSuccess ? (
<P style={[t.atoms.text_contrast_medium, a.text_center]}>
<Trans>
Your appeal has been submitted. If your appeal succeeds,
you will receive an email.
</Trans>
</P>
) : (
<>
<TextField.LabelText>
<Trans>Reason for appeal</Trans>
</TextField.LabelText>
<TextField.Root
isInvalid={
reasonGraphemeLength >
MAX_REPORT_REASON_GRAPHEME_LENGTH || !!error
}>
<TextField.Input
label={_(msg`Reason for appeal`)}
defaultValue={reason}
onChangeText={setReason}
placeholder={_(msg`Why are you appealing?`)}
multiline
numberOfLines={5}
autoFocus
style={{paddingBottom: 40, minHeight: 150}}
maxLength={MAX_REPORT_REASON_GRAPHEME_LENGTH * 10}
/>
</TextField.Root>
<View
style={[
a.absolute,
a.flex_row,
a.align_center,
a.pr_md,
a.pb_sm,
{
bottom: 0,
right: 0,
},
]}>
<CharProgress
count={reasonGraphemeLength}
max={MAX_REPORT_REASON_GRAPHEME_LENGTH}
/>
</View>
</>
)}
{error && (
<Text
style={[
a.text_md,
a.leading_normal,
{color: t.palette.negative_500},
a.mt_lg,
]}>
{cleanError(error)}
</Text>
)}
</View>
) : (
<P style={[t.atoms.text_contrast_medium]}>
<Trans>
Your account was found to be in violation of the{' '}
<InlineLinkText
label={_(msg`Bluesky Social Terms of Service`)}
to="https://bsky.social/about/support/tos"
style={[a.text_md, a.leading_normal]}
overridePresentation>
Bluesky Social Terms of Service
</InlineLinkText>
. You have been sent an email outlining the specific violation
and suspension period, if applicable. You can appeal this
decision if you believe it was made in error.
</Trans>
</P>
)}
{webLayout && (
<View
style={[
a.w_full,
a.flex_row,
a.justify_between,
a.pt_5xl,
{paddingBottom: 200},
]}>
{secondaryBtn}
{primaryBtn}
</View>
)}
</View>
</View>
</KeyboardAwareScrollView>
{!webLayout && (
<View
style={[
a.align_center,
t.atoms.bg,
gtMobile ? a.px_5xl : a.px_xl,
{paddingBottom: Math.max(insets.bottom, a.pb_5xl.paddingBottom)},
]}>
<View style={[a.w_full, a.gap_sm, {maxWidth: COL_WIDTH}]}>
{primaryBtn}
{secondaryBtn}
</View>
</View>
)}
</Modal>
)
}
+180
View File
@@ -0,0 +1,180 @@
import {useCallback} from 'react'
import {GestureResponderEvent, View} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {HITSLOP_30} from '#/lib/constants'
import {NavigationProp} from '#/lib/routes/types'
import {sanitizeHandle} from '#/lib/strings/handles'
import {useFeedSourceInfoQuery} from '#/state/queries/feed'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {VideoFeedSourceContext} from '#/screens/VideoFeed/types'
import {atoms as a, useBreakpoints} from '#/alf'
import {Button, ButtonProps} from '#/components/Button'
import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeft} from '#/components/icons/Arrow'
import * as Layout from '#/components/Layout'
import {BUTTON_VISUAL_ALIGNMENT_OFFSET} from '#/components/Layout/const'
import {Text} from '#/components/Typography'
export function HeaderPlaceholder() {
return (
<View style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
<View
style={[
a.rounded_sm,
{
width: 36,
height: 36,
backgroundColor: 'white',
opacity: 0.8,
},
]}
/>
<View style={[a.flex_1, a.gap_xs]}>
<View
style={[
a.w_full,
a.rounded_xs,
{
backgroundColor: 'white',
height: 14,
width: 80,
opacity: 0.8,
},
]}
/>
<View
style={[
a.w_full,
a.rounded_xs,
{
backgroundColor: 'white',
height: 10,
width: 140,
opacity: 0.6,
},
]}
/>
</View>
</View>
)
}
export function Header({
sourceContext,
}: {
sourceContext: VideoFeedSourceContext
}) {
let content = null
switch (sourceContext.type) {
case 'feedgen': {
content = <FeedHeader sourceContext={sourceContext} />
break
}
case 'author':
// TODO
default: {
break
}
}
return (
<Layout.Header.Outer noBottomBorder>
<BackButton />
<Layout.Header.Content align="left">{content}</Layout.Header.Content>
</Layout.Header.Outer>
)
}
export function FeedHeader({
sourceContext,
}: {
sourceContext: Exclude<VideoFeedSourceContext, {type: 'author'}>
}) {
const {gtMobile} = useBreakpoints()
const {
data: info,
isLoading,
error,
} = useFeedSourceInfoQuery({uri: sourceContext.uri})
if (sourceContext.sourceInterstitial !== undefined) {
// For now, don't show the header if coming from an interstitial.
return null
}
if (isLoading) {
return <HeaderPlaceholder />
} else if (error || !info) {
return null
}
return (
<View style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
{info.avatar && <UserAvatar size={36} type="algo" avatar={info.avatar} />}
<View style={[a.flex_1]}>
<Text
style={[
a.text_md,
a.font_heavy,
a.leading_tight,
gtMobile && a.text_lg,
]}
numberOfLines={2}>
{info.displayName}
</Text>
<View style={[a.flex_row, {gap: 6}]}>
<Text
style={[a.flex_shrink, a.text_sm, a.leading_snug]}
numberOfLines={1}>
{sanitizeHandle(info.creatorHandle, '@')}
</Text>
</View>
</View>
</View>
)
}
// TODO: This customization should be a part of the layout component
export function BackButton({onPress, style, ...props}: Partial<ButtonProps>) {
const {_} = useLingui()
const navigation = useNavigation<NavigationProp>()
const onPressBack = useCallback(
(evt: GestureResponderEvent) => {
onPress?.(evt)
if (evt.defaultPrevented) return
if (navigation.canGoBack()) {
navigation.goBack()
} else {
navigation.navigate('Home')
}
},
[onPress, navigation],
)
return (
<Layout.Header.Slot>
<Button
label={_(msg`Go back`)}
size="small"
variant="ghost"
color="secondary"
shape="round"
onPress={onPressBack}
hitSlop={HITSLOP_30}
style={[
{marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET},
a.bg_transparent,
style,
]}
{...props}>
<ArrowLeft size="lg" fill="white" />
</Button>
</Layout.Header.Slot>
)
}
@@ -0,0 +1,265 @@
import {useCallback, useMemo, useState} from 'react'
import {View} from 'react-native'
import {
Gesture,
GestureDetector,
NativeGesture,
} from 'react-native-gesture-handler'
import Animated, {
interpolate,
runOnJS,
runOnUI,
SharedValue,
useAnimatedReaction,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated'
import {
useSafeAreaFrame,
useSafeAreaInsets,
} from 'react-native-safe-area-context'
import {useEventListener} from 'expo'
import {VideoPlayer} from 'expo-video'
import {formatTime} from '#/view/com/util/post-embeds/VideoEmbedInner/web-controls/utils'
import {tokens} from '#/alf'
import {atoms as a} from '#/alf'
import {Text} from '#/components/Typography'
// magic number that is roughly the min height of the write reply button
// we inset the video by this amount
export const VIDEO_PLAYER_BOTTOM_INSET = 57
export function Scrubber({
active,
player,
seekingAnimationSV,
scrollGesture,
children,
}: {
active: boolean
player?: VideoPlayer
seekingAnimationSV: SharedValue<number>
scrollGesture: NativeGesture
children?: React.ReactNode
}) {
const {width: screenWidth} = useSafeAreaFrame()
const insets = useSafeAreaInsets()
const currentTimeSV = useSharedValue(0)
const durationSV = useSharedValue(0)
const [currentSeekTime, setCurrentSeekTime] = useState(0)
const [duration, setDuration] = useState(0)
const updateTime = (currentTime: number, duration: number) => {
'worklet'
currentTimeSV.set(currentTime)
if (duration !== 0) {
durationSV.set(duration)
}
}
const isSeekingSV = useSharedValue(false)
const seekProgressSV = useSharedValue(0)
useAnimatedReaction(
() => Math.round(seekProgressSV.get()),
(progress, prevProgress) => {
if (progress !== prevProgress) {
runOnJS(setCurrentSeekTime)(progress)
}
},
)
const seekBy = useCallback(
(time: number) => {
player?.seekBy(time)
setTimeout(() => {
runOnUI(() => {
'worklet'
isSeekingSV.set(false)
seekingAnimationSV.set(withTiming(0, {duration: 500}))
})()
}, 50)
},
[player, isSeekingSV, seekingAnimationSV],
)
const scrubPanGesture = useMemo(() => {
return Gesture.Pan()
.blocksExternalGesture(scrollGesture)
.activeOffsetX([-10, 10])
.failOffsetY([-10, 10])
.onStart(() => {
'worklet'
seekProgressSV.set(currentTimeSV.get())
isSeekingSV.set(true)
seekingAnimationSV.set(withTiming(1, {duration: 500}))
})
.onUpdate(evt => {
'worklet'
const progress = evt.x / screenWidth
seekProgressSV.set(
clamp(progress * durationSV.get(), 0, durationSV.get()),
)
})
.onEnd(evt => {
'worklet'
isSeekingSV.get()
const progress = evt.x / screenWidth
const newTime = clamp(progress * durationSV.get(), 0, durationSV.get())
// optimisically set the progress bar
seekProgressSV.set(newTime)
// it's seek by, so offset by the current time
// seekBy sets isSeekingSV back to false, so no need to do that here
runOnJS(seekBy)(newTime - currentTimeSV.get())
})
}, [
scrollGesture,
seekingAnimationSV,
seekBy,
screenWidth,
currentTimeSV,
durationSV,
isSeekingSV,
seekProgressSV,
])
const timeStyle = useAnimatedStyle(() => {
return {
display: seekingAnimationSV.get() === 0 ? 'none' : 'flex',
opacity: seekingAnimationSV.get(),
}
})
const barStyle = useAnimatedStyle(() => {
const currentTime = isSeekingSV.get()
? seekProgressSV.get()
: currentTimeSV.get()
const progress = currentTime === 0 ? 0 : currentTime / durationSV.get()
const isSeeking = seekingAnimationSV.get()
return {
height: isSeeking * 3 + 1,
opacity: interpolate(isSeeking, [0, 1], [0.4, 0.6]),
width: `${progress * 100}%`,
}
})
const trackStyle = useAnimatedStyle(() => {
return {
height: seekingAnimationSV.get() * 3 + 1,
}
})
const childrenStyle = useAnimatedStyle(() => {
return {
opacity: 1 - seekingAnimationSV.get(),
}
})
return (
<>
{player && active && (
<PlayerListener
player={player}
setDuration={setDuration}
updateTime={updateTime}
/>
)}
<Animated.View
style={[
a.absolute,
{
left: 0,
right: 0,
bottom: insets.bottom + 80,
},
timeStyle,
]}
pointerEvents="none">
<Text style={[a.text_center, a.font_bold]}>
<Text style={[a.text_5xl, {fontVariant: ['tabular-nums']}]}>
{formatTime(currentSeekTime)}
</Text>
<Text style={[a.text_2xl, {opacity: 0.8}]}>{' / '}</Text>
<Text
style={[
a.text_5xl,
{opacity: 0.8},
{fontVariant: ['tabular-nums']},
]}>
{formatTime(duration)}
</Text>
</Text>
</Animated.View>
<GestureDetector gesture={scrubPanGesture}>
<View
style={[
a.relative,
a.w_full,
a.justify_end,
{
paddingBottom: insets.bottom,
minHeight:
// bottom padding
insets.bottom +
// scrubber height
tokens.space.lg +
// write reply height
VIDEO_PLAYER_BOTTOM_INSET,
},
a.z_10,
]}>
<View style={[a.w_full, a.relative]}>
<Animated.View
style={[
a.w_full,
{backgroundColor: 'white', opacity: 0.2},
trackStyle,
]}
/>
<Animated.View
style={[
a.absolute,
{top: 0, left: 0, backgroundColor: 'white'},
barStyle,
]}
/>
</View>
<Animated.View
style={[{minHeight: VIDEO_PLAYER_BOTTOM_INSET}, childrenStyle]}>
{children}
</Animated.View>
</View>
</GestureDetector>
</>
)
}
function PlayerListener({
player,
setDuration,
updateTime,
}: {
player: VideoPlayer
setDuration: (duration: number) => void
updateTime: (currentTime: number, duration: number) => void
}) {
useEventListener(player, 'timeUpdate', evt => {
const duration = player.duration
if (duration !== 0) {
setDuration(Math.round(duration))
}
runOnUI(updateTime)(evt.currentTime, duration)
})
return null
}
function clamp(num: number, min: number, max: number) {
'worklet'
return Math.min(Math.max(num, min), max)
}
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
export function VideoScreen() {
return null
}
+18
View File
@@ -0,0 +1,18 @@
import {AuthorFilter} from '#/state/queries/post-feed'
/**
* Kind of like `FeedDescriptor` but not
*/
export type VideoFeedSourceContext =
| {
type: 'feedgen'
uri: string
sourceInterstitial: 'discover' | 'explore' | 'none'
initialPostUri?: string
}
| {
type: 'author'
did: string
filter: AuthorFilter
initialPostUri?: string
}
+14 -11
View File
@@ -3,11 +3,11 @@ import {AppState, AppStateStatus} from 'react-native'
import {AppBskyFeedDefs} from '@atproto/api' import {AppBskyFeedDefs} from '@atproto/api'
import throttle from 'lodash.throttle' import throttle from 'lodash.throttle'
import {PROD_DEFAULT_FEED} from '#/lib/constants' import {FEEDBACK_FEEDS, STAGING_FEEDS} from '#/lib/constants'
import {logEvent} from '#/lib/statsig/statsig' import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger' import {logger} from '#/logger'
import {FeedDescriptor, FeedPostSliceItem} from '#/state/queries/post-feed' import {FeedDescriptor, FeedPostSliceItem} from '#/state/queries/post-feed'
import {getFeedPostSlice} from '#/view/com/posts/PostFeed' import {getItemsForFeedback} from '#/view/com/posts/PostFeed'
import {useAgent} from './session' import {useAgent} from './session'
type StateContext = { type StateContext = {
@@ -25,6 +25,7 @@ const stateContext = React.createContext<StateContext>({
export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) { export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
const agent = useAgent() const agent = useAgent()
const enabled = isDiscoverFeed(feed) && hasSession const enabled = isDiscoverFeed(feed) && hasSession
const queue = React.useRef<Set<string>>(new Set()) const queue = React.useRef<Set<string>>(new Set())
const history = React.useRef< const history = React.useRef<
// Use a WeakSet so that we don't need to clear it. // Use a WeakSet so that we don't need to clear it.
@@ -46,6 +47,11 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
const interactions = Array.from(queue.current).map(toInteraction) const interactions = Array.from(queue.current).map(toInteraction)
queue.current.clear() queue.current.clear()
let proxyDid = 'did:web:discover.bsky.app'
if (STAGING_FEEDS.includes(feed)) {
proxyDid = 'did:web:algo.pop2.bsky.app'
}
// Send to the feed // Send to the feed
agent.app.bsky.feed agent.app.bsky.feed
.sendInteractions( .sendInteractions(
@@ -54,7 +60,7 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
encoding: 'application/json', encoding: 'application/json',
headers: { headers: {
// TODO when we start sending to other feeds, we need to grab their DID -prf // TODO when we start sending to other feeds, we need to grab their DID -prf
'atproto-proxy': 'did:web:discover.bsky.app#bsky_fg', 'atproto-proxy': `${proxyDid}#bsky_fg`,
}, },
}, },
) )
@@ -68,7 +74,7 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
} }
sendOrAggregateInteractionsForStats(aggregatedStats.current, interactions) sendOrAggregateInteractionsForStats(aggregatedStats.current, interactions)
throttledFlushAggregatedStats() throttledFlushAggregatedStats()
}, [agent, throttledFlushAggregatedStats]) }, [agent, throttledFlushAggregatedStats, feed])
const sendToFeed = React.useMemo( const sendToFeed = React.useMemo(
() => () =>
@@ -96,18 +102,15 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
if (!enabled) { if (!enabled) {
return return
} }
const slice = getFeedPostSlice(feedItem) const items = getItemsForFeedback(feedItem)
if (slice === null) { for (const {item: postItem, feedContext} of items) {
return
}
for (const postItem of slice.items) {
if (!history.current.has(postItem)) { if (!history.current.has(postItem)) {
history.current.add(postItem) history.current.add(postItem)
queue.current.add( queue.current.add(
toString({ toString({
item: postItem.uri, item: postItem.uri,
event: 'app.bsky.feed.defs#interactionSeen', event: 'app.bsky.feed.defs#interactionSeen',
feedContext: slice.feedContext, feedContext,
}), }),
) )
sendToFeed() sendToFeed()
@@ -155,7 +158,7 @@ export function useFeedFeedbackContext() {
// place, we're hardcoding it to the discover feed. // place, we're hardcoding it to the discover feed.
// -prf // -prf
function isDiscoverFeed(feed: FeedDescriptor) { function isDiscoverFeed(feed: FeedDescriptor) {
return feed === `feedgen|${PROD_DEFAULT_FEED('whats-hot')}` return FEEDBACK_FEEDS.includes(feed)
} }
function toString(interaction: AppBskyFeedDefs.Interaction): string { function toString(interaction: AppBskyFeedDefs.Interaction): string {
+2
View File
@@ -126,6 +126,7 @@ const schema = z.object({
/** @deprecated */ /** @deprecated */
mutedThreads: z.array(z.string()), mutedThreads: z.array(z.string()),
trendingDisabled: z.boolean().optional(), trendingDisabled: z.boolean().optional(),
trendingVideoDisabled: z.boolean().optional(),
}) })
export type Schema = z.infer<typeof schema> export type Schema = z.infer<typeof schema>
@@ -172,6 +173,7 @@ export const defaults: Schema = {
hasCheckedForStarterPack: false, hasCheckedForStarterPack: false,
subtitlesEnabled: true, subtitlesEnabled: true,
trendingDisabled: false, trendingDisabled: false,
trendingVideoDisabled: false,
} }
export function tryParse(rawData: string): Schema | undefined { export function tryParse(rawData: string): Schema | undefined {
+17 -3
View File
@@ -4,18 +4,27 @@ import * as persisted from '#/state/persisted'
type StateContext = { type StateContext = {
trendingDisabled: Exclude<persisted.Schema['trendingDisabled'], undefined> trendingDisabled: Exclude<persisted.Schema['trendingDisabled'], undefined>
trendingVideoDisabled: Exclude<
persisted.Schema['trendingVideoDisabled'],
undefined
>
} }
type ApiContext = { type ApiContext = {
setTrendingDisabled( setTrendingDisabled(
hidden: Exclude<persisted.Schema['trendingDisabled'], undefined>, hidden: Exclude<persisted.Schema['trendingDisabled'], undefined>,
): void ): void
setTrendingVideoDisabled(
hidden: Exclude<persisted.Schema['trendingVideoDisabled'], undefined>,
): void
} }
const StateContext = React.createContext<StateContext>({ const StateContext = React.createContext<StateContext>({
trendingDisabled: Boolean(persisted.defaults.trendingDisabled), trendingDisabled: Boolean(persisted.defaults.trendingDisabled),
trendingVideoDisabled: Boolean(persisted.defaults.trendingVideoDisabled),
}) })
const ApiContext = React.createContext<ApiContext>({ const ApiContext = React.createContext<ApiContext>({
setTrendingDisabled() {}, setTrendingDisabled() {},
setTrendingVideoDisabled() {},
}) })
function usePersistedBooleanValue<T extends keyof persisted.Schema>(key: T) { function usePersistedBooleanValue<T extends keyof persisted.Schema>(key: T) {
@@ -43,14 +52,19 @@ function usePersistedBooleanValue<T extends keyof persisted.Schema>(key: T) {
export function Provider({children}: React.PropsWithChildren<{}>) { export function Provider({children}: React.PropsWithChildren<{}>) {
const [trendingDisabled, setTrendingDisabled] = const [trendingDisabled, setTrendingDisabled] =
usePersistedBooleanValue('trendingDisabled') usePersistedBooleanValue('trendingDisabled')
const [trendingVideoDisabled, setTrendingVideoDisabled] =
usePersistedBooleanValue('trendingVideoDisabled')
/* /*
* Context * Context
*/ */
const state = React.useMemo(() => ({trendingDisabled}), [trendingDisabled]) const state = React.useMemo(
() => ({trendingDisabled, trendingVideoDisabled}),
[trendingDisabled, trendingVideoDisabled],
)
const api = React.useMemo( const api = React.useMemo(
() => ({setTrendingDisabled}), () => ({setTrendingDisabled, setTrendingVideoDisabled}),
[setTrendingDisabled], [setTrendingDisabled, setTrendingVideoDisabled],
) )
return ( return (
+6
View File
@@ -48,6 +48,7 @@ export type FeedSourceFeedInfo = {
creatorHandle: string creatorHandle: string
likeCount: number | undefined likeCount: number | undefined
likeUri: string | undefined likeUri: string | undefined
contentMode: AppBskyFeedDefs.GeneratorView['contentMode']
} }
export type FeedSourceListInfo = { export type FeedSourceListInfo = {
@@ -65,6 +66,7 @@ export type FeedSourceListInfo = {
description: RichText description: RichText
creatorDid: string creatorDid: string
creatorHandle: string creatorHandle: string
contentMode: undefined
} }
export type FeedSourceInfo = FeedSourceFeedInfo | FeedSourceListInfo export type FeedSourceInfo = FeedSourceFeedInfo | FeedSourceListInfo
@@ -111,6 +113,7 @@ export function hydrateFeedGenerator(
creatorHandle: view.creator.handle, creatorHandle: view.creator.handle,
likeCount: view.likeCount, likeCount: view.likeCount,
likeUri: view.viewer?.like, likeUri: view.viewer?.like,
contentMode: view.contentMode,
} }
} }
@@ -141,6 +144,7 @@ export function hydrateList(view: AppBskyGraphDefs.ListView): FeedSourceInfo {
displayName: view.name displayName: view.name
? sanitizeDisplayName(view.name) ? sanitizeDisplayName(view.name)
: `User List by ${sanitizeHandle(view.creator.handle, '@')}`, : `User List by ${sanitizeHandle(view.creator.handle, '@')}`,
contentMode: undefined,
} }
} }
@@ -399,6 +403,7 @@ const PWI_DISCOVER_FEED_STUB: SavedFeedSourceInfo = {
id: 'pwi-discover', id: 'pwi-discover',
...DISCOVER_SAVED_FEED, ...DISCOVER_SAVED_FEED,
}, },
contentMode: undefined,
} }
const pinnedFeedInfosQueryKeyRoot = 'pinnedFeedsInfos' const pinnedFeedInfosQueryKeyRoot = 'pinnedFeedsInfos'
@@ -485,6 +490,7 @@ export function usePinnedFeedsInfos() {
likeCount: 0, likeCount: 0,
likeUri: '', likeUri: '',
savedFeed: pinnedItem, savedFeed: pinnedItem,
contentMode: undefined,
}) })
} }
} }
@@ -1,17 +1,28 @@
import {ChatBskyConvoLeaveConvo, ChatBskyConvoListConvos} from '@atproto/api' import {ChatBskyConvoLeaveConvo, ChatBskyConvoListConvos} from '@atproto/api'
import {useMutation, useQueryClient} from '@tanstack/react-query' import {
useMutation,
useMutationState,
useQueryClient,
} from '@tanstack/react-query'
import {logger} from '#/logger' import {logger} from '#/logger'
import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const' import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
import {useAgent} from '#/state/session' import {useAgent} from '#/state/session'
import {RQKEY as CONVO_LIST_KEY} from './list-conversations' import {RQKEY as CONVO_LIST_KEY} from './list-conversations'
const RQKEY_ROOT = 'leave-convo'
export function RQKEY(convoId: string | undefined) {
return [RQKEY_ROOT, convoId]
}
export function useLeaveConvo( export function useLeaveConvo(
convoId: string | undefined, convoId: string | undefined,
{ {
onSuccess, onSuccess,
onMutate,
onError, onError,
}: { }: {
onMutate?: () => void
onSuccess?: (data: ChatBskyConvoLeaveConvo.OutputSchema) => void onSuccess?: (data: ChatBskyConvoLeaveConvo.OutputSchema) => void
onError?: (error: Error) => void onError?: (error: Error) => void
}, },
@@ -20,6 +31,7 @@ export function useLeaveConvo(
const agent = useAgent() const agent = useAgent()
return useMutation({ return useMutation({
mutationKey: RQKEY(convoId),
mutationFn: async () => { mutationFn: async () => {
if (!convoId) throw new Error('No convoId provided') if (!convoId) throw new Error('No convoId provided')
@@ -51,6 +63,7 @@ export function useLeaveConvo(
} }
}, },
) )
onMutate?.()
return {prevPages} return {prevPages}
}, },
onSuccess: data => { onSuccess: data => {
@@ -77,3 +90,20 @@ export function useLeaveConvo(
}, },
}) })
} }
/**
* Gets currently pending and successful leave convo mutations
*
* @returns Array of `convoId`
*/
export function useLeftConvos() {
const pending = useMutationState({
filters: {mutationKey: [RQKEY_ROOT], status: 'pending'},
select: mutation => mutation.options.mutationKey?.[1] as string | undefined,
})
const success = useMutationState({
filters: {mutationKey: [RQKEY_ROOT], status: 'success'},
select: mutation => mutation.options.mutationKey?.[1] as string | undefined,
})
return [...pending, ...success].filter(id => id !== undefined)
}
+5 -1
View File
@@ -44,11 +44,12 @@ import {
} from './util' } from './util'
type ActorDid = string type ActorDid = string
type AuthorFilter = export type AuthorFilter =
| 'posts_with_replies' | 'posts_with_replies'
| 'posts_no_replies' | 'posts_no_replies'
| 'posts_and_author_threads' | 'posts_and_author_threads'
| 'posts_with_media' | 'posts_with_media'
| 'posts_with_video'
type FeedUri = string type FeedUri = string
type ListUri = string type ListUri = string
@@ -61,6 +62,7 @@ export type FeedDescriptor =
export interface FeedParams { export interface FeedParams {
mergeFeedEnabled?: boolean mergeFeedEnabled?: boolean
mergeFeedSources?: string[] mergeFeedSources?: string[]
feedCacheKey?: 'discover' | 'explore' | undefined
} }
type RQPageParam = {cursor: string | undefined; api: FeedAPI} | undefined type RQPageParam = {cursor: string | undefined; api: FeedAPI} | undefined
@@ -88,6 +90,7 @@ export interface FeedPostSlice {
isIncompleteThread: boolean isIncompleteThread: boolean
isFallbackMarker: boolean isFallbackMarker: boolean
feedContext: string | undefined feedContext: string | undefined
feedPostUri: string
reason?: reason?:
| AppBskyFeedDefs.ReasonRepost | AppBskyFeedDefs.ReasonRepost
| AppBskyFeedDefs.ReasonPin | AppBskyFeedDefs.ReasonPin
@@ -329,6 +332,7 @@ export function usePostFeedQuery(
isFallbackMarker: slice.isFallbackMarker, isFallbackMarker: slice.isFallbackMarker,
feedContext: slice.feedContext, feedContext: slice.feedContext,
reason: slice.reason, reason: slice.reason,
feedPostUri: slice.feedPostUri,
items: slice.items.map((item, i) => { items: slice.items.map((item, i) => {
const feedPostSliceItem: FeedPostSliceItem = { const feedPostSliceItem: FeedPostSliceItem = {
_reactKey: `${slice._reactKey}-${i}-${item.post.uri}`, _reactKey: `${slice._reactKey}-${i}-${item.post.uri}`,
+14 -7
View File
@@ -53,8 +53,9 @@ export type ThreadPost = {
uri: string uri: string
post: AppBskyFeedDefs.PostView post: AppBskyFeedDefs.PostView
record: AppBskyFeedPost.Record record: AppBskyFeedPost.Record
parent?: ThreadNode parent: ThreadNode | undefined
replies?: ThreadNode[] replies: ThreadNode[] | undefined
hasOPLike: boolean | undefined
ctx: ThreadCtx ctx: ThreadCtx
} }
@@ -255,8 +256,8 @@ export function sortThread(
if (aFetchedAt !== bFetchedAt) { if (aFetchedAt !== bFetchedAt) {
return aFetchedAt - bFetchedAt // older fetches first return aFetchedAt - bFetchedAt // older fetches first
} else if (opts.sort === 'hotness') { } else if (opts.sort === 'hotness') {
const aHotness = getHotness(a.post, aFetchedAt) const aHotness = getHotness(a, aFetchedAt)
const bHotness = getHotness(b.post, bFetchedAt /* same as aFetchedAt */) const bHotness = getHotness(b, bFetchedAt /* same as aFetchedAt */)
return bHotness - aHotness return bHotness - aHotness
} else if (opts.sort === 'oldest') { } else if (opts.sort === 'oldest') {
return a.post.indexedAt.localeCompare(b.post.indexedAt) return a.post.indexedAt.localeCompare(b.post.indexedAt)
@@ -309,16 +310,18 @@ export function sortThread(
// We want to give recent comments a real chance (and not bury them deep below the fold) // We want to give recent comments a real chance (and not bury them deep below the fold)
// while also surfacing well-liked comments from the past. In the future, we can explore // while also surfacing well-liked comments from the past. In the future, we can explore
// something more sophisticated, but we don't have much data on the client right now. // something more sophisticated, but we don't have much data on the client right now.
function getHotness(post: AppBskyFeedDefs.PostView, fetchedAt: number) { function getHotness(threadPost: ThreadPost, fetchedAt: number) {
const {post, hasOPLike} = threadPost
const hoursAgo = Math.max( const hoursAgo = Math.max(
0, 0,
(new Date(fetchedAt).getTime() - new Date(post.indexedAt).getTime()) / (new Date(fetchedAt).getTime() - new Date(post.indexedAt).getTime()) /
(1000 * 60 * 60), (1000 * 60 * 60),
) )
const likeCount = post.likeCount ?? 0 const likeCount = post.likeCount ?? 0
const likeOrder = Math.log(3 + likeCount) const likeOrder = Math.log(3 + likeCount) * (hasOPLike ? 1.45 : 1.0)
const timePenaltyExponent = 1.5 + 1.5 / (1 + Math.log(1 + likeCount)) const timePenaltyExponent = 1.5 + 1.5 / (1 + Math.log(1 + likeCount))
const timePenalty = Math.pow(hoursAgo + 2, timePenaltyExponent) const opLikeBoost = hasOPLike ? 0.8 : 1.0
const timePenalty = Math.pow(hoursAgo + 2, timePenaltyExponent * opLikeBoost)
return likeOrder / timePenalty return likeOrder / timePenalty
} }
@@ -356,6 +359,7 @@ function responseToThreadNodes(
// do not show blocked posts in replies // do not show blocked posts in replies
.filter(node => node.type !== 'blocked') .filter(node => node.type !== 'blocked')
: undefined, : undefined,
hasOPLike: Boolean(node?.threadContext?.rootAuthorLike),
ctx: { ctx: {
depth, depth,
isHighlightedPost: depth === 0, isHighlightedPost: depth === 0,
@@ -552,6 +556,7 @@ function threadNodeToPlaceholderThread(
record: node.record, record: node.record,
parent: undefined, parent: undefined,
replies: undefined, replies: undefined,
hasOPLike: undefined,
ctx: { ctx: {
depth: 0, depth: 0,
isHighlightedPost: true, isHighlightedPost: true,
@@ -573,6 +578,7 @@ function postViewToPlaceholderThread(
record: post.record as AppBskyFeedPost.Record, // validated in notifs record: post.record as AppBskyFeedPost.Record, // validated in notifs
parent: undefined, parent: undefined,
replies: undefined, replies: undefined,
hasOPLike: undefined,
ctx: { ctx: {
depth: 0, depth: 0,
isHighlightedPost: true, isHighlightedPost: true,
@@ -594,6 +600,7 @@ function embedViewRecordToPlaceholderThread(
record: record.value as AppBskyFeedPost.Record, // validated in getEmbeddedPost record: record.value as AppBskyFeedPost.Record, // validated in getEmbeddedPost
parent: undefined, parent: undefined,
replies: undefined, replies: undefined,
hasOPLike: undefined,
ctx: { ctx: {
depth: 0, depth: 0,
isHighlightedPost: true, isHighlightedPost: true,
+6 -1
View File
@@ -83,7 +83,12 @@ export async function createAgentAndLogin(
) => void, ) => void,
) { ) {
const agent = new BskyAppAgent({service}) const agent = new BskyAppAgent({service})
await agent.login({identifier, password, authFactorToken}) await agent.login({
identifier,
password,
authFactorToken,
allowTakendown: true,
})
const account = agentToSessionAccountOrThrow(agent) const account = agentToSessionAccountOrThrow(agent)
const gates = tryFetchGates(account.did, 'prefer-fresh-gates') const gates = tryFetchGates(account.did, 'prefer-fresh-gates')
+1 -1
View File
@@ -258,7 +258,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
], ],
) )
// @ts-ignore // @ts-expect-error window type is not declared, debug only
if (__DEV__ && isWeb) window.agent = state.currentAgentState.agent if (__DEV__ && isWeb) window.agent = state.currentAgentState.agent
const agent = state.currentAgentState.agent as BskyAppAgent const agent = state.currentAgentState.agent as BskyAppAgent
+13 -1
View File
@@ -1,11 +1,12 @@
import React from 'react' import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {AppBskyActorDefs} from '@atproto/api' import {AppBskyActorDefs, AppBskyFeedDefs} from '@atproto/api'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {NavigationProp, useNavigation} from '@react-navigation/native' import {NavigationProp, useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {VIDEO_FEED_URIS} from '#/lib/constants'
import {ComposeIcon2} from '#/lib/icons' import {ComposeIcon2} from '#/lib/icons'
import {getRootNavigation, getTabState, TabState} from '#/lib/routes/helpers' import {getRootNavigation, getTabState, TabState} from '#/lib/routes/helpers'
import {AllNavigatorParams} from '#/lib/routes/types' import {AllNavigatorParams} from '#/lib/routes/types'
@@ -15,6 +16,7 @@ import {isNative} from '#/platform/detection'
import {listenSoftReset} from '#/state/events' import {listenSoftReset} from '#/state/events'
import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback' import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
import {useSetHomeBadge} from '#/state/home-badge' import {useSetHomeBadge} from '#/state/home-badge'
import {SavedFeedSourceInfo} from '#/state/queries/feed'
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed' import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed'
import {truncateAndInvalidate} from '#/state/queries/util' import {truncateAndInvalidate} from '#/state/queries/util'
@@ -39,6 +41,7 @@ export function FeedPage({
renderEmptyState, renderEmptyState,
renderEndOfFeed, renderEndOfFeed,
savedFeedConfig, savedFeedConfig,
feedInfo,
}: { }: {
testID?: string testID?: string
feed: FeedDescriptor feed: FeedDescriptor
@@ -48,6 +51,7 @@ export function FeedPage({
renderEmptyState: () => JSX.Element renderEmptyState: () => JSX.Element
renderEndOfFeed?: () => JSX.Element renderEndOfFeed?: () => JSX.Element
savedFeedConfig?: AppBskyActorDefs.SavedFeed savedFeedConfig?: AppBskyActorDefs.SavedFeed
feedInfo: SavedFeedSourceInfo
}) { }) {
const {hasSession} = useSession() const {hasSession} = useSession()
const {_} = useLingui() const {_} = useLingui()
@@ -61,6 +65,13 @@ export function FeedPage({
const scrollElRef = React.useRef<ListMethods>(null) const scrollElRef = React.useRef<ListMethods>(null)
const [hasNew, setHasNew] = React.useState(false) const [hasNew, setHasNew] = React.useState(false)
const setHomeBadge = useSetHomeBadge() const setHomeBadge = useSetHomeBadge()
const isVideoFeed = React.useMemo(() => {
const isBskyVideoFeed = VIDEO_FEED_URIS.includes(feedInfo.uri)
const feedIsVideoMode =
feedInfo.contentMode === AppBskyFeedDefs.CONTENTMODEVIDEO
const _isVideoFeed = isBskyVideoFeed || feedIsVideoMode
return isNative && _isVideoFeed
}, [feedInfo])
React.useEffect(() => { React.useEffect(() => {
if (isPageFocused) { if (isPageFocused) {
@@ -134,6 +145,7 @@ export function FeedPage({
renderEndOfFeed={renderEndOfFeed} renderEndOfFeed={renderEndOfFeed}
headerOffset={headerOffset} headerOffset={headerOffset}
savedFeedConfig={savedFeedConfig} savedFeedConfig={savedFeedConfig}
isVideoFeed={isVideoFeed}
/> />
</FeedFeedbackProvider> </FeedFeedbackProvider>
</MainScrollProvider> </MainScrollProvider>
+1 -1
View File
@@ -17,7 +17,6 @@ import {usePalette} from '#/lib/hooks/usePalette'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {s} from '#/lib/styles' import {s} from '#/lib/styles'
import {logger} from '#/logger' import {logger} from '#/logger'
import {shouldClickOpenNewTab} from '#/platform/urls'
import {FeedSourceInfo, useFeedSourceInfoQuery} from '#/state/queries/feed' import {FeedSourceInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
import { import {
useAddSavedFeedsMutation, useAddSavedFeedsMutation,
@@ -29,6 +28,7 @@ import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import * as Toast from '#/view/com/util/Toast' import * as Toast from '#/view/com/util/Toast'
import {useTheme} from '#/alf' import {useTheme} from '#/alf'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
import {shouldClickOpenNewTab} from '#/components/Link'
import * as Prompt from '#/components/Prompt' import * as Prompt from '#/components/Prompt'
import {RichText} from '#/components/RichText' import {RichText} from '#/components/RichText'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
-2
View File
@@ -205,9 +205,7 @@ export const ProfileFeedgens = React.forwardRef<
headerOffset={headerOffset} headerOffset={headerOffset}
progressViewOffset={ios(0)} progressViewOffset={ios(0)}
contentContainerStyle={isMobile && {paddingBottom: headerOffset + 100}} contentContainerStyle={isMobile && {paddingBottom: headerOffset + 100}}
indicatorStyle={t.name === 'light' ? 'black' : 'white'}
removeClippedSubviews={true} removeClippedSubviews={true}
// @ts-ignore our .web version only -prf
desktopFixedHeight desktopFixedHeight
onEndReached={onEndReached} onEndReached={onEndReached}
/> />
-2
View File
@@ -203,9 +203,7 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
contentContainerStyle={ contentContainerStyle={
isMobile && {paddingBottom: headerOffset + 100} isMobile && {paddingBottom: headerOffset + 100}
} }
indicatorStyle={t.name === 'light' ? 'black' : 'white'}
removeClippedSubviews={true} removeClippedSubviews={true}
// @ts-ignore our .web version only -prf
desktopFixedHeight desktopFixedHeight
onEndReached={onEndReached} onEndReached={onEndReached}
/> />
+2 -40
View File
@@ -1,4 +1,4 @@
import React, {Children, forwardRef, useCallback, useContext} from 'react' import React, {forwardRef, useCallback, useContext} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {DrawerGestureContext} from 'react-native-drawer-layout' import {DrawerGestureContext} from 'react-native-drawer-layout'
import {Gesture, GestureDetector} from 'react-native-gesture-handler' import {Gesture, GestureDetector} from 'react-native-gesture-handler'
@@ -17,7 +17,6 @@ import Animated, {
} from 'react-native-reanimated' } from 'react-native-reanimated'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
import {isAndroid} from '#/platform/detection'
import {useSetDrawerSwipeDisabled} from '#/state/shell' import {useSetDrawerSwipeDisabled} from '#/state/shell'
import {atoms as a, native} from '#/alf' import {atoms as a, native} from '#/alf'
@@ -149,11 +148,7 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
style={[a.flex_1]} style={[a.flex_1]}
initialPage={initialPage} initialPage={initialPage}
onPageScroll={handlePageScroll}> onPageScroll={handlePageScroll}>
{isAndroid {children}
? Children.map(children, child => (
<CaptureSwipesAndroid>{child}</CaptureSwipesAndroid>
))
: children}
</AnimatedPagerView> </AnimatedPagerView>
</GestureDetector> </GestureDetector>
</View> </View>
@@ -161,39 +156,6 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
}, },
) )
// HACK.
// This works around https://github.com/callstack/react-native-pager-view/issues/960.
// It appears that the Pressables inside the pager get confused if there's enough work
// happening on the JS thread, and mistakingly interpret a pager swipe as a tap.
// We can prevent this by stealing all horizontal movements from the tree inside.
function CaptureSwipesAndroid({children}: {children: React.ReactNode}) {
const lastTouchStart = React.useRef<{x: number; y: number} | null>(null)
return (
<View
onTouchStart={e => {
lastTouchStart.current = {
x: e.nativeEvent.pageX,
y: e.nativeEvent.pageY,
}
}}
onMoveShouldSetResponderCapture={e => {
const coords = lastTouchStart.current
if (!coords) {
return false
}
const dx = Math.abs(e.nativeEvent.pageX - coords.x)
if (dx > 0) {
// This is a horizontal movement and will result in a swipe.
// Prevent pager children from receiving this touch.
return true
}
return false
}}>
{children}
</View>
)
}
function usePagerHandlers( function usePagerHandlers(
handlers: { handlers: {
onPageScroll: (e: PagerViewOnPageScrollEventData) => void onPageScroll: (e: PagerViewOnPageScrollEventData) => void

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