Merge branch 'main' into hailey/tweak-env-var
This commit is contained in:
@@ -93,6 +93,7 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/ban-ts-comment': 'warn',
|
||||
},
|
||||
ignorePatterns: [
|
||||
'**/__mocks__/*.ts',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright 2023–2024 Bluesky PBC
|
||||
Copyright 2023–2025 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:
|
||||
|
||||
|
||||
@@ -10,8 +10,7 @@ appId: xyz.blueskyweb.app
|
||||
id: "e2eOpenLoggedOutView"
|
||||
- tapOn:
|
||||
id: "createAccountButton"
|
||||
- tapOn:
|
||||
id: "selectServiceButton"
|
||||
- tapOn: "Bluesky Social"
|
||||
- tapOn:
|
||||
id: "customSelectBtn"
|
||||
- tapOn:
|
||||
|
||||
@@ -21,7 +21,7 @@ appId: xyz.blueskyweb.app
|
||||
label: "Feeds button disappears after pinning a feed"
|
||||
id: "bottomBarProfileBtn"
|
||||
- tapOn:
|
||||
id: "profilePager-selector-4"
|
||||
id: "profilePager-selector-5"
|
||||
- tapOn: "alice-favs"
|
||||
- tapOn: "Pin to Home"
|
||||
- tapOn:
|
||||
|
||||
+21
-3
@@ -75,9 +75,11 @@ module.exports = function (config) {
|
||||
'an',
|
||||
'ast',
|
||||
'ca',
|
||||
'da',
|
||||
'de',
|
||||
'el',
|
||||
'es',
|
||||
'eu',
|
||||
'fi',
|
||||
'fr',
|
||||
'ga',
|
||||
@@ -95,11 +97,12 @@ module.exports = function (config) {
|
||||
'pt-BR',
|
||||
'ro',
|
||||
'ru',
|
||||
'sv',
|
||||
'th',
|
||||
'tr',
|
||||
'uk',
|
||||
'vi',
|
||||
'yue-Hant',
|
||||
'yue',
|
||||
'zh-Hans',
|
||||
'zh-Hant',
|
||||
],
|
||||
@@ -191,6 +194,7 @@ module.exports = function (config) {
|
||||
channel: UPDATES_CHANNEL,
|
||||
},
|
||||
plugins: [
|
||||
'expo-video',
|
||||
'expo-localization',
|
||||
USE_SENTRY && [
|
||||
'@sentry/react-native/expo',
|
||||
@@ -225,8 +229,12 @@ module.exports = function (config) {
|
||||
},
|
||||
],
|
||||
'react-native-compressor',
|
||||
// TODO: Reenable when the build issue is fixed.
|
||||
// '@bitdrift/react-native',
|
||||
[
|
||||
'@bitdrift/react-native',
|
||||
{
|
||||
networkInstrumentation: true,
|
||||
},
|
||||
],
|
||||
'./plugins/starterPackAppClipExtension/withStarterPackAppClip.js',
|
||||
'./plugins/withAndroidManifestPlugin.js',
|
||||
'./plugins/withAndroidManifestFCMIconPlugin.js',
|
||||
@@ -347,6 +355,16 @@ module.exports = function (config) {
|
||||
},
|
||||
],
|
||||
['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),
|
||||
extra: {
|
||||
eas: {
|
||||
|
||||
@@ -214,15 +214,21 @@ func serve(cctx *cli.Context) error {
|
||||
e.GET("/iframe/youtube.html", echo.WrapHandler(staticHandler))
|
||||
e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler)), func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
path := c.Request().URL.Path
|
||||
maxAge := 1 * (60 * 60) // default is 1 hour
|
||||
c.Response().Before(func() {
|
||||
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
|
||||
if strings.HasPrefix(path, "/static/js/") || strings.HasPrefix(path, "/static/css/") || strings.HasPrefix(path, "/static/media/") {
|
||||
maxAge = 365 * (60 * 60 * 24) // 1 year
|
||||
}
|
||||
path := c.Request().URL.Path
|
||||
maxAge := 1 * (60 * 60) // default is 1 hour
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
Preconnect to essential domains
|
||||
-->
|
||||
<link rel="preconnect" href="https://bsky.social">
|
||||
<link rel="preconnect" href="https://bsky.network">
|
||||
<title>{%- block head_title -%}Bluesky{%- endblock -%}</title>
|
||||
|
||||
<!-- Hello Humans! API docs at https://atproto.com -->
|
||||
|
||||
@@ -5,10 +5,12 @@ module.exports = {
|
||||
'an',
|
||||
'ast',
|
||||
'ca',
|
||||
'da',
|
||||
'de',
|
||||
'el',
|
||||
'en-GB',
|
||||
'es',
|
||||
'eu',
|
||||
'fi',
|
||||
'fr',
|
||||
'ga',
|
||||
@@ -26,6 +28,7 @@ module.exports = {
|
||||
'pt-BR',
|
||||
'ro',
|
||||
'ru',
|
||||
'sv',
|
||||
'th',
|
||||
'tr',
|
||||
'uk',
|
||||
|
||||
+5
-2
@@ -54,7 +54,8 @@
|
||||
"icons:optimize": "svgo -f ./assets/icons"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.13.21",
|
||||
"@atproto/api": "^0.13.31",
|
||||
"@bitdrift/react-native": "^0.6.2",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
@@ -145,6 +146,7 @@
|
||||
"expo-system-ui": "^4.0.4",
|
||||
"expo-task-manager": "~12.0.3",
|
||||
"expo-updates": "~0.26.10",
|
||||
"expo-video": "^2.0.5",
|
||||
"expo-web-browser": "~14.0.1",
|
||||
"fast-text-encoding": "^1.0.6",
|
||||
"history": "^5.3.0",
|
||||
@@ -178,7 +180,7 @@
|
||||
"react-native-ios-context-menu": "^1.15.3",
|
||||
"react-native-keyboard-controller": "^1.14.5",
|
||||
"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-progress": "bluesky-social/react-native-progress",
|
||||
"react-native-qrcode-styled": "^0.3.3",
|
||||
@@ -191,6 +193,7 @@
|
||||
"react-native-url-polyfill": "^1.3.0",
|
||||
"react-native-uuid": "^2.0.2",
|
||||
"react-native-view-shot": "^4.0.3",
|
||||
"react-native-vision-camera": "^4.6.3",
|
||||
"react-native-web": "~0.19.13",
|
||||
"react-native-web-webview": "^1.0.2",
|
||||
"react-native-webview": "13.12.3",
|
||||
|
||||
@@ -86,6 +86,7 @@ import {
|
||||
StarterPackScreenShort,
|
||||
} from '#/screens/StarterPack/StarterPackScreen'
|
||||
import {Wizard} from '#/screens/StarterPack/Wizard'
|
||||
import {VideoFeed} from '#/screens/VideoFeed'
|
||||
import {useTheme} from '#/alf'
|
||||
import {router} from '#/routes'
|
||||
import {Referrer} from '../modules/expo-bluesky-swiss-army'
|
||||
@@ -422,6 +423,14 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
||||
getComponent={() => Wizard}
|
||||
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
@@ -497,7 +497,7 @@ export function createThemes({
|
||||
color: dimPalette.contrast_400,
|
||||
},
|
||||
text_contrast_medium: {
|
||||
color: dimPalette.contrast_700,
|
||||
color: dimPalette.contrast_600,
|
||||
},
|
||||
text_contrast_high: {
|
||||
color: dimPalette.contrast_900,
|
||||
|
||||
@@ -27,6 +27,7 @@ import {useA11y} from '#/state/a11y'
|
||||
import {useDialogStateControlContext} from '#/state/dialogs'
|
||||
import {List, ListMethods, ListProps} from '#/view/com/util/List'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {useThemeName} from '#/alf/util/useColorModeTheme'
|
||||
import {Context, useDialogContext} from '#/components/Dialog/context'
|
||||
import {
|
||||
DialogControlProps,
|
||||
@@ -55,7 +56,8 @@ export function Outer({
|
||||
nativeOptions,
|
||||
testID,
|
||||
}: React.PropsWithChildren<DialogOuterProps>) {
|
||||
const t = useTheme()
|
||||
const themeName = useThemeName()
|
||||
const t = useTheme(themeName)
|
||||
const ref = React.useRef<BottomSheetNativeComponent>(null)
|
||||
const closeCallbacks = React.useRef<(() => void)[]>([])
|
||||
const {setDialogIsOpen, setFullyExpandedCount} =
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {ScrollView} from 'react-native-gesture-handler'
|
||||
import {ScrollView, View} from 'react-native'
|
||||
import {AppBskyActorDefs, AppBskyFeedDefs, AtUri} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
@@ -17,6 +16,7 @@ import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows
|
||||
import {useSession} from '#/state/session'
|
||||
import * as userActionHistory 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 {Button} from '#/components/Button'
|
||||
import * as FeedCard from '#/components/FeedCard'
|
||||
@@ -358,33 +358,37 @@ export function ProfileGrid({
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap}
|
||||
decelerationRate="fast">
|
||||
<View style={[a.px_lg, a.pt_sm, a.pb_lg, a.flex_row, a.gap_md]}>
|
||||
{content}
|
||||
<BlockDrawerGesture>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap}
|
||||
decelerationRate="fast">
|
||||
<View style={[a.px_lg, a.pt_sm, a.pb_lg, a.flex_row, a.gap_md]}>
|
||||
{content}
|
||||
|
||||
<Button
|
||||
label={_(msg`Browse more accounts on the Explore page`)}
|
||||
onPress={() => {
|
||||
navigation.navigate('SearchTab')
|
||||
}}>
|
||||
<CardOuter style={[a.flex_1, {borderWidth: 0}]}>
|
||||
<View style={[a.flex_1, a.justify_center]}>
|
||||
<View style={[a.flex_row, a.px_lg]}>
|
||||
<Text style={[a.pr_xl, a.flex_1, a.leading_snug]}>
|
||||
<Trans>Browse more suggestions on the Explore page</Trans>
|
||||
</Text>
|
||||
<Button
|
||||
label={_(msg`Browse more accounts on the Explore page`)}
|
||||
onPress={() => {
|
||||
navigation.navigate('SearchTab')
|
||||
}}>
|
||||
<CardOuter style={[a.flex_1, {borderWidth: 0}]}>
|
||||
<View style={[a.flex_1, a.justify_center]}>
|
||||
<View style={[a.flex_row, a.px_lg]}>
|
||||
<Text style={[a.pr_xl, a.flex_1, a.leading_snug]}>
|
||||
<Trans>
|
||||
Browse more suggestions on the Explore page
|
||||
</Trans>
|
||||
</Text>
|
||||
|
||||
<Arrow size="xl" />
|
||||
<Arrow size="xl" />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</CardOuter>
|
||||
</Button>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</CardOuter>
|
||||
</Button>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</BlockDrawerGesture>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
@@ -491,34 +495,38 @@ export function SuggestedFeeds() {
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap}
|
||||
decelerationRate="fast">
|
||||
<View style={[a.px_lg, a.pt_md, a.pb_xl, a.flex_row, a.gap_md]}>
|
||||
{content}
|
||||
<BlockDrawerGesture>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap}
|
||||
decelerationRate="fast">
|
||||
<View style={[a.px_lg, a.pt_md, a.pb_xl, a.flex_row, a.gap_md]}>
|
||||
{content}
|
||||
|
||||
<Button
|
||||
label={_(msg`Browse more feeds on the Explore page`)}
|
||||
onPress={() => {
|
||||
navigation.navigate('SearchTab')
|
||||
}}
|
||||
style={[a.flex_col]}>
|
||||
<CardOuter style={[a.flex_1]}>
|
||||
<View style={[a.flex_1, a.justify_center]}>
|
||||
<View style={[a.flex_row, a.px_lg]}>
|
||||
<Text style={[a.pr_xl, a.flex_1, a.leading_snug]}>
|
||||
<Trans>Browse more suggestions on the Explore page</Trans>
|
||||
</Text>
|
||||
<Button
|
||||
label={_(msg`Browse more feeds on the Explore page`)}
|
||||
onPress={() => {
|
||||
navigation.navigate('SearchTab')
|
||||
}}
|
||||
style={[a.flex_col]}>
|
||||
<CardOuter style={[a.flex_1]}>
|
||||
<View style={[a.flex_1, a.justify_center]}>
|
||||
<View style={[a.flex_row, a.px_lg]}>
|
||||
<Text style={[a.pr_xl, a.flex_1, a.leading_snug]}>
|
||||
<Trans>
|
||||
Browse more suggestions on the Explore page
|
||||
</Trans>
|
||||
</Text>
|
||||
|
||||
<Arrow size="xl" />
|
||||
<Arrow size="xl" />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</CardOuter>
|
||||
</Button>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</CardOuter>
|
||||
</Button>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</BlockDrawerGesture>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -122,7 +122,11 @@ export function BackButton({onPress, style, ...props}: Partial<ButtonProps>) {
|
||||
shape="square"
|
||||
onPress={onPressBack}
|
||||
hitSlop={HITSLOP_30}
|
||||
style={[{marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET}, style]}
|
||||
style={[
|
||||
{marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET},
|
||||
a.bg_transparent,
|
||||
style,
|
||||
]}
|
||||
{...props}>
|
||||
<ButtonIcon icon={ArrowLeft} size="lg" />
|
||||
</Button>
|
||||
|
||||
@@ -58,6 +58,7 @@ export const Content = React.memo(function Content({
|
||||
contentContainerStyle,
|
||||
...props
|
||||
}: ContentProps) {
|
||||
const t = useTheme()
|
||||
const {footerHeight} = useShellLayout()
|
||||
const animatedProps = useAnimatedProps(() => {
|
||||
return {
|
||||
@@ -73,6 +74,7 @@ export const Content = React.memo(function Content({
|
||||
<Animated.ScrollView
|
||||
id="content"
|
||||
automaticallyAdjustsScrollIndicatorInsets={false}
|
||||
indicatorStyle={t.scheme === 'dark' ? 'white' : 'black'}
|
||||
// sets the scroll inset to the height of the footer
|
||||
animatedProps={animatedProps}
|
||||
style={[scrollViewStyles.common, style]}
|
||||
|
||||
@@ -6,12 +6,18 @@ import {gradients} from '#/alf/tokens'
|
||||
|
||||
export function LinearGradientBackground({
|
||||
style,
|
||||
gradient = 'sky',
|
||||
children,
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
style: StyleProp<ViewStyle>
|
||||
children: React.ReactNode
|
||||
style?: StyleProp<ViewStyle>
|
||||
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
|
||||
}) as [string, string, ...string[]]
|
||||
|
||||
@@ -20,7 +26,7 @@ export function LinearGradientBackground({
|
||||
}
|
||||
|
||||
return (
|
||||
<LinearGradient colors={gradient} style={style}>
|
||||
<LinearGradient colors={colors} style={style} start={start} end={end}>
|
||||
{children}
|
||||
</LinearGradient>
|
||||
)
|
||||
|
||||
+120
-24
@@ -15,7 +15,6 @@ import {
|
||||
linkRequiresWarning,
|
||||
} from '#/lib/strings/url-helpers'
|
||||
import {isNative, isWeb} from '#/platform/detection'
|
||||
import {shouldClickOpenNewTab} from '#/platform/urls'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {atoms as a, flatten, TextStyleProp, useTheme, web} from '#/alf'
|
||||
import {Button, ButtonProps} from '#/components/Button'
|
||||
@@ -55,6 +54,12 @@ type BaseLinkProps = Pick<
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
@@ -72,9 +77,12 @@ export function useLink({
|
||||
action = 'push',
|
||||
disableMismatchWarning,
|
||||
onPress: outerOnPress,
|
||||
onLongPress: outerOnLongPress,
|
||||
shareOnLongPress,
|
||||
overridePresentation,
|
||||
}: BaseLinkProps & {
|
||||
displayText: string
|
||||
overridePresentation?: boolean
|
||||
}) {
|
||||
const navigation = useNavigationDeduped()
|
||||
const {href} = useLinkProps<AllNavigatorParams>({
|
||||
@@ -110,7 +118,7 @@ export function useLink({
|
||||
})
|
||||
} else {
|
||||
if (isExternal) {
|
||||
openLink(href)
|
||||
openLink(href, overridePresentation)
|
||||
} else {
|
||||
const shouldOpenInNewTab = shouldClickOpenNewTab(e)
|
||||
|
||||
@@ -152,6 +160,7 @@ export function useLink({
|
||||
closeModal,
|
||||
action,
|
||||
navigation,
|
||||
overridePresentation,
|
||||
],
|
||||
)
|
||||
|
||||
@@ -175,8 +184,14 @@ export function useLink({
|
||||
}
|
||||
}, [disableMismatchWarning, displayText, href, isExternal, openModal])
|
||||
|
||||
const onLongPress =
|
||||
isNative && isExternal && shareOnLongPress ? handleLongPress : undefined
|
||||
const onLongPress = React.useCallback(
|
||||
(e: GestureResponderEvent) => {
|
||||
const exitEarlyIfFalse = outerOnLongPress?.(e)
|
||||
if (exitEarlyIfFalse === false) return
|
||||
return isNative && shareOnLongPress ? handleLongPress() : undefined
|
||||
},
|
||||
[outerOnLongPress, handleLongPress, shareOnLongPress],
|
||||
)
|
||||
|
||||
return {
|
||||
isExternal,
|
||||
@@ -202,14 +217,16 @@ export function Link({
|
||||
to,
|
||||
action = 'push',
|
||||
onPress: outerOnPress,
|
||||
onLongPress: outerOnLongPress,
|
||||
download,
|
||||
...rest
|
||||
}: LinkProps) {
|
||||
const {href, isExternal, onPress} = useLink({
|
||||
const {href, isExternal, onPress, onLongPress} = useLink({
|
||||
to,
|
||||
displayText: typeof children === 'string' ? children : '',
|
||||
action,
|
||||
onPress: outerOnPress,
|
||||
onLongPress: outerOnLongPress,
|
||||
})
|
||||
|
||||
return (
|
||||
@@ -220,6 +237,7 @@ export function Link({
|
||||
accessibilityRole="link"
|
||||
href={href}
|
||||
onPress={download ? undefined : onPress}
|
||||
onLongPress={onLongPress}
|
||||
{...web({
|
||||
hrefAttrs: {
|
||||
target: download ? undefined : isExternal ? 'blank' : undefined,
|
||||
@@ -239,12 +257,13 @@ export function Link({
|
||||
export type InlineLinkProps = React.PropsWithChildren<
|
||||
BaseLinkProps &
|
||||
TextStyleProp &
|
||||
Pick<TextProps, 'selectable' | 'numberOfLines'>
|
||||
> &
|
||||
Pick<ButtonProps, 'label'> & {
|
||||
disableUnderline?: boolean
|
||||
title?: TextProps['title']
|
||||
}
|
||||
Pick<TextProps, 'selectable' | 'numberOfLines'> &
|
||||
Pick<ButtonProps, 'label' | 'accessibilityHint'> & {
|
||||
disableUnderline?: boolean
|
||||
title?: TextProps['title']
|
||||
overridePresentation?: boolean
|
||||
}
|
||||
>
|
||||
|
||||
export function InlineLinkText({
|
||||
children,
|
||||
@@ -253,11 +272,13 @@ export function InlineLinkText({
|
||||
disableMismatchWarning,
|
||||
style,
|
||||
onPress: outerOnPress,
|
||||
onLongPress: outerOnLongPress,
|
||||
download,
|
||||
selectable,
|
||||
label,
|
||||
shareOnLongPress,
|
||||
disableUnderline,
|
||||
overridePresentation,
|
||||
...rest
|
||||
}: InlineLinkProps) {
|
||||
const t = useTheme()
|
||||
@@ -268,7 +289,9 @@ export function InlineLinkText({
|
||||
action,
|
||||
disableMismatchWarning,
|
||||
onPress: outerOnPress,
|
||||
onLongPress: outerOnLongPress,
|
||||
shareOnLongPress,
|
||||
overridePresentation,
|
||||
})
|
||||
const {
|
||||
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
|
||||
*
|
||||
@@ -327,7 +365,10 @@ export function InlineLinkText({
|
||||
*/
|
||||
export function createStaticClick(
|
||||
onPressHandler: Exclude<BaseLinkProps['onPress'], undefined>,
|
||||
): Pick<BaseLinkProps, 'to' | 'onPress'> {
|
||||
): {
|
||||
to: BaseLinkProps['to']
|
||||
onPress: Exclude<BaseLinkProps['onPress'], undefined>
|
||||
} {
|
||||
return {
|
||||
to: '#',
|
||||
onPress(e: GestureResponderEvent) {
|
||||
@@ -338,17 +379,72 @@ export function createStaticClick(
|
||||
}
|
||||
}
|
||||
|
||||
export function WebOnlyInlineLinkText({
|
||||
children,
|
||||
to,
|
||||
onPress,
|
||||
...props
|
||||
}: InlineLinkProps) {
|
||||
return isWeb ? (
|
||||
<InlineLinkText {...props} to={to} onPress={onPress}>
|
||||
{children}
|
||||
</InlineLinkText>
|
||||
) : (
|
||||
<Text {...props}>{children}</Text>
|
||||
/**
|
||||
* Utility to create a static `onPress` handler for a `Link`, but only if the
|
||||
* click was not modified in some way e.g. `Cmd` or a middle click.
|
||||
*
|
||||
* On native, this behaves the same as `createStaticClick` because there are no
|
||||
* options to "modify" the click in this sense.
|
||||
*
|
||||
* Example:
|
||||
* `<Link {...createStaticClick(e => {...})} />`
|
||||
*/
|
||||
export function createStaticClickIfUnmodified(
|
||||
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
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ export function ListFooter({
|
||||
style,
|
||||
showEndMessage = false,
|
||||
endMessageText,
|
||||
renderEndMessage,
|
||||
}: {
|
||||
isFetchingNextPage?: boolean
|
||||
hasNextPage?: boolean
|
||||
@@ -29,6 +30,7 @@ export function ListFooter({
|
||||
style?: StyleProp<ViewStyle>
|
||||
showEndMessage?: boolean
|
||||
endMessageText?: string
|
||||
renderEndMessage?: () => React.ReactNode
|
||||
}) {
|
||||
const t = useTheme()
|
||||
|
||||
@@ -48,9 +50,13 @@ export function ListFooter({
|
||||
) : error ? (
|
||||
<ListFooterMaybeError error={error} onRetry={onRetry} />
|
||||
) : !hasNextPage && showEndMessage ? (
|
||||
<Text style={[a.text_sm, t.atoms.text_contrast_low]}>
|
||||
{endMessageText ?? <Trans>You have reached the end</Trans>}
|
||||
</Text>
|
||||
renderEndMessage ? (
|
||||
renderEndMessage()
|
||||
) : (
|
||||
<Text style={[a.text_sm, t.atoms.text_contrast_low]}>
|
||||
{endMessageText ?? <Trans>You have reached the end</Trans>}
|
||||
</Text>
|
||||
)
|
||||
) : null}
|
||||
</View>
|
||||
)
|
||||
|
||||
@@ -47,7 +47,12 @@ export function Root({
|
||||
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 {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
|
||||
const {
|
||||
@@ -65,11 +70,13 @@ export function Trigger({children, label, role = 'button'}: TriggerProps) {
|
||||
pressed,
|
||||
},
|
||||
props: {
|
||||
ref: null,
|
||||
onPress: context.control.open,
|
||||
onFocus,
|
||||
onBlur,
|
||||
onPressIn,
|
||||
onPressOut,
|
||||
accessibilityHint: hint,
|
||||
accessibilityLabel: label,
|
||||
accessibilityRole: role,
|
||||
},
|
||||
|
||||
@@ -110,7 +110,12 @@ const RadixTriggerPassThrough = React.forwardRef(
|
||||
)
|
||||
RadixTriggerPassThrough.displayName = 'RadixTriggerPassThrough'
|
||||
|
||||
export function Trigger({children, label, role = 'button'}: TriggerProps) {
|
||||
export function Trigger({
|
||||
children,
|
||||
label,
|
||||
role = 'button',
|
||||
hint,
|
||||
}: TriggerProps) {
|
||||
const {control} = useMenuContext()
|
||||
const {
|
||||
state: hovered,
|
||||
@@ -153,6 +158,7 @@ export function Trigger({children, label, role = 'button'}: TriggerProps) {
|
||||
onBlur: onBlur,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
accessibilityHint: hint,
|
||||
accessibilityLabel: label,
|
||||
accessibilityRole: role,
|
||||
},
|
||||
|
||||
@@ -19,6 +19,7 @@ export type ItemContextType = {
|
||||
}
|
||||
|
||||
export type RadixPassThroughTriggerProps = {
|
||||
ref: React.RefObject<any>
|
||||
id: string
|
||||
type: 'button'
|
||||
disabled: boolean
|
||||
@@ -37,6 +38,7 @@ export type RadixPassThroughTriggerProps = {
|
||||
export type TriggerProps = {
|
||||
children(props: TriggerChildProps): React.ReactNode
|
||||
label: string
|
||||
hint?: string
|
||||
role?: AccessibilityRole
|
||||
}
|
||||
export type TriggerChildProps =
|
||||
@@ -59,11 +61,13 @@ export type TriggerChildProps =
|
||||
* object is empty.
|
||||
*/
|
||||
props: {
|
||||
ref: null
|
||||
onPress: () => void
|
||||
onFocus: () => void
|
||||
onBlur: () => void
|
||||
onPressIn: () => void
|
||||
onPressOut: () => void
|
||||
accessibilityHint?: string
|
||||
accessibilityLabel: string
|
||||
accessibilityRole: AccessibilityRole
|
||||
}
|
||||
@@ -85,6 +89,7 @@ export type TriggerChildProps =
|
||||
onBlur: () => void
|
||||
onMouseEnter: () => void
|
||||
onMouseLeave: () => void
|
||||
accessibilityHint?: string
|
||||
accessibilityLabel: string
|
||||
accessibilityRole: AccessibilityRole
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import * as Dialog from '#/components/Dialog'
|
||||
import * as Toggle from '#/components/forms/Toggle'
|
||||
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||
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 {Text} from '#/components/Typography'
|
||||
import {ReportDialogProps} from './types'
|
||||
@@ -223,7 +224,7 @@ export function SubmitView({
|
||||
<ButtonText>
|
||||
<Trans>Send report</Trans>
|
||||
</ButtonText>
|
||||
{submitting && <ButtonIcon icon={Loader} />}
|
||||
<ButtonIcon icon={submitting ? Loader : SendIcon} />
|
||||
</Button>
|
||||
</View>
|
||||
{/* Maybe fix this later -h */}
|
||||
|
||||
+13
-91
@@ -1,25 +1,19 @@
|
||||
import React from 'react'
|
||||
import {TextStyle} from 'react-native'
|
||||
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 {isNative} from '#/platform/detection'
|
||||
import {atoms as a, flatten, native, TextStyleProp, useTheme, web} from '#/alf'
|
||||
import {atoms as a, flatten, TextStyleProp} from '#/alf'
|
||||
import {isOnlyEmoji} from '#/alf/typography'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {InlineLinkText, LinkProps} from '#/components/Link'
|
||||
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
|
||||
import {TagMenu, useTagMenuControl} from '#/components/TagMenu'
|
||||
import {RichTextTag} from '#/components/RichTextTag'
|
||||
import {Text, TextProps} from '#/components/Typography'
|
||||
|
||||
const WORD_WRAP = {wordWrap: 1}
|
||||
|
||||
export type RichTextProps = TextStyleProp &
|
||||
Pick<TextProps, 'selectable'> & {
|
||||
Pick<TextProps, 'selectable' | 'onLayout' | 'onTextLayout'> & {
|
||||
value: RichTextAPI | string
|
||||
testID?: string
|
||||
numberOfLines?: number
|
||||
@@ -43,6 +37,8 @@ export function RichText({
|
||||
onLinkPress,
|
||||
interactiveStyle,
|
||||
emojiMultiplier = 1.85,
|
||||
onLayout,
|
||||
onTextLayout,
|
||||
}: RichTextProps) {
|
||||
const richText = React.useMemo(
|
||||
() =>
|
||||
@@ -70,6 +66,8 @@ export function RichText({
|
||||
selectable={selectable}
|
||||
testID={testID}
|
||||
style={[plainStyles, {fontSize}]}
|
||||
onLayout={onLayout}
|
||||
onTextLayout={onTextLayout}
|
||||
// @ts-ignore web only -prf
|
||||
dataSet={WORD_WRAP}>
|
||||
{text}
|
||||
@@ -83,6 +81,8 @@ export function RichText({
|
||||
testID={testID}
|
||||
style={plainStyles}
|
||||
numberOfLines={numberOfLines}
|
||||
onLayout={onLayout}
|
||||
onTextLayout={onTextLayout}
|
||||
// @ts-ignore web only -prf
|
||||
dataSet={WORD_WRAP}>
|
||||
{text}
|
||||
@@ -143,10 +143,9 @@ export function RichText({
|
||||
els.push(
|
||||
<RichTextTag
|
||||
key={key}
|
||||
text={segment.text}
|
||||
display={segment.text}
|
||||
tag={tag.tag}
|
||||
style={interactiveStyles}
|
||||
selectable={selectable}
|
||||
textStyle={interactiveStyles}
|
||||
authorHandle={authorHandle}
|
||||
/>,
|
||||
)
|
||||
@@ -163,88 +162,11 @@ export function RichText({
|
||||
testID={testID}
|
||||
style={plainStyles}
|
||||
numberOfLines={numberOfLines}
|
||||
onLayout={onLayout}
|
||||
onTextLayout={onTextLayout}
|
||||
// @ts-ignore web only -prf
|
||||
dataSet={WORD_WRAP}>
|
||||
{els}
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
progressViewOffset={ios(0)}
|
||||
contentContainerStyle={{paddingBottom: headerOffset + bottomBarOffset}}
|
||||
indicatorStyle={t.name === 'light' ? 'black' : 'white'}
|
||||
removeClippedSubviews={true}
|
||||
desktopFixedHeight
|
||||
onEndReached={onEndReached}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -227,6 +227,7 @@ let ConvoMenu = ({
|
||||
/>
|
||||
{latestReportableMessage ? (
|
||||
<ReportDialog
|
||||
currentScreen={currentScreen}
|
||||
params={{
|
||||
type: 'convoMessage',
|
||||
convoId: convo.id,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {msg} from '@lingui/macro'
|
||||
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 {isNative} from '#/platform/detection'
|
||||
@@ -22,15 +22,10 @@ export function LeaveConvoPrompt({
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
|
||||
const {mutate: leaveConvo} = useLeaveConvo(convoId, {
|
||||
onSuccess: () => {
|
||||
onMutate: () => {
|
||||
if (currentScreen === 'conversation') {
|
||||
navigation.replace(
|
||||
'Messages',
|
||||
isNative
|
||||
? {
|
||||
animation: 'pop',
|
||||
}
|
||||
: {},
|
||||
navigation.dispatch(
|
||||
StackActions.replace('Messages', isNative ? {animation: 'pop'} : {}),
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -138,6 +138,7 @@ export let MessageMenu = ({
|
||||
</Menu.Root>
|
||||
|
||||
<ReportDialog
|
||||
currentScreen="conversation"
|
||||
params={{type: 'convoMessage', convoId: convo.convo.id, message}}
|
||||
control={reportControl}
|
||||
/>
|
||||
|
||||
@@ -1,27 +1,39 @@
|
||||
import React, {memo, useMemo, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {
|
||||
AppBskyActorDefs,
|
||||
ChatBskyConvoDefs,
|
||||
ComAtprotoModerationCreateReport,
|
||||
RichText as RichTextAPI,
|
||||
} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {StackActions, useNavigation} from '@react-navigation/native'
|
||||
import {useMutation} from '@tanstack/react-query'
|
||||
|
||||
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 {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
|
||||
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 {Button, ButtonIcon, ButtonText} from '../Button'
|
||||
import {Divider} from '../Divider'
|
||||
import {ChevronLeft_Stroke2_Corner0_Rounded as Chevron} from '../icons/Chevron'
|
||||
import {Loader} from '../Loader'
|
||||
import {SelectReportOptionView} from '../ReportDialog/SelectReportOptionView'
|
||||
import {RichText} from '../RichText'
|
||||
import {Text} from '../Typography'
|
||||
import {Divider} from '#/components/Divider'
|
||||
import * as Toggle from '#/components/forms/Toggle'
|
||||
import {ChevronLeft_Stroke2_Corner0_Rounded as Chevron} from '#/components/icons/Chevron'
|
||||
import {PaperPlane_Stroke2_Corner0_Rounded as SendIcon} from '#/components/icons/PaperPlane'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {SelectReportOptionView} from '#/components/ReportDialog/SelectReportOptionView'
|
||||
import {RichText} from '#/components/RichText'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {MessageItemMetadata} from './MessageItem'
|
||||
|
||||
type ReportDialogParams = {
|
||||
@@ -33,16 +45,18 @@ type ReportDialogParams = {
|
||||
let ReportDialog = ({
|
||||
control,
|
||||
params,
|
||||
currentScreen,
|
||||
}: {
|
||||
control: Dialog.DialogControlProps
|
||||
params: ReportDialogParams
|
||||
currentScreen: 'list' | 'conversation'
|
||||
}): React.ReactNode => {
|
||||
const {_} = useLingui()
|
||||
return (
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Handle />
|
||||
<Dialog.ScrollableInner label={_(msg`Report this message`)}>
|
||||
<DialogInner params={params} />
|
||||
<DialogInner params={params} currentScreen={currentScreen} />
|
||||
<Dialog.Close />
|
||||
</Dialog.ScrollableInner>
|
||||
</Dialog.Outer>
|
||||
@@ -51,14 +65,44 @@ let ReportDialog = ({
|
||||
ReportDialog = memo(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 [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
|
||||
params={params}
|
||||
reportOption={reportOption}
|
||||
goBack={() => setReportOption(null)}
|
||||
onComplete={() => {
|
||||
if (isError) {
|
||||
control.close()
|
||||
} else {
|
||||
setDone(true)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ReasonStep params={params} setReportOption={setReportOption} />
|
||||
@@ -89,16 +133,17 @@ function SubmitStep({
|
||||
params,
|
||||
reportOption,
|
||||
goBack,
|
||||
onComplete,
|
||||
}: {
|
||||
params: ReportDialogParams
|
||||
reportOption: ReportOption
|
||||
goBack: () => void
|
||||
onComplete: () => void
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const t = useTheme()
|
||||
const [details, setDetails] = useState('')
|
||||
const control = Dialog.useDialogContext()
|
||||
const agent = useAgent()
|
||||
|
||||
const {
|
||||
@@ -124,11 +169,7 @@ function SubmitStep({
|
||||
await agent.createModerationReport(report)
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
control.close(() => {
|
||||
Toast.show(_(msg`Thank you. Your report has been sent.`))
|
||||
})
|
||||
},
|
||||
onSuccess: onComplete,
|
||||
})
|
||||
|
||||
const copy = useMemo(() => {
|
||||
@@ -181,13 +222,12 @@ function SubmitStep({
|
||||
<View style={[a.relative, a.w_full]}>
|
||||
<Dialog.Input
|
||||
multiline
|
||||
value={details}
|
||||
defaultValue={details}
|
||||
onChangeText={setDetails}
|
||||
label="Text field"
|
||||
label={_(msg`Text field`)}
|
||||
style={{paddingRight: 60}}
|
||||
numberOfLines={6}
|
||||
numberOfLines={5}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
@@ -231,7 +271,115 @@ function SubmitStep({
|
||||
<ButtonText>
|
||||
<Trans>Send report</Trans>
|
||||
</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>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
@@ -11,10 +11,12 @@ import React from 'react'
|
||||
import {formatDistance, Locale} from 'date-fns'
|
||||
import {
|
||||
ca,
|
||||
da,
|
||||
de,
|
||||
el,
|
||||
enGB,
|
||||
es,
|
||||
eu,
|
||||
fi,
|
||||
fr,
|
||||
gl,
|
||||
@@ -30,6 +32,7 @@ import {
|
||||
ptBR,
|
||||
ro,
|
||||
ru,
|
||||
sv,
|
||||
th,
|
||||
tr,
|
||||
uk,
|
||||
@@ -50,10 +53,12 @@ const locales: Record<AppLanguage, Locale | undefined> = {
|
||||
an: undefined,
|
||||
ast: undefined,
|
||||
ca,
|
||||
da,
|
||||
de,
|
||||
el,
|
||||
['en-GB']: enGB,
|
||||
es,
|
||||
eu,
|
||||
fi,
|
||||
fr,
|
||||
ga: undefined,
|
||||
@@ -71,6 +76,7 @@ const locales: Record<AppLanguage, Locale | undefined> = {
|
||||
['pt-BR']: ptBR,
|
||||
ro,
|
||||
ru,
|
||||
sv,
|
||||
th,
|
||||
tr,
|
||||
uk,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import React, {useContext} from 'react'
|
||||
import React from 'react'
|
||||
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 {useLingui} from '@lingui/react'
|
||||
|
||||
@@ -13,6 +11,7 @@ import {
|
||||
import {useTrendingTopics} from '#/state/queries/trending/useTrendingTopics'
|
||||
import {useTrendingConfig} from '#/state/trending-config'
|
||||
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
|
||||
import {atoms as a, useGutters, useTheme} from '#/alf'
|
||||
import {Button, ButtonIcon} from '#/components/Button'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||
@@ -41,13 +40,9 @@ export function Inner() {
|
||||
setTrendingDisabled(true)
|
||||
}, [setTrendingDisabled])
|
||||
|
||||
const drawerGesture = useContext(DrawerGestureContext) ?? Gesture.Native() // noop for web
|
||||
const trendingScrollGesture =
|
||||
Gesture.Native().blocksExternalGesture(drawerGesture)
|
||||
|
||||
return error || noTopics ? null : (
|
||||
<View style={[t.atoms.border_contrast_low, a.border_t]}>
|
||||
<GestureDetector gesture={trendingScrollGesture}>
|
||||
<BlockDrawerGesture>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
@@ -127,7 +122,7 @@ export function Inner() {
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</GestureDetector>
|
||||
</BlockDrawerGesture>
|
||||
|
||||
<Prompt.Basic
|
||||
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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -41,6 +41,7 @@ export class FeedViewPostsSlice {
|
||||
isFallbackMarker: boolean
|
||||
isOrphan: boolean
|
||||
rootUri: string
|
||||
feedPostUri: string
|
||||
|
||||
constructor(feedPost: FeedViewPost) {
|
||||
const {post, reply, reason} = feedPost
|
||||
@@ -48,6 +49,7 @@ export class FeedViewPostsSlice {
|
||||
this.isIncompleteThread = false
|
||||
this.isFallbackMarker = false
|
||||
this.isOrphan = false
|
||||
this.feedPostUri = post.uri
|
||||
if (AppBskyFeedDefs.isPostView(reply?.root)) {
|
||||
this.rootUri = reply.root.uri
|
||||
} else {
|
||||
|
||||
@@ -23,7 +23,9 @@ export class AuthorFeedAPI implements FeedAPI {
|
||||
|
||||
get 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
|
||||
}
|
||||
|
||||
|
||||
@@ -193,7 +193,12 @@ export async function post(
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
rt = shortenLinks(rt)
|
||||
|
||||
+20
-24
@@ -1,27 +1,23 @@
|
||||
// import {init} from '@bitdrift/react-native'
|
||||
// import {Statsig} from 'statsig-react-native-expo'
|
||||
// export {debug, error, info, warn} from '@bitdrift/react-native'
|
||||
import {init, SessionStrategy} from '@bitdrift/react-native'
|
||||
import {Statsig} from 'statsig-react-native-expo'
|
||||
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(() => {
|
||||
// let isEnabled = false
|
||||
// try {
|
||||
// if (Statsig.checkGate('enable_bitdrift')) {
|
||||
// isEnabled = true
|
||||
// }
|
||||
// } catch (e) {
|
||||
// // Statsig may complain about it being called too early.
|
||||
// }
|
||||
// if (isEnabled && BITDRIFT_API_KEY) {
|
||||
// init(BITDRIFT_API_KEY, {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) {}
|
||||
initPromise.then(() => {
|
||||
let isEnabled = false
|
||||
try {
|
||||
if (Statsig.checkGate('enable_bitdrift')) {
|
||||
isEnabled = true
|
||||
}
|
||||
} catch (e) {
|
||||
// Statsig may complain about it being called too early.
|
||||
}
|
||||
if (isEnabled && BITDRIFT_API_KEY) {
|
||||
init(BITDRIFT_API_KEY, SessionStrategy.Activity, {
|
||||
url: 'https://api-bsky.bitdrift.io',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -52,6 +52,8 @@ export const MAX_DM_GRAPHEME_LENGTH = 1000
|
||||
// but increasing limit per user feedback
|
||||
export const MAX_ALT_TEXT = 2000
|
||||
|
||||
export const MAX_REPORT_REASON_GRAPHEME_LENGTH = 2000
|
||||
|
||||
export function IS_TEST_USER(handle?: string) {
|
||||
return handle && handle?.endsWith('.test')
|
||||
}
|
||||
@@ -63,6 +65,21 @@ export function IS_PROD_SERVICE(url?: string) {
|
||||
export const PROD_DEFAULT_FEED = (rkey: string) =>
|
||||
`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 = {
|
||||
width: 2000,
|
||||
height: 2000,
|
||||
@@ -109,6 +126,11 @@ export const BSKY_FEED_OWNER_DIDS = [
|
||||
|
||||
export const DISCOVER_FEED_URI =
|
||||
'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 = {
|
||||
type: 'feed',
|
||||
value: DISCOVER_FEED_URI,
|
||||
@@ -119,6 +141,11 @@ export const TIMELINE_SAVED_FEED = {
|
||||
value: 'following',
|
||||
pinned: true,
|
||||
}
|
||||
export const VIDEO_SAVED_FEED = {
|
||||
type: 'feed',
|
||||
value: VIDEO_FEED_URI,
|
||||
pinned: true,
|
||||
}
|
||||
|
||||
export const RECOMMENDED_SAVED_FEEDS: Pick<
|
||||
AppBskyActorDefs.SavedFeed,
|
||||
|
||||
@@ -19,9 +19,13 @@ export function makeProfileLink(
|
||||
export function makeCustomFeedLink(
|
||||
did: 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[]) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {NavigationState, PartialState} from '@react-navigation/native'
|
||||
import type {NativeStackNavigationProp} from '@react-navigation/native-stack'
|
||||
|
||||
import {VideoFeedSourceContext} from '#/screens/VideoFeed/types'
|
||||
|
||||
export type {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
|
||||
export type CommonNavigatorParams = {
|
||||
@@ -20,7 +22,11 @@ export type CommonNavigatorParams = {
|
||||
PostLikedBy: {name: string; rkey: string}
|
||||
PostRepostedBy: {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}
|
||||
ProfileLabelerLikedBy: {name: string}
|
||||
Debug: undefined
|
||||
@@ -57,6 +63,7 @@ export type CommonNavigatorParams = {
|
||||
StarterPackShort: {code: string}
|
||||
StarterPackWizard: undefined
|
||||
StarterPackEdit: {rkey?: string}
|
||||
VideoFeed: VideoFeedSourceContext
|
||||
}
|
||||
|
||||
export type BottomTabNavigatorParams = CommonNavigatorParams & {
|
||||
|
||||
@@ -13,7 +13,12 @@ export type LogEvents = {
|
||||
withPassword: boolean
|
||||
}
|
||||
'account:loggedOut': {
|
||||
logContext: 'SwitchAccount' | 'Settings' | 'SignupQueued' | 'Deactivated'
|
||||
logContext:
|
||||
| 'SwitchAccount'
|
||||
| 'Settings'
|
||||
| 'SignupQueued'
|
||||
| 'Deactivated'
|
||||
| 'Takendown'
|
||||
scope: 'current' | 'every'
|
||||
}
|
||||
'notifications:openApp': {}
|
||||
@@ -131,16 +136,16 @@ export type LogEvents = {
|
||||
doesPosterFollowLiker: boolean | undefined
|
||||
likerClout: number | undefined
|
||||
postClout: number | undefined
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||
}
|
||||
'post:repost': {
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||
}
|
||||
'post:unlike': {
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||
}
|
||||
'post:unrepost': {
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||
}
|
||||
'post:mute': {}
|
||||
'post:unmute': {}
|
||||
@@ -163,6 +168,7 @@ export type LogEvents = {
|
||||
| 'FeedInterstitial'
|
||||
| 'ProfileHeaderSuggestedFollows'
|
||||
| 'PostOnboardingFindFollows'
|
||||
| 'ImmersiveVideo'
|
||||
}
|
||||
'suggestedUser:follow': {
|
||||
logContext:
|
||||
@@ -199,6 +205,7 @@ export type LogEvents = {
|
||||
| 'FeedInterstitial'
|
||||
| 'ProfileHeaderSuggestedFollows'
|
||||
| 'PostOnboardingFindFollows'
|
||||
| 'ImmersiveVideo'
|
||||
}
|
||||
'chat:create': {
|
||||
logContext: 'ProfileHeader' | 'NewChatDialog' | 'SendViaChatDialog'
|
||||
@@ -269,6 +276,15 @@ export type LogEvents = {
|
||||
'recommendedTopic:click': {
|
||||
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:followDialog:open': {}
|
||||
|
||||
@@ -3,6 +3,7 @@ export type Gate =
|
||||
| 'debug_show_feedcontext'
|
||||
| 'debug_subscriptions'
|
||||
| 'new_postonboarding'
|
||||
| 'onboarding_add_video_feed'
|
||||
| 'remove_show_latest_button'
|
||||
| 'test_gate_1'
|
||||
| 'test_gate_2'
|
||||
|
||||
@@ -5,8 +5,7 @@ import {sha256} from 'js-sha256'
|
||||
import {Statsig, StatsigProvider} from 'statsig-react-native-expo'
|
||||
|
||||
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 {isWeb} from '#/platform/detection'
|
||||
import * as persisted from '#/state/persisted'
|
||||
@@ -108,8 +107,7 @@ export function logEvent<E extends keyof LogEvents>(
|
||||
console.groupCollapsed(eventName)
|
||||
console.log(fullMetadata)
|
||||
console.groupEnd()
|
||||
// TODO: Reenable when the build issue is fixed.
|
||||
// bitdrift.info(eventName, fullMetadata)
|
||||
bitdrift.info(eventName, fullMetadata)
|
||||
} catch (e) {
|
||||
// A log should never interrupt the calling code, whatever happens.
|
||||
logger.error('Failed to log an event', {message: e})
|
||||
|
||||
@@ -161,6 +161,8 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
|
||||
return AppLanguage.ast
|
||||
case 'ca':
|
||||
return AppLanguage.ca
|
||||
case 'da':
|
||||
return AppLanguage.da
|
||||
case 'de':
|
||||
return AppLanguage.de
|
||||
case 'el':
|
||||
@@ -169,6 +171,8 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
|
||||
return AppLanguage.en_GB
|
||||
case 'es':
|
||||
return AppLanguage.es
|
||||
case 'eu':
|
||||
return AppLanguage.eu
|
||||
case 'fi':
|
||||
return AppLanguage.fi
|
||||
case 'fr':
|
||||
@@ -203,6 +207,8 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
|
||||
return AppLanguage.ro
|
||||
case 'ru':
|
||||
return AppLanguage.ru
|
||||
case 'sv':
|
||||
return AppLanguage.sv
|
||||
case 'th':
|
||||
return AppLanguage.th
|
||||
case 'tr':
|
||||
|
||||
@@ -14,11 +14,13 @@ import {AppLanguage} from '#/locale/languages'
|
||||
import {messages as messagesAn} from '#/locale/locales/an/messages'
|
||||
import {messages as messagesAst} from '#/locale/locales/ast/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 messagesEl} from '#/locale/locales/el/messages'
|
||||
import {messages as messagesEn} from '#/locale/locales/en/messages'
|
||||
import {messages as messagesEn_GB} from '#/locale/locales/en-GB/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 messagesFr} from '#/locale/locales/fr/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 messagesRo} from '#/locale/locales/ro/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 messagesTr} from '#/locale/locales/tr/messages'
|
||||
import {messages as messagesUk} from '#/locale/locales/uk/messages'
|
||||
@@ -74,6 +77,14 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
])
|
||||
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: {
|
||||
i18n.loadAndActivate({locale, messages: messagesDe})
|
||||
await Promise.all([
|
||||
@@ -106,6 +117,14 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
])
|
||||
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: {
|
||||
i18n.loadAndActivate({locale, messages: messagesFi})
|
||||
await Promise.all([
|
||||
@@ -238,6 +257,14 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
])
|
||||
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: {
|
||||
i18n.loadAndActivate({locale, messages: messagesTh})
|
||||
await Promise.all([
|
||||
|
||||
@@ -24,6 +24,10 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
mod = await import(`./locales/ca/messages`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.da: {
|
||||
mod = await import(`./locales/da/messages`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.de: {
|
||||
mod = await import(`./locales/de/messages`)
|
||||
break
|
||||
@@ -40,6 +44,10 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
mod = await import(`./locales/es/messages`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.eu: {
|
||||
mod = await import(`./locales/eu/messages`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.fi: {
|
||||
mod = await import(`./locales/fi/messages`)
|
||||
break
|
||||
@@ -108,6 +116,10 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
mod = await import(`./locales/ru/messages`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.sv: {
|
||||
mod = await import(`./locales/sv/messages`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.th: {
|
||||
mod = await import(`./locales/th/messages`)
|
||||
break
|
||||
|
||||
@@ -9,10 +9,12 @@ export enum AppLanguage {
|
||||
an = 'an',
|
||||
ast = 'ast',
|
||||
ca = 'ca',
|
||||
da = 'da',
|
||||
de = 'de',
|
||||
el = 'el',
|
||||
en_GB = 'en-GB',
|
||||
es = 'es',
|
||||
eu = 'eu',
|
||||
fi = 'fi',
|
||||
fr = 'fr',
|
||||
ga = 'ga',
|
||||
@@ -30,6 +32,7 @@ export enum AppLanguage {
|
||||
pt_BR = 'pt-BR',
|
||||
ro = 'ro',
|
||||
ru = 'ru',
|
||||
sv = 'sv',
|
||||
th = 'th',
|
||||
tr = 'tr',
|
||||
uk = 'uk',
|
||||
@@ -49,10 +52,12 @@ export const APP_LANGUAGES: AppLanguageConfig[] = [
|
||||
{code2: AppLanguage.an, name: 'Aragonés – Aragonese'},
|
||||
{code2: AppLanguage.ast, name: 'Asturianu – Asturian'},
|
||||
{code2: AppLanguage.ca, name: 'Català – Catalan'},
|
||||
{code2: AppLanguage.da, name: 'Dansk – Danish'},
|
||||
{code2: AppLanguage.de, name: 'Deutsch – German'},
|
||||
{code2: AppLanguage.el, name: 'Ελληνικά – Greek'},
|
||||
{code2: AppLanguage.en_GB, name: 'English (UK)'},
|
||||
{code2: AppLanguage.es, name: 'Español – Spanish'},
|
||||
{code2: AppLanguage.eu, name: 'Euskera – Basque'},
|
||||
{code2: AppLanguage.fi, name: 'Suomi – Finnish'},
|
||||
{code2: AppLanguage.fr, name: 'Français – French'},
|
||||
{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.ro, name: 'Română – Romanian'},
|
||||
{code2: AppLanguage.ru, name: 'Русский – Russian'},
|
||||
{code2: AppLanguage.sv, name: 'Svenska – Swedish'},
|
||||
{code2: AppLanguage.th, name: 'ภาษาไทย – Thai'},
|
||||
{code2: AppLanguage.tr, name: 'Türkçe – Turkish'},
|
||||
{code2: AppLanguage.uk, name: 'Українська – Ukrainian'},
|
||||
|
||||
@@ -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:173
|
||||
msgid "Close emoji picker"
|
||||
msgstr ""
|
||||
msgstr "Tanca el selector d'emojis"
|
||||
|
||||
#: src/components/dialogs/GifSelect.tsx:169
|
||||
msgid "Close GIF dialog"
|
||||
@@ -2429,7 +2429,7 @@ msgstr "Creat {0}"
|
||||
|
||||
#: src/screens/List/ListHiddenScreen.tsx:127
|
||||
msgid "Creator has been blocked"
|
||||
msgstr ""
|
||||
msgstr "S'ha bloquejat al creador"
|
||||
|
||||
#: src/screens/Onboarding/index.tsx:26
|
||||
#: src/screens/Onboarding/state.ts:99
|
||||
@@ -3049,7 +3049,7 @@ msgstr "Ensenyament"
|
||||
|
||||
#: src/screens/List/ListHiddenScreen.tsx:141
|
||||
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/Signup/StepInfo/index.tsx:170
|
||||
@@ -4837,7 +4837,7 @@ msgstr "Llista per <0/>"
|
||||
|
||||
#: src/view/com/profile/ProfileSubpageHeader.tsx:154
|
||||
msgid "List by you"
|
||||
msgstr ""
|
||||
msgstr "Llista teva"
|
||||
|
||||
#: src/view/screens/ProfileList.tsx:455
|
||||
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:174
|
||||
msgid "Tap to close the emoji picker"
|
||||
msgstr ""
|
||||
msgstr "Toca per a tancar el selector d'emojis"
|
||||
|
||||
#: src/components/ProgressGuide/Toast.tsx:156
|
||||
msgid "Tap to dismiss"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+115
-115
@@ -1,6 +1,5 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"POT-Creation-Date: 2023-12-22 01:56+0530\n"
|
||||
msgstr "POT-Creation-Date: 2023-12-22 01:56+0530\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
@@ -9,13 +8,14 @@ msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: brodieavoult\n"
|
||||
"Last-Translator: hogaza\n"
|
||||
"Language-Team: brodieavoult\n"
|
||||
"Plural-Forms: \n"
|
||||
"Plural-Forms:\n"
|
||||
" \n"
|
||||
|
||||
#: src/components/ProgressGuide/FollowDialog.tsx:567
|
||||
msgid "(active)"
|
||||
msgstr ""
|
||||
msgstr "(activo)"
|
||||
|
||||
#: src/screens/Messages/components/ChatListItem.tsx:130
|
||||
msgid "(contains embedded content)"
|
||||
@@ -37,11 +37,11 @@ msgstr "{0, plural, one {# día} other {# días}}"
|
||||
|
||||
#: src/screens/Profile/ProfileFollowers.tsx:40
|
||||
msgid "{0, plural, one {# follower} other {# followers}}"
|
||||
msgstr ""
|
||||
msgstr "{0, plural, one {# seguidor} other {# seguidores}}"
|
||||
|
||||
#: src/screens/Profile/ProfileFollows.tsx:40
|
||||
msgid "{0, plural, one {# following} other {# following}}"
|
||||
msgstr ""
|
||||
msgstr "{0, plural, one {# siguiendo} other {# siguiendo}}"
|
||||
|
||||
#: src/lib/hooks/useTimeAgo.ts:146
|
||||
msgid "{0, plural, one {# hour} other {# hours}}"
|
||||
@@ -65,15 +65,15 @@ msgstr "{0, plural, one {# hora} other {# horas}}"
|
||||
|
||||
#: src/components/moderation/LabelsOnMe.tsx:53
|
||||
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
|
||||
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
|
||||
msgid "{0, plural, one {# like} other {# likes}}"
|
||||
msgstr ""
|
||||
msgstr "{0, plural, one {# me gusta} other {# me gusta}}"
|
||||
|
||||
#: src/lib/hooks/useTimeAgo.ts:136
|
||||
msgid "{0, plural, one {# minute} other {# minutes}}"
|
||||
@@ -85,7 +85,7 @@ msgstr "{0, plural, one {# mes} other {# meses}}"
|
||||
|
||||
#: src/screens/Post/PostQuotes.tsx:41
|
||||
msgid "{0, plural, one {# quote} other {# quotes}}"
|
||||
msgstr ""
|
||||
msgstr "{0, plural, one {# cita} other {# citas}}"
|
||||
|
||||
#: src/screens/Post/PostRepostedBy.tsx:41
|
||||
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
|
||||
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/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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
msgid "{firstAuthorLink} reposted your post"
|
||||
@@ -308,11 +308,11 @@ msgstr "{firstAuthorName} y {formattedAuthorsCount} más te siguieron"
|
||||
|
||||
#: src/view/com/notifications/NotificationFeedItem.tsx:320
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
msgid "{notificationCount} unread items"
|
||||
msgstr ""
|
||||
msgstr "{notificationCount} elementos sin leer"
|
||||
|
||||
#: src/view/shell/Drawer.tsx:448
|
||||
msgid "{numUnreadNotifications} unread"
|
||||
@@ -532,16 +532,16 @@ msgstr "Cuenta seguida"
|
||||
|
||||
#: src/view/com/profile/ProfileMenu.tsx:108
|
||||
msgid "Account muted"
|
||||
msgstr "Cuenta muteada"
|
||||
msgstr "Cuenta silenciada"
|
||||
|
||||
#: src/components/moderation/ModerationDetailsDialog.tsx:101
|
||||
#: src/lib/moderation/useModerationCauseDescription.ts:96
|
||||
msgid "Account Muted"
|
||||
msgstr "Cuenta muteada"
|
||||
msgstr "Cuenta silenciada"
|
||||
|
||||
#: src/components/moderation/ModerationDetailsDialog.tsx:87
|
||||
msgid "Account Muted by List"
|
||||
msgstr "Cuenta muteada por lista"
|
||||
msgstr "Cuenta silenciada por lista"
|
||||
|
||||
#: src/screens/Settings/Settings.tsx:428
|
||||
msgid "Account options"
|
||||
@@ -645,7 +645,7 @@ msgstr "Agregar nueva publicación"
|
||||
#: src/view/screens/ProfileList.tsx:903
|
||||
#: src/view/screens/ProfileList.tsx:921
|
||||
msgid "Add people"
|
||||
msgstr ""
|
||||
msgstr "Añadir personas"
|
||||
|
||||
#: src/screens/StarterPack/Wizard/index.tsx:197
|
||||
#~ 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
|
||||
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:197
|
||||
@@ -727,7 +727,7 @@ msgstr "Avanzado"
|
||||
|
||||
#: src/view/screens/Notifications.tsx:86
|
||||
msgid "All"
|
||||
msgstr ""
|
||||
msgstr "Todas"
|
||||
|
||||
#: src/screens/StarterPack/StarterPackScreen.tsx:381
|
||||
msgid "All accounts have been followed!"
|
||||
@@ -754,7 +754,7 @@ msgstr "Permitir nuevos mensajes de"
|
||||
|
||||
#: src/components/dialogs/PostInteractionSettingsDialog.tsx:314
|
||||
msgid "Allow quote posts"
|
||||
msgstr ""
|
||||
msgstr "Permitir citas"
|
||||
|
||||
#: src/components/dialogs/PostInteractionSettingsDialog.tsx:359
|
||||
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:23
|
||||
msgid "App Icon"
|
||||
msgstr ""
|
||||
msgstr "Icono de la app"
|
||||
|
||||
#: src/screens/Settings/LanguageSettings.tsx:80
|
||||
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
|
||||
msgid "BETA"
|
||||
msgstr ""
|
||||
msgstr "BETA"
|
||||
|
||||
#: src/components/dialogs/BirthDateSettings.tsx:106
|
||||
#: 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
|
||||
msgid "Bluesky+"
|
||||
msgstr ""
|
||||
msgstr "Bluesky+"
|
||||
|
||||
#: src/screens/Settings/AppIconSettings/index.tsx:102
|
||||
msgid "Bluesky+ icons"
|
||||
msgstr ""
|
||||
msgstr "Iconos Bluesky+"
|
||||
|
||||
#: src/lib/moderation/useLabelBehaviorDescription.ts:53
|
||||
msgid "Blur images"
|
||||
@@ -1339,15 +1339,15 @@ msgstr "Explorar otros feeds"
|
||||
|
||||
#: src/components/TrendingTopics.tsx:176
|
||||
msgid "Browse posts about {displayName}"
|
||||
msgstr ""
|
||||
msgstr "Explorar posts sobre {displayName}"
|
||||
|
||||
#: src/components/TrendingTopics.tsx:184
|
||||
msgid "Browse posts tagged with {displayName}"
|
||||
msgstr ""
|
||||
msgstr "Explorar posts etiquetados con {displayName}"
|
||||
|
||||
#: src/components/TrendingTopics.tsx:222
|
||||
msgid "Browse topic {displayName}"
|
||||
msgstr ""
|
||||
msgstr "Explorar el tema {displayName}"
|
||||
|
||||
#: src/view/com/auth/SplashScreen.web.tsx:168
|
||||
msgid "Business"
|
||||
@@ -1371,7 +1371,7 @@ msgstr "Por {0}"
|
||||
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:447
|
||||
msgid "By <0>{0}</0>"
|
||||
msgstr ""
|
||||
msgstr "Por <0>{0}</0>"
|
||||
|
||||
#: src/screens/Signup/StepInfo/Policies.tsx:80
|
||||
#~ msgid "By creating an account you agree to the {els}."
|
||||
@@ -1504,12 +1504,12 @@ msgstr "Cambiar"
|
||||
|
||||
#: src/screens/Settings/AppIconSettings/index.tsx:39
|
||||
msgid "Change app icon"
|
||||
msgstr ""
|
||||
msgstr "Cambiar icono de la app"
|
||||
|
||||
#: src/screens/Settings/AppIconSettings/index.tsx:38
|
||||
#: src/screens/Settings/AppIconSettings/index.tsx:221
|
||||
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:101
|
||||
@@ -1578,7 +1578,7 @@ msgstr "Configuración de chat"
|
||||
|
||||
#: src/components/dms/ConvoMenu.tsx:84
|
||||
msgid "Chat unmuted"
|
||||
msgstr "Chat demuteado"
|
||||
msgstr "Chat no silenciado"
|
||||
|
||||
#: src/screens/SignupQueued.tsx:78
|
||||
#: 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/Search/Explore.tsx:411
|
||||
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
|
||||
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:173
|
||||
msgid "Close emoji picker"
|
||||
msgstr ""
|
||||
msgstr "Cerrar el selector de emoji"
|
||||
|
||||
#: src/components/dialogs/GifSelect.tsx:169
|
||||
msgid "Close GIF dialog"
|
||||
msgstr "Cerrar ventana de GIF"
|
||||
msgstr "Cerrar la ventana de GIF"
|
||||
|
||||
#: src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx:31
|
||||
msgid "Close image"
|
||||
@@ -1934,7 +1934,7 @@ msgstr "Contactar a soporte"
|
||||
|
||||
#: src/screens/Settings/ContentAndMediaSettings.tsx:49
|
||||
msgid "Content & Media"
|
||||
msgstr ""
|
||||
msgstr "Contenido y medios"
|
||||
|
||||
#: src/screens/Settings/AccessibilitySettings.tsx:109
|
||||
#: src/screens/Settings/Settings.tsx:175
|
||||
@@ -2212,7 +2212,7 @@ msgstr "Creado {0}"
|
||||
|
||||
#: src/screens/List/ListHiddenScreen.tsx:127
|
||||
msgid "Creator has been blocked"
|
||||
msgstr ""
|
||||
msgstr "Se ha bloqueado al creador"
|
||||
|
||||
#: src/screens/Onboarding/index.tsx:26
|
||||
#: src/screens/Onboarding/state.ts:99
|
||||
@@ -2286,7 +2286,7 @@ msgstr "Por Defecto"
|
||||
|
||||
#: src/screens/Settings/AppIconSettings/index.tsx:75
|
||||
msgid "Default icons"
|
||||
msgstr ""
|
||||
msgstr "Iconos por defecto"
|
||||
|
||||
#: src/components/dms/MessageMenu.tsx:151
|
||||
#: src/screens/Settings/AppPasswords.tsx:212
|
||||
@@ -2720,7 +2720,7 @@ msgstr "Editar los detalles de la lista"
|
||||
|
||||
#: src/view/com/modals/CreateOrEditList.tsx:230
|
||||
msgid "Edit Moderation List"
|
||||
msgstr "Editar lista de Moderación"
|
||||
msgstr "Editar lista de moderación"
|
||||
|
||||
#: src/Navigation.tsx:296
|
||||
#: src/view/screens/Feeds.tsx:515
|
||||
@@ -2792,7 +2792,7 @@ msgstr "Educación"
|
||||
|
||||
#: src/screens/List/ListHiddenScreen.tsx:141
|
||||
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/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:136
|
||||
msgid "Enable trending topics"
|
||||
msgstr ""
|
||||
msgstr "Activar las tendencias"
|
||||
|
||||
#: src/screens/Messages/Settings.tsx:130
|
||||
#: src/screens/Messages/Settings.tsx:133
|
||||
@@ -3261,7 +3261,7 @@ msgstr "Feed por {0}"
|
||||
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:353
|
||||
msgid "Feed menu"
|
||||
msgstr ""
|
||||
msgstr "Menú de feed"
|
||||
|
||||
#: src/view/screens/Feeds.tsx:709
|
||||
#~ msgid "Feed offline"
|
||||
@@ -3308,7 +3308,7 @@ msgstr "¡Feeds actualizados!"
|
||||
|
||||
#: src/screens/Search/components/ExploreRecommendations.tsx:54
|
||||
msgid "Feeds we think you might like."
|
||||
msgstr ""
|
||||
msgstr "Creemos que estos estos feeds te gustarán."
|
||||
|
||||
#: src/view/com/modals/ChangeHandle.tsx:468
|
||||
#~ msgid "File Contents"
|
||||
@@ -3340,7 +3340,7 @@ msgstr "Buscar cuentas para seguir"
|
||||
#: src/components/ProgressGuide/FollowDialog.tsx:88
|
||||
#: src/components/ProgressGuide/FollowDialog.tsx:417
|
||||
msgid "Find people to follow"
|
||||
msgstr ""
|
||||
msgstr "Buscar a gente para seguir"
|
||||
|
||||
#: src/view/screens/Search/Search.tsx:595
|
||||
msgid "Find posts and users on Bluesky"
|
||||
@@ -3405,7 +3405,7 @@ msgstr "Seguir {name}"
|
||||
#: src/components/ProgressGuide/List.tsx:52
|
||||
#: src/state/shell/progress-guide.tsx:229
|
||||
msgid "Follow 10 accounts"
|
||||
msgstr ""
|
||||
msgstr "Sigue 10 cuentas"
|
||||
|
||||
#: src/components/ProgressGuide/List.tsx:69
|
||||
msgid "Follow 7 accounts"
|
||||
@@ -3856,13 +3856,13 @@ msgstr "¿Ocultar esta respuesta?"
|
||||
#: src/screens/Search/components/ExploreTrendingTopics.tsx:83
|
||||
#: src/view/shell/desktop/SidebarTrendingTopics.tsx:62
|
||||
msgid "Hide trending topics"
|
||||
msgstr ""
|
||||
msgstr "Ocultar las tendencias"
|
||||
|
||||
#: src/components/interstitials/Trending.tsx:99
|
||||
#: src/screens/Search/components/ExploreTrendingTopics.tsx:135
|
||||
#: src/view/shell/desktop/SidebarTrendingTopics.tsx:108
|
||||
msgid "Hide trending topics?"
|
||||
msgstr ""
|
||||
msgstr "¿Quieres ocultar las tendencias?"
|
||||
|
||||
#: src/view/com/notifications/NotificationFeedItem.tsx:592
|
||||
msgid "Hide user list"
|
||||
@@ -4322,11 +4322,11 @@ msgstr "Claro"
|
||||
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:514
|
||||
msgid "Like"
|
||||
msgstr ""
|
||||
msgstr "Me gusta"
|
||||
|
||||
#: src/view/com/util/post-ctrls/PostCtrls.tsx:311
|
||||
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
|
||||
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
|
||||
msgid "Like feed"
|
||||
msgstr ""
|
||||
msgstr "Me gusta este feed"
|
||||
|
||||
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:275
|
||||
msgid "Like this feed"
|
||||
@@ -4349,18 +4349,18 @@ msgstr "Dar \"me gusta\" a este feed"
|
||||
#: src/Navigation.tsx:236
|
||||
#: src/Navigation.tsx:241
|
||||
msgid "Liked by"
|
||||
msgstr "Le ha gustado a"
|
||||
msgstr "Le gusta a"
|
||||
|
||||
#: src/screens/Post/PostLikedBy.tsx:38
|
||||
#: src/screens/Profile/ProfileLabelerLikedBy.tsx:29
|
||||
#: src/view/screens/ProfileFeedLikedBy.tsx:30
|
||||
msgid "Liked By"
|
||||
msgstr "Le ha gustado a"
|
||||
msgstr "Le gusta a"
|
||||
|
||||
#: src/components/FeedCard.tsx:213
|
||||
#: src/view/com/feeds/FeedSourceCard.tsx:303
|
||||
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
|
||||
#~ msgid "Liked by {0} {1}"
|
||||
@@ -4375,7 +4375,7 @@ msgstr ""
|
||||
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:295
|
||||
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:309
|
||||
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:301
|
||||
@@ -4393,7 +4393,7 @@ msgstr ""
|
||||
|
||||
#: src/view/screens/Profile.tsx:234
|
||||
msgid "Likes"
|
||||
msgstr "\"Me gusta\""
|
||||
msgstr "Me gusta"
|
||||
|
||||
#: src/view/com/post-thread/PostThreadItem.tsx:217
|
||||
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:631
|
||||
msgid "Linear"
|
||||
msgstr ""
|
||||
msgstr "Lineal"
|
||||
|
||||
#: src/Navigation.tsx:198
|
||||
msgid "List"
|
||||
@@ -4423,11 +4423,11 @@ msgstr "Lista por {0}"
|
||||
|
||||
#: src/view/com/profile/ProfileSubpageHeader.tsx:156
|
||||
msgid "List by <0/>"
|
||||
msgstr ""
|
||||
msgstr "Lista de {0}"
|
||||
|
||||
#: src/view/com/profile/ProfileSubpageHeader.tsx:154
|
||||
msgid "List by you"
|
||||
msgstr ""
|
||||
msgstr "Lista tuya"
|
||||
|
||||
#: src/view/screens/ProfileList.tsx:455
|
||||
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
|
||||
msgid "Logo by @sawaratsuki.bsky.social"
|
||||
msgstr ""
|
||||
msgstr "Logo de @sawaratsuki.bsky.social</0>"
|
||||
|
||||
#: src/view/shell/desktop/RightNav.tsx:103
|
||||
#~ msgid "Logo by <0/>"
|
||||
@@ -4532,7 +4532,7 @@ msgstr ""
|
||||
#: src/view/shell/desktop/RightNav.tsx:118
|
||||
#: src/view/shell/Drawer.tsx:629
|
||||
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
|
||||
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
|
||||
msgid "Looks like XXXXX-XXXXX"
|
||||
msgstr "Es como XXXXX-XXXXX"
|
||||
msgstr "Es algo así: XXXXX-XXXXX"
|
||||
|
||||
#: src/screens/Feeds/NoSavedFeedsOfAnyType.tsx:39
|
||||
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
|
||||
msgid "Mentions"
|
||||
msgstr ""
|
||||
msgstr "Menciones"
|
||||
|
||||
#: src/components/Menu/index.tsx:96
|
||||
#: 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/ModerationModlists.tsx:66
|
||||
msgid "New list"
|
||||
msgstr ""
|
||||
msgstr "Nueva lista"
|
||||
|
||||
#: src/components/dms/NewMessagesPill.tsx:92
|
||||
msgid "New messages"
|
||||
@@ -5394,31 +5394,31 @@ msgstr "Abrir el creador de avatares"
|
||||
|
||||
#: src/screens/Settings/AccountSettings.tsx:127
|
||||
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:273
|
||||
msgid "Open conversation options"
|
||||
msgstr "Abrir opciones de conversación"
|
||||
msgstr "Abrir las opciones de conversación"
|
||||
|
||||
#: src/components/Layout/Header/index.tsx:145
|
||||
msgid "Open drawer menu"
|
||||
msgstr ""
|
||||
msgstr "Abrir el menú lateral"
|
||||
|
||||
#: src/screens/Messages/components/MessageInput.web.tsx:181
|
||||
#: src/view/com/composer/Composer.tsx:1228
|
||||
#: src/view/com/composer/Composer.tsx:1229
|
||||
msgid "Open emoji picker"
|
||||
msgstr "Abrir selector de emoji"
|
||||
msgstr "Abrir el selector de emoji"
|
||||
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:190
|
||||
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:295
|
||||
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
|
||||
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:526
|
||||
msgid "Pin feed"
|
||||
msgstr ""
|
||||
msgstr "Anclar el feed"
|
||||
|
||||
#: src/view/screens/ProfileList.tsx:685
|
||||
msgid "Pin to home"
|
||||
@@ -5767,7 +5767,7 @@ msgstr "Anclado"
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:162
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:174
|
||||
msgid "Pinned {0} to Home"
|
||||
msgstr ""
|
||||
msgstr "{0} anclados en inicio"
|
||||
|
||||
#: src/view/screens/SavedFeeds.tsx:123
|
||||
msgid "Pinned Feeds"
|
||||
@@ -6097,11 +6097,11 @@ msgstr "Público"
|
||||
|
||||
#: src/view/com/lists/MyLists.tsx:77
|
||||
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
|
||||
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
|
||||
#~ 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
|
||||
msgid "Recommended"
|
||||
msgstr ""
|
||||
msgstr "Recomendaciones"
|
||||
|
||||
#: src/screens/Messages/components/MessageListError.tsx:20
|
||||
msgid "Reconnect"
|
||||
@@ -6426,7 +6426,7 @@ msgstr "Responder"
|
||||
|
||||
#: src/view/com/util/post-ctrls/PostCtrls.tsx:263
|
||||
msgid "Reply ({0, plural, one {# reply} other {# replies}})"
|
||||
msgstr ""
|
||||
msgstr "Responder ({0, plural, one {# respuesta} other {# respuestas}})"
|
||||
|
||||
#: src/view/screens/PreferencesFollowingFeed.tsx:142
|
||||
#~ 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
|
||||
msgid "Reply sorting"
|
||||
msgstr ""
|
||||
msgstr "Orden de las respuestas"
|
||||
|
||||
#: src/view/com/post/Post.tsx:204
|
||||
#: src/view/com/posts/PostFeedItem.tsx:553
|
||||
@@ -6576,7 +6576,7 @@ msgstr "Republicar"
|
||||
|
||||
#: src/view/com/util/post-ctrls/RepostButton.tsx:74
|
||||
msgid "Repost ({0, plural, one {# repost} other {# reposts}})"
|
||||
msgstr ""
|
||||
msgstr "Republicar ({0, plural, one {# repost} other {# reposts}})"
|
||||
|
||||
#: src/screens/StarterPack/StarterPackScreen.tsx:548
|
||||
#: src/view/com/util/post-ctrls/RepostButton.tsx:148
|
||||
@@ -6860,15 +6860,15 @@ msgstr "Buscar"
|
||||
|
||||
#: src/components/ProgressGuide/FollowDialog.tsx:745
|
||||
msgid "Search by name or interest"
|
||||
msgstr ""
|
||||
msgstr "Buscar por nombre o interés"
|
||||
|
||||
#: src/view/screens/Feeds.tsx:441
|
||||
msgid "Search feeds"
|
||||
msgstr ""
|
||||
msgstr "Buscar feeds"
|
||||
|
||||
#: src/components/ProgressGuide/FollowDialog.tsx:575
|
||||
msgid "Search for \"{interestsDisplayName}\"{activeText}"
|
||||
msgstr ""
|
||||
msgstr "Buscar \"{interestsDisplayName}\"{activeText}"
|
||||
|
||||
#: src/view/shell/desktop/Search.tsx:201
|
||||
msgid "Search for \"{query}\""
|
||||
@@ -7148,7 +7148,7 @@ msgstr "Dirección del servidor"
|
||||
|
||||
#: src/screens/Settings/AppIconSettings/index.tsx:178
|
||||
msgid "Set app icon to {0}"
|
||||
msgstr ""
|
||||
msgstr "Cambiar el icono de la app a {0}"
|
||||
|
||||
#: src/screens/Moderation/index.tsx:290
|
||||
msgid "Set birthdate"
|
||||
@@ -7305,7 +7305,7 @@ msgstr "Compartir código QR"
|
||||
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:470
|
||||
msgid "Share this feed"
|
||||
msgstr ""
|
||||
msgstr "Compartir este feed"
|
||||
|
||||
#: src/screens/StarterPack/StarterPackScreen.tsx:416
|
||||
msgid "Share this starter pack"
|
||||
@@ -7436,11 +7436,11 @@ msgstr "Mostrar respuestas"
|
||||
|
||||
#: src/view/com/post-thread/PostThread.tsx:622
|
||||
msgid "Show replies as"
|
||||
msgstr ""
|
||||
msgstr "Mostrar las respuestas como"
|
||||
|
||||
#: src/screens/Settings/ThreadPreferences.tsx:151
|
||||
msgid "Show replies as threaded"
|
||||
msgstr ""
|
||||
msgstr "Mostrar las respuestas como hilos"
|
||||
|
||||
#: src/screens/Settings/ThreadPreferences.tsx:126
|
||||
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:52
|
||||
msgid "Create account"
|
||||
msgstr "Crear cuenta"
|
||||
msgstr "Crear una cuenta"
|
||||
|
||||
#: src/view/shell/NavSignupCard.tsx:47
|
||||
#~ 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
|
||||
msgid "Something wrong? Let us know."
|
||||
msgstr ""
|
||||
msgstr "¿Algo va mal? Avísanos."
|
||||
|
||||
#: src/App.native.tsx:122
|
||||
#: 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:938
|
||||
msgid "Start adding people"
|
||||
msgstr ""
|
||||
msgstr "Empieza a añadir personas"
|
||||
|
||||
#: src/view/screens/ProfileList.tsx:824
|
||||
#: src/view/screens/ProfileList.tsx:945
|
||||
msgid "Start adding people!"
|
||||
msgstr ""
|
||||
msgstr "¡Empieza a añadir personas!"
|
||||
|
||||
#: src/components/dms/dialogs/SearchablePeopleList.tsx:353
|
||||
msgid "Start chat with {displayName}"
|
||||
@@ -7750,7 +7750,7 @@ msgstr "Paquete de inicio de {0}"
|
||||
|
||||
#: src/view/com/profile/ProfileSubpageHeader.tsx:182
|
||||
msgid "Starter pack by <0/>"
|
||||
msgstr ""
|
||||
msgstr "Paquete de inicio de <0/>"
|
||||
|
||||
#: src/components/StarterPack/StarterPackCard.tsx:80
|
||||
#: src/view/com/profile/ProfileSubpageHeader.tsx:180
|
||||
@@ -7902,12 +7902,12 @@ msgstr "Solo etiquetas"
|
||||
|
||||
#: src/screens/Settings/AppIconSettings/index.tsx:216
|
||||
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:174
|
||||
msgid "Tap to close the emoji picker"
|
||||
msgstr ""
|
||||
msgstr "Toca para cerrar el selector de emoji"
|
||||
|
||||
#: src/components/ProgressGuide/Toast.tsx:156
|
||||
msgid "Tap to dismiss"
|
||||
@@ -7936,11 +7936,11 @@ msgstr "Toca para ver la imagen completa"
|
||||
|
||||
#: src/state/shell/progress-guide.tsx:233
|
||||
msgid "Task complete - 10 follows!"
|
||||
msgstr ""
|
||||
msgstr "¡Tarea completada: 10 cuentas seguidas!"
|
||||
|
||||
#: src/state/shell/progress-guide.tsx:223
|
||||
msgid "Task complete - 10 likes!"
|
||||
msgstr "¡Tarea completada - 10 \"me gusta\"!"
|
||||
msgstr "¡Tarea completada: 10 me gusta!"
|
||||
|
||||
#: src/components/ProgressGuide/List.tsx:64
|
||||
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:222
|
||||
msgid "The app will be restarted"
|
||||
msgstr ""
|
||||
msgstr "Se reiniciará la app"
|
||||
|
||||
#: src/components/moderation/ModerationDetailsDialog.tsx:127
|
||||
#~ 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
|
||||
msgid "This list is empty."
|
||||
msgstr ""
|
||||
msgstr "Esta lista está vacía"
|
||||
|
||||
#: src/screens/Profile/ErrorState.tsx:40
|
||||
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:609
|
||||
msgid "Thread options"
|
||||
msgstr ""
|
||||
msgstr "Opciones de hilos"
|
||||
|
||||
#: src/screens/Settings/ContentAndMediaSettings.tsx:66
|
||||
#: 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:641
|
||||
msgid "Threaded"
|
||||
msgstr ""
|
||||
msgstr "En hilos"
|
||||
|
||||
#: src/screens/Settings/ThreadPreferences.tsx:142
|
||||
msgid "Threaded mode"
|
||||
@@ -8604,7 +8604,7 @@ msgstr "Top"
|
||||
|
||||
#: src/Navigation.tsx:383
|
||||
msgid "Topic"
|
||||
msgstr ""
|
||||
msgstr "Tema"
|
||||
|
||||
#: src/view/com/modals/EditImage.tsx:272
|
||||
#~ msgid "Transformations"
|
||||
@@ -8622,7 +8622,7 @@ msgstr "Traducir"
|
||||
#: src/screens/Search/components/ExploreTrendingTopics.tsx:69
|
||||
#: src/view/shell/desktop/SidebarTrendingTopics.tsx:59
|
||||
msgid "Trending"
|
||||
msgstr ""
|
||||
msgstr "Tendencias"
|
||||
|
||||
#: src/view/com/util/error/ErrorScreen.tsx:103
|
||||
msgctxt "action"
|
||||
@@ -8716,7 +8716,7 @@ msgstr "Deshacer repost"
|
||||
|
||||
#: src/view/com/util/post-ctrls/RepostButton.tsx:68
|
||||
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
|
||||
msgctxt "action"
|
||||
@@ -8742,7 +8742,7 @@ msgstr "No me gusta"
|
||||
|
||||
#: src/view/com/util/post-ctrls/PostCtrls.tsx:305
|
||||
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
|
||||
#~ msgid "Unlike this feed"
|
||||
@@ -8800,7 +8800,7 @@ msgstr "Desfijar"
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:519
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:526
|
||||
msgid "Unpin feed"
|
||||
msgstr ""
|
||||
msgstr "Desfijar feed"
|
||||
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:309
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:311
|
||||
@@ -8818,7 +8818,7 @@ msgstr "Desfijar lista de moderación"
|
||||
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:164
|
||||
msgid "Unpinned {0} from Home"
|
||||
msgstr ""
|
||||
msgstr "{0} desfijado de inicio"
|
||||
|
||||
#: src/view/screens/ProfileList.tsx:352
|
||||
msgid "Unpinned from your feeds"
|
||||
@@ -9284,7 +9284,7 @@ msgstr "No pudimos encontrar resultados para ese hashtag."
|
||||
|
||||
#: src/screens/Topic.tsx:178
|
||||
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
|
||||
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
|
||||
msgid "What people are posting about."
|
||||
msgstr ""
|
||||
msgstr "Los temas del momento."
|
||||
|
||||
#: src/view/com/auth/SplashScreen.tsx:38
|
||||
#: 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/view/shell/desktop/SidebarTrendingTopics.tsx:109
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
#~ msgid "Your default feed is \"Following\""
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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:173
|
||||
msgid "Close emoji picker"
|
||||
msgstr ""
|
||||
msgstr "Sulje emojinvalitsin"
|
||||
|
||||
#: src/components/dialogs/GifSelect.tsx:169
|
||||
msgid "Close GIF dialog"
|
||||
@@ -1823,7 +1823,7 @@ msgstr "Luotu {0}"
|
||||
|
||||
#: src/screens/List/ListHiddenScreen.tsx:127
|
||||
msgid "Creator has been blocked"
|
||||
msgstr ""
|
||||
msgstr "Tekijä on estetty"
|
||||
|
||||
#: src/screens/Onboarding/index.tsx:26
|
||||
#: src/screens/Onboarding/state.ts:99
|
||||
@@ -2321,7 +2321,7 @@ msgstr "Koulutus"
|
||||
|
||||
#: src/screens/List/ListHiddenScreen.tsx:141
|
||||
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/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:149
|
||||
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
|
||||
msgid "Invalid Verification Code"
|
||||
@@ -4549,7 +4549,7 @@ msgstr "Hups!"
|
||||
|
||||
#: src/screens/Onboarding/StepFinished.tsx:260
|
||||
msgid "Open"
|
||||
msgstr "Avaa"
|
||||
msgstr "Avoin"
|
||||
|
||||
#: src/view/com/posts/AviFollowButton.tsx:86
|
||||
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:1229
|
||||
msgid "Open emoji picker"
|
||||
msgstr "Avaa emoji-valitsin"
|
||||
msgstr "Avaa emojinvalitsin"
|
||||
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:190
|
||||
msgid "Open feed info screen"
|
||||
@@ -6283,7 +6283,7 @@ msgstr "Kirjaudutaanko ulos?"
|
||||
#: src/view/shell/NavSignupCard.tsx:47
|
||||
#: src/view/shell/NavSignupCard.tsx:52
|
||||
msgid "Create account"
|
||||
msgstr "Rekisteröidy"
|
||||
msgstr "Luo tili"
|
||||
|
||||
#: src/components/moderation/ScreenHider.tsx:91
|
||||
#: 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:174
|
||||
msgid "Tap to close the emoji picker"
|
||||
msgstr ""
|
||||
msgstr "Napauta sulkeaksesi emojinvalitsimen"
|
||||
|
||||
#: src/components/ProgressGuide/Toast.tsx:156
|
||||
msgid "Tap to dismiss"
|
||||
|
||||
+841
-624
File diff suppressed because it is too large
Load Diff
+780
-792
File diff suppressed because it is too large
Load Diff
+2279
-1603
File diff suppressed because it is too large
Load Diff
+871
-654
File diff suppressed because it is too large
Load Diff
+839
-622
File diff suppressed because it is too large
Load Diff
+850
-633
File diff suppressed because it is too large
Load Diff
+705
-755
File diff suppressed because it is too large
Load Diff
@@ -8062,7 +8062,7 @@ msgstr "Вы включили игнорирование этого пользо
|
||||
|
||||
#: src/screens/Messages/ChatList.tsx:190
|
||||
msgid "You have no conversations yet. Start one!"
|
||||
msgstr "У вас ещё нет бесед. Начните одну!"
|
||||
msgstr "У вас ещё нет бесед. Создайте первую!"
|
||||
|
||||
#: src/view/com/feeds/ProfileFeedgens.tsx:143
|
||||
msgid "You have no feeds."
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+1073
-2194
File diff suppressed because it is too large
Load Diff
@@ -8,14 +8,14 @@ msgstr ""
|
||||
"Language: vi\n"
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2024-11-20T08:45:59.857Z\n"
|
||||
"Last-Translator: vnphanquang\n"
|
||||
"PO-Revision-Date: 2025-01-22T08:45:59.857Z\n"
|
||||
"Last-Translator: vinhphm\n"
|
||||
"Language-Team: Translators in PR #2524, ManhNguyen98, vinhphm, vnphanquang\n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/components/ProgressGuide/FollowDialog.tsx:567
|
||||
msgid "(active)"
|
||||
msgstr ""
|
||||
msgstr "(hoạt động)"
|
||||
|
||||
#: src/screens/Messages/components/ChatListItem.tsx:130
|
||||
msgid "(contains embedded content)"
|
||||
@@ -32,11 +32,11 @@ msgstr "{0, plural, one {# ngày} other {# ngày}}"
|
||||
|
||||
#: src/screens/Profile/ProfileFollowers.tsx:40
|
||||
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
|
||||
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
|
||||
msgid "{0, plural, one {# hour} other {# hours}}"
|
||||
@@ -52,15 +52,15 @@ msgstr "{0, plural, one {# giờ} other {# giờ}}"
|
||||
|
||||
#: src/components/moderation/LabelsOnMe.tsx:53
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
msgid "{notificationCount} unread items"
|
||||
msgstr ""
|
||||
msgstr "{notificationCount} mục chưa đọc"
|
||||
|
||||
#: src/view/shell/Drawer.tsx:448
|
||||
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:921
|
||||
msgid "Add people"
|
||||
msgstr ""
|
||||
msgstr "Thêm người"
|
||||
|
||||
#: src/screens/Home/NoFeedsPinned.tsx:99
|
||||
msgid "Add recommended feeds"
|
||||
@@ -629,7 +629,7 @@ msgstr "Nâng cao"
|
||||
|
||||
#: src/view/screens/Notifications.tsx:86
|
||||
msgid "All"
|
||||
msgstr ""
|
||||
msgstr "Tất cả"
|
||||
|
||||
#: src/screens/StarterPack/StarterPackScreen.tsx:381
|
||||
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
|
||||
msgid "Allow quote posts"
|
||||
msgstr ""
|
||||
msgstr "Cho phép trích dẫn bài đăng"
|
||||
|
||||
#: src/components/dialogs/PostInteractionSettingsDialog.tsx:359
|
||||
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
|
||||
msgid "BETA"
|
||||
msgstr ""
|
||||
msgstr "Thử nghiệm"
|
||||
|
||||
#: src/components/dialogs/BirthDateSettings.tsx:106
|
||||
#: src/screens/Settings/AccountSettings.tsx:109
|
||||
@@ -1114,7 +1114,7 @@ msgstr ""
|
||||
|
||||
#: src/screens/Settings/AppIconSettings/index.tsx:102
|
||||
msgid "Bluesky+ icons"
|
||||
msgstr ""
|
||||
msgstr "Biểu tượng Bluesky+"
|
||||
|
||||
#: src/lib/moderation/useLabelBehaviorDescription.ts:53
|
||||
msgid "Blur images"
|
||||
@@ -1156,15 +1156,15 @@ msgstr "Xem các bảng tin khác"
|
||||
|
||||
#: src/components/TrendingTopics.tsx:176
|
||||
msgid "Browse posts about {displayName}"
|
||||
msgstr ""
|
||||
msgstr "Tìm bài đăng về {displayName}"
|
||||
|
||||
#: src/components/TrendingTopics.tsx:184
|
||||
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
|
||||
msgid "Browse topic {displayName}"
|
||||
msgstr ""
|
||||
msgstr "Tìm chủ đề {displayName}"
|
||||
|
||||
#: src/view/com/auth/SplashScreen.web.tsx:168
|
||||
msgid "Business"
|
||||
@@ -1184,7 +1184,7 @@ msgstr "Bởi {0}"
|
||||
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:447
|
||||
msgid "By <0>{0}</0>"
|
||||
msgstr ""
|
||||
msgstr "Bởi <0>{0}</0>"
|
||||
|
||||
#: src/screens/Signup/StepInfo/Policies.tsx:81
|
||||
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
|
||||
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: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/Search/Explore.tsx:411
|
||||
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
|
||||
msgid "Choose your password"
|
||||
@@ -1468,7 +1468,7 @@ msgstr "Khí hậu"
|
||||
|
||||
#: src/components/dms/ChatEmptyPill.tsx:39
|
||||
msgid "Clip 🐴 clop 🐴"
|
||||
msgstr "Cộp 🐴 cộp 🐴"
|
||||
msgstr "Lộp 🐴 cộp 🐴"
|
||||
|
||||
#: src/components/dialogs/GifSelect.tsx:281
|
||||
#: 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:173
|
||||
msgid "Close emoji picker"
|
||||
msgstr ""
|
||||
msgstr "Đóng bảng chọn emoji"
|
||||
|
||||
#: src/components/dialogs/GifSelect.tsx:169
|
||||
msgid "Close GIF dialog"
|
||||
@@ -1657,7 +1657,7 @@ msgstr "Liên hệ hỗ trợ"
|
||||
|
||||
#: src/screens/Settings/ContentAndMediaSettings.tsx:49
|
||||
msgid "Content & Media"
|
||||
msgstr ""
|
||||
msgstr "Nội dung & phương tiện"
|
||||
|
||||
#: src/screens/Settings/AccessibilitySettings.tsx:109
|
||||
#: src/screens/Settings/Settings.tsx:175
|
||||
@@ -1890,7 +1890,7 @@ msgstr "Đã tạo {0}"
|
||||
|
||||
#: src/screens/List/ListHiddenScreen.tsx:127
|
||||
msgid "Creator has been blocked"
|
||||
msgstr ""
|
||||
msgstr "Người tạo đã bị chặn"
|
||||
|
||||
#: src/screens/Onboarding/index.tsx:26
|
||||
#: src/screens/Onboarding/state.ts:99
|
||||
@@ -1948,7 +1948,7 @@ msgstr "Mặc định"
|
||||
|
||||
#: src/screens/Settings/AppIconSettings/index.tsx:75
|
||||
msgid "Default icons"
|
||||
msgstr ""
|
||||
msgstr "Biểu tượng mặc định"
|
||||
|
||||
#: src/components/dms/MessageMenu.tsx:151
|
||||
#: 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
|
||||
msgid "Duration:"
|
||||
msgstr "Thời gian:"
|
||||
msgstr "Thời hạn:"
|
||||
|
||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:213
|
||||
msgid "e.g. alice"
|
||||
@@ -2393,7 +2393,7 @@ msgstr "Giáo dục"
|
||||
|
||||
#: src/screens/List/ListHiddenScreen.tsx:141
|
||||
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/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:136
|
||||
msgid "Enable trending topics"
|
||||
msgstr ""
|
||||
msgstr "Mở chủ đề nổi trội"
|
||||
|
||||
#: src/screens/Messages/Settings.tsx:130
|
||||
#: src/screens/Messages/Settings.tsx:133
|
||||
@@ -2808,7 +2808,7 @@ msgstr "Bảng tin bởi {0}"
|
||||
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:353
|
||||
msgid "Feed menu"
|
||||
msgstr ""
|
||||
msgstr "Menu bảng tin"
|
||||
|
||||
#: src/components/StarterPack/Wizard/WizardListCard.tsx:54
|
||||
msgid "Feed toggle"
|
||||
@@ -2847,7 +2847,7 @@ msgstr "Đã cập nhật bảng tin"
|
||||
|
||||
#: src/screens/Search/components/ExploreRecommendations.tsx:54
|
||||
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
|
||||
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:417
|
||||
msgid "Find people to follow"
|
||||
msgstr ""
|
||||
msgstr "Tìm người để theo dõi"
|
||||
|
||||
#: src/view/screens/Search/Search.tsx:595
|
||||
msgid "Find posts and users on Bluesky"
|
||||
@@ -2915,7 +2915,7 @@ msgstr "Theo dõi {name}"
|
||||
#: src/components/ProgressGuide/List.tsx:52
|
||||
#: src/state/shell/progress-guide.tsx:229
|
||||
msgid "Follow 10 accounts"
|
||||
msgstr ""
|
||||
msgstr "Theo dõi 10 tài khoản"
|
||||
|
||||
#: src/components/ProgressGuide/List.tsx:69
|
||||
msgid "Follow 7 accounts"
|
||||
@@ -3289,13 +3289,13 @@ msgstr "Ẩn trả lời này?"
|
||||
#: src/screens/Search/components/ExploreTrendingTopics.tsx:83
|
||||
#: src/view/shell/desktop/SidebarTrendingTopics.tsx:62
|
||||
msgid "Hide trending topics"
|
||||
msgstr ""
|
||||
msgstr "Ẩn chủ đề nổi trội"
|
||||
|
||||
#: src/components/interstitials/Trending.tsx:99
|
||||
#: src/screens/Search/components/ExploreTrendingTopics.tsx:135
|
||||
#: src/view/shell/desktop/SidebarTrendingTopics.tsx:108
|
||||
msgid "Hide trending topics?"
|
||||
msgstr ""
|
||||
msgstr "Ẩn chủ đề nổi trội?"
|
||||
|
||||
#: src/view/com/notifications/NotificationFeedItem.tsx:592
|
||||
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:657
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
msgid "Like"
|
||||
msgstr ""
|
||||
msgstr "Thích"
|
||||
|
||||
#: src/view/com/util/post-ctrls/PostCtrls.tsx:311
|
||||
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
|
||||
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
|
||||
msgid "Like feed"
|
||||
msgstr ""
|
||||
msgstr "Thích bảng tin"
|
||||
|
||||
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:275
|
||||
msgid "Like this feed"
|
||||
@@ -3741,14 +3741,14 @@ msgstr "Thích bởi"
|
||||
#: src/components/FeedCard.tsx:213
|
||||
#: src/view/com/feeds/FeedSourceCard.tsx:303
|
||||
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/screens/Profile/components/ProfileFeedHeader.tsx:489
|
||||
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:295
|
||||
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:309
|
||||
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
|
||||
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:631
|
||||
msgid "Linear"
|
||||
msgstr ""
|
||||
msgstr "Ngang hàng"
|
||||
|
||||
#: src/Navigation.tsx:198
|
||||
msgid "List"
|
||||
@@ -3782,11 +3782,11 @@ msgstr "Danh sách bởi {0}"
|
||||
|
||||
#: src/view/com/profile/ProfileSubpageHeader.tsx:156
|
||||
msgid "List by <0/>"
|
||||
msgstr ""
|
||||
msgstr "Danh sách tạo bởi <0/>"
|
||||
|
||||
#: src/view/com/profile/ProfileSubpageHeader.tsx:154
|
||||
msgid "List by you"
|
||||
msgstr ""
|
||||
msgstr "Danh sách của bạn"
|
||||
|
||||
#: src/view/screens/ProfileList.tsx:455
|
||||
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
|
||||
msgid "Logo by @sawaratsuki.bsky.social"
|
||||
msgstr ""
|
||||
msgstr "Logo bởi @sawaratsuki.bsky.social"
|
||||
|
||||
#: src/view/shell/desktop/RightNav.tsx:103
|
||||
#~ msgid "Logo by <0/>"
|
||||
@@ -3953,7 +3953,7 @@ msgstr "Người dùng được đề cập"
|
||||
|
||||
#: src/view/screens/Notifications.tsx:99
|
||||
msgid "Mentions"
|
||||
msgstr ""
|
||||
msgstr "Nhắc đến"
|
||||
|
||||
#: src/components/Menu/index.tsx:96
|
||||
#: 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/ModerationModlists.tsx:66
|
||||
msgid "New list"
|
||||
msgstr ""
|
||||
msgstr "Danh sách mới"
|
||||
|
||||
#: src/components/dms/NewMessagesPill.tsx:92
|
||||
msgid "New messages"
|
||||
@@ -4668,7 +4668,7 @@ msgstr "Mở tùy chọn hội thoại"
|
||||
|
||||
#: src/components/Layout/Header/index.tsx:145
|
||||
msgid "Open drawer menu"
|
||||
msgstr ""
|
||||
msgstr "Mở trình đơn"
|
||||
|
||||
#: src/screens/Messages/components/MessageInput.web.tsx:181
|
||||
#: 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
|
||||
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: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:526
|
||||
msgid "Pin feed"
|
||||
msgstr ""
|
||||
msgstr "Ghim bảng tin"
|
||||
|
||||
#: src/view/screens/ProfileList.tsx:685
|
||||
msgid "Pin to home"
|
||||
@@ -4934,7 +4934,7 @@ msgstr "Đã ghim"
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:162
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:174
|
||||
msgid "Pinned {0} to Home"
|
||||
msgstr ""
|
||||
msgstr "Đã ghim {0} vào trang chủ"
|
||||
|
||||
#: src/view/screens/SavedFeeds.tsx:123
|
||||
msgid "Pinned Feeds"
|
||||
@@ -5226,11 +5226,11 @@ msgstr "Công khai"
|
||||
|
||||
#: src/view/com/lists/MyLists.tsx:77
|
||||
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
|
||||
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
|
||||
#~ 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
|
||||
msgid "Recommended"
|
||||
msgstr ""
|
||||
msgstr "Đề xuất"
|
||||
|
||||
#: src/screens/Messages/components/MessageListError.tsx:20
|
||||
msgid "Reconnect"
|
||||
@@ -5506,7 +5506,7 @@ msgstr "Trả lời"
|
||||
|
||||
#: src/view/com/util/post-ctrls/PostCtrls.tsx:263
|
||||
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/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
|
||||
msgid "Reply sorting"
|
||||
msgstr ""
|
||||
msgstr "Sắp xếp trả lời"
|
||||
|
||||
#: src/view/com/post/Post.tsx:204
|
||||
#: src/view/com/posts/PostFeedItem.tsx:553
|
||||
@@ -5647,7 +5647,7 @@ msgstr "Đăng lại"
|
||||
|
||||
#: src/view/com/util/post-ctrls/RepostButton.tsx:74
|
||||
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/view/com/util/post-ctrls/RepostButton.tsx:148
|
||||
@@ -5890,15 +5890,15 @@ msgstr "Tìm kiếm"
|
||||
|
||||
#: src/components/ProgressGuide/FollowDialog.tsx:745
|
||||
msgid "Search by name or interest"
|
||||
msgstr ""
|
||||
msgstr "Tìm theo tên hoặc sở thích"
|
||||
|
||||
#: src/view/screens/Feeds.tsx:441
|
||||
msgid "Search feeds"
|
||||
msgstr ""
|
||||
msgstr "Tìm bảng tin"
|
||||
|
||||
#: src/components/ProgressGuide/FollowDialog.tsx:575
|
||||
msgid "Search for \"{interestsDisplayName}\"{activeText}"
|
||||
msgstr ""
|
||||
msgstr "Tìm kiếm \"{interestsDisplayName}\"{activeText}"
|
||||
|
||||
#: src/view/shell/desktop/Search.tsx:201
|
||||
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
|
||||
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
|
||||
msgid "Select a color"
|
||||
@@ -6125,7 +6125,7 @@ msgstr "Địa chỉ máy chủ"
|
||||
|
||||
#: src/screens/Settings/AppIconSettings/index.tsx:178
|
||||
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
|
||||
msgid "Set birthdate"
|
||||
@@ -6218,7 +6218,7 @@ msgstr "Chia sẻ mã QR"
|
||||
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:470
|
||||
msgid "Share this feed"
|
||||
msgstr ""
|
||||
msgstr "Chia sẻ bảng tin này"
|
||||
|
||||
#: src/screens/StarterPack/StarterPackScreen.tsx:416
|
||||
msgid "Share this starter pack"
|
||||
@@ -6313,11 +6313,11 @@ msgstr "Hiển thị trả lời"
|
||||
|
||||
#: src/view/com/post-thread/PostThread.tsx:622
|
||||
msgid "Show replies as"
|
||||
msgstr ""
|
||||
msgstr "Hiển thị trả lời dưới dạng"
|
||||
|
||||
#: src/screens/Settings/ThreadPreferences.tsx:151
|
||||
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
|
||||
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
|
||||
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.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:938
|
||||
msgid "Start adding people"
|
||||
msgstr ""
|
||||
msgstr "Bắt đầu thêm người"
|
||||
|
||||
#: src/view/screens/ProfileList.tsx:824
|
||||
#: src/view/screens/ProfileList.tsx:945
|
||||
msgid "Start adding people!"
|
||||
msgstr ""
|
||||
msgstr "Bắt đầu thêm người!"
|
||||
|
||||
#: src/components/dms/dialogs/SearchablePeopleList.tsx:353
|
||||
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
|
||||
msgid "Starter pack by <0/>"
|
||||
msgstr ""
|
||||
msgstr "Gói khởi đầu bởi <0/>"
|
||||
|
||||
#: src/components/StarterPack/StarterPackCard.tsx:80
|
||||
#: 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:174
|
||||
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
|
||||
msgid "Tap to dismiss"
|
||||
@@ -6695,7 +6695,7 @@ msgstr "Nhấn để xem ảnh đầy đủ"
|
||||
|
||||
#: src/state/shell/progress-guide.tsx:233
|
||||
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
|
||||
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
|
||||
msgid "This list is empty."
|
||||
msgstr ""
|
||||
msgstr "Danh sách này đang trống."
|
||||
|
||||
#: src/screens/Profile/ErrorState.tsx:40
|
||||
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:609
|
||||
msgid "Thread options"
|
||||
msgstr ""
|
||||
msgstr "Tùy chọn thảo luận"
|
||||
|
||||
#: src/screens/Settings/ContentAndMediaSettings.tsx:66
|
||||
#: 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:641
|
||||
msgid "Threaded"
|
||||
msgstr ""
|
||||
msgstr "Phân luồng"
|
||||
|
||||
#: src/screens/Settings/ThreadPreferences.tsx:142
|
||||
msgid "Threaded mode"
|
||||
@@ -7254,7 +7254,7 @@ msgstr "Hàng đầu"
|
||||
|
||||
#: src/Navigation.tsx:383
|
||||
msgid "Topic"
|
||||
msgstr ""
|
||||
msgstr "Chủ đề"
|
||||
|
||||
#: src/components/dms/MessageMenu.tsx:103
|
||||
#: src/components/dms/MessageMenu.tsx:105
|
||||
@@ -7268,7 +7268,7 @@ msgstr "Dịch"
|
||||
#: src/screens/Search/components/ExploreTrendingTopics.tsx:69
|
||||
#: src/view/shell/desktop/SidebarTrendingTopics.tsx:59
|
||||
msgid "Trending"
|
||||
msgstr ""
|
||||
msgstr "Chủ đề nổi trội"
|
||||
|
||||
#: src/view/com/util/error/ErrorScreen.tsx:103
|
||||
msgctxt "action"
|
||||
@@ -7293,7 +7293,7 @@ msgstr "Gõ tin nhắn của bạn ở đây"
|
||||
|
||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:415
|
||||
msgid "Type:"
|
||||
msgstr "Type:"
|
||||
msgstr "Loại:"
|
||||
|
||||
#: src/view/screens/ProfileList.tsx:590
|
||||
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
|
||||
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
|
||||
msgctxt "action"
|
||||
@@ -7376,11 +7376,11 @@ msgstr "Bỏ theo dõi tài khoản"
|
||||
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:514
|
||||
msgid "Unlike"
|
||||
msgstr ""
|
||||
msgstr "Bỏ thích"
|
||||
|
||||
#: src/view/com/util/post-ctrls/PostCtrls.tsx:305
|
||||
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
|
||||
#~ msgid "Unlike this feed"
|
||||
@@ -7430,7 +7430,7 @@ msgstr "Bỏ ghim"
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:519
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:526
|
||||
msgid "Unpin feed"
|
||||
msgstr ""
|
||||
msgstr "Bỏ ghim bảng tin"
|
||||
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:309
|
||||
#: 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
|
||||
msgid "Unpinned {0} from Home"
|
||||
msgstr ""
|
||||
msgstr "Đã bỏ ghim {0} khỏi Trang chủ"
|
||||
|
||||
#: src/view/screens/ProfileList.tsx:352
|
||||
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
|
||||
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
|
||||
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
|
||||
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.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/view/shell/desktop/SidebarTrendingTopics.tsx:109
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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/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
@@ -1,4 +1,4 @@
|
||||
import {GestureResponderEvent, Linking} from 'react-native'
|
||||
import {Linking} from 'react-native'
|
||||
|
||||
import {isNative, isWeb} from './detection'
|
||||
|
||||
@@ -24,15 +24,3 @@ export function clearHash() {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -64,4 +64,5 @@ export const router = new Router({
|
||||
StarterPack: '/starter-pack/:name/:rkey',
|
||||
StarterPackShort: '/starter-pack-short/:code',
|
||||
StarterPackWizard: '/starter-pack/create',
|
||||
VideoFeed: '/video-feed',
|
||||
})
|
||||
|
||||
@@ -14,6 +14,7 @@ import {logger} from '#/logger'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {MESSAGE_SCREEN_POLL_INTERVAL} from '#/state/messages/convo/const'
|
||||
import {useMessagesEventBus} from '#/state/messages/events'
|
||||
import {useLeftConvos} from '#/state/queries/messages/leave-conversation'
|
||||
import {useListConvosQuery} from '#/state/queries/messages/list-conversations'
|
||||
import {List} from '#/view/com/util/List'
|
||||
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
||||
@@ -94,12 +95,19 @@ export function MessagesScreen({navigation, route}: Props) {
|
||||
|
||||
useRefreshOnFocus(refetch)
|
||||
|
||||
const leftConvos = useLeftConvos()
|
||||
|
||||
const conversations = useMemo(() => {
|
||||
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 []
|
||||
}, [data])
|
||||
}, [data, leftConvos])
|
||||
|
||||
const onRefresh = useCallback(async () => {
|
||||
setIsPTRing(true)
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
BSKY_APP_ACCOUNT_DID,
|
||||
DISCOVER_SAVED_FEED,
|
||||
TIMELINE_SAVED_FEED,
|
||||
VIDEO_SAVED_FEED,
|
||||
} from '#/lib/constants'
|
||||
import {useRequestNotificationsPermission} from '#/lib/notifications/notifications'
|
||||
import {logEvent, useGate} from '#/lib/statsig/statsig'
|
||||
@@ -111,6 +112,12 @@ export function StepFinished() {
|
||||
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
|
||||
if (starterPack && starterPack.feeds?.length) {
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import React, {useCallback, useMemo} from 'react'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import {useAnimatedRef} from 'react-native-reanimated'
|
||||
import {AppBskyFeedDefs} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useIsFocused, useNavigation} from '@react-navigation/native'
|
||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {VIDEO_FEED_URIS} from '#/lib/constants'
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {useSetTitle} from '#/lib/hooks/useSetTitle'
|
||||
import {ComposeIcon2} from '#/lib/icons'
|
||||
@@ -18,7 +20,7 @@ import {isNative} from '#/platform/detection'
|
||||
import {listenSoftReset} from '#/state/events'
|
||||
import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
|
||||
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 {
|
||||
usePreferencesQuery,
|
||||
@@ -46,6 +48,11 @@ type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFeed'>
|
||||
export function ProfileFeedScreen(props: Props) {
|
||||
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 {_} = useLingui()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
@@ -96,7 +103,10 @@ export function ProfileFeedScreen(props: Props) {
|
||||
|
||||
return resolvedUri ? (
|
||||
<Layout.Screen>
|
||||
<ProfileFeedScreenIntermediate feedUri={resolvedUri.uri} />
|
||||
<ProfileFeedScreenIntermediate
|
||||
feedUri={resolvedUri.uri}
|
||||
feedParams={feedParams}
|
||||
/>
|
||||
</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: info} = useFeedSourceInfoQuery({uri: feedUri})
|
||||
|
||||
@@ -125,15 +141,18 @@ function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) {
|
||||
<ProfileFeedScreenInner
|
||||
preferences={preferences}
|
||||
feedInfo={info as FeedSourceFeedInfo}
|
||||
feedParams={feedParams}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function ProfileFeedScreenInner({
|
||||
feedInfo,
|
||||
feedParams,
|
||||
}: {
|
||||
preferences: UsePreferencesQueryResponse
|
||||
feedInfo: FeedSourceFeedInfo
|
||||
feedParams: FeedParams | undefined
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const {hasSession} = useSession()
|
||||
@@ -170,6 +189,14 @@ export function ProfileFeedScreenInner({
|
||||
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 (
|
||||
<>
|
||||
<ProfileFeedHeader info={feedInfo} />
|
||||
@@ -177,12 +204,14 @@ export function ProfileFeedScreenInner({
|
||||
<FeedFeedbackProvider value={feedFeedback}>
|
||||
<PostFeed
|
||||
feed={feed}
|
||||
feedParams={feedParams}
|
||||
pollInterval={60e3}
|
||||
disablePoll={hasNew}
|
||||
onHasNew={setHasNew}
|
||||
scrollElRef={scrollElRef}
|
||||
onScrolledDownChange={setIsScrolledDown}
|
||||
renderEmptyState={renderPostsEmpty}
|
||||
isVideoFeed={isVideoFeed}
|
||||
/>
|
||||
</FeedFeedbackProvider>
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ export const ProfileFeedSection = React.forwardRef<
|
||||
const [hasNew, setHasNew] = React.useState(false)
|
||||
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
|
||||
const shouldUseAdjustedNumToRender = feed.endsWith('posts_and_author_threads')
|
||||
const isVideoFeed = isNative && feed.endsWith('posts_with_video')
|
||||
const adjustedInitialNumToRender = useInitialNumToRender({
|
||||
screenHeightOffset: headerHeight,
|
||||
})
|
||||
@@ -84,11 +85,12 @@ export const ProfileFeedSection = React.forwardRef<
|
||||
renderEmptyState={renderPostsEmpty}
|
||||
headerOffset={headerHeight}
|
||||
progressViewOffset={ios(0)}
|
||||
renderEndOfFeed={ProfileEndOfFeed}
|
||||
renderEndOfFeed={isVideoFeed ? undefined : ProfileEndOfFeed}
|
||||
ignoreFilterFor={ignoreFilterFor}
|
||||
initialNumToRender={
|
||||
shouldUseAdjustedNumToRender ? adjustedInitialNumToRender : undefined
|
||||
}
|
||||
isVideoFeed={isVideoFeed}
|
||||
/>
|
||||
{(isScrolledDown || hasNew) && (
|
||||
<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 setUseInAppBrowser = useSetInAppBrowser()
|
||||
const {enabled: trendingEnabled} = useTrendingConfig()
|
||||
const {trendingDisabled} = useTrendingSettings()
|
||||
const {setTrendingDisabled} = useTrendingSettingsApi()
|
||||
const {trendingDisabled, trendingVideoDisabled} = useTrendingSettings()
|
||||
const {setTrendingDisabled, setTrendingVideoDisabled} =
|
||||
useTrendingSettingsApi()
|
||||
|
||||
return (
|
||||
<Layout.Screen>
|
||||
@@ -138,6 +139,27 @@ export function ContentAndMediaSettingsScreen({}: Props) {
|
||||
<Toggle.Platform />
|
||||
</SettingsList.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>
|
||||
|
||||
@@ -80,9 +80,8 @@ export function StepCaptcha() {
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.pb_xl,
|
||||
a.overflow_hidden,
|
||||
{minHeight: 500},
|
||||
{minHeight: 510},
|
||||
completed && [a.align_center, a.justify_center],
|
||||
]}>
|
||||
{!completed ? (
|
||||
|
||||
@@ -85,6 +85,21 @@ export function SignupQueued() {
|
||||
</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 (
|
||||
<Modal
|
||||
visible
|
||||
@@ -108,7 +123,7 @@ export function SignupQueued() {
|
||||
<Logo width={120} />
|
||||
</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>
|
||||
</Text>
|
||||
<P style={[t.atoms.text_contrast_medium]}>
|
||||
@@ -153,7 +168,7 @@ export function SignupQueued() {
|
||||
</P>
|
||||
</View>
|
||||
|
||||
{isWeb && gtMobile && (
|
||||
{webLayout && (
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
@@ -162,15 +177,7 @@ export function SignupQueued() {
|
||||
a.pt_5xl,
|
||||
{paddingBottom: 200},
|
||||
]}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="large"
|
||||
label={_(msg`Log out`)}
|
||||
onPress={() => logoutCurrentAccount('SignupQueued')}>
|
||||
<ButtonText style={[{color: t.palette.primary_500}]}>
|
||||
<Trans>Log out</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
{logoutBtn}
|
||||
{checkBtn}
|
||||
</View>
|
||||
)}
|
||||
@@ -178,27 +185,17 @@ export function SignupQueued() {
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{(!isWeb || !gtMobile) && (
|
||||
{!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),
|
||||
},
|
||||
{paddingBottom: Math.max(insets.bottom, a.pb_5xl.paddingBottom)},
|
||||
]}>
|
||||
<View style={[a.w_full, a.gap_sm, {maxWidth: COL_WIDTH}]}>
|
||||
{checkBtn}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="large"
|
||||
label={_(msg`Log out`)}
|
||||
onPress={() => logoutCurrentAccount('SignupQueued')}>
|
||||
<ButtonText style={[{color: t.palette.primary_500}]}>
|
||||
<Trans>Log out</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
{logoutBtn}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
@@ -0,0 +1,3 @@
|
||||
export function VideoScreen() {
|
||||
return null
|
||||
}
|
||||
@@ -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
@@ -3,11 +3,11 @@ import {AppState, AppStateStatus} from 'react-native'
|
||||
import {AppBskyFeedDefs} from '@atproto/api'
|
||||
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 {logger} from '#/logger'
|
||||
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'
|
||||
|
||||
type StateContext = {
|
||||
@@ -25,6 +25,7 @@ const stateContext = React.createContext<StateContext>({
|
||||
export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
|
||||
const agent = useAgent()
|
||||
const enabled = isDiscoverFeed(feed) && hasSession
|
||||
|
||||
const queue = React.useRef<Set<string>>(new Set())
|
||||
const history = React.useRef<
|
||||
// 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)
|
||||
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
|
||||
agent.app.bsky.feed
|
||||
.sendInteractions(
|
||||
@@ -54,7 +60,7 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
|
||||
encoding: 'application/json',
|
||||
headers: {
|
||||
// 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)
|
||||
throttledFlushAggregatedStats()
|
||||
}, [agent, throttledFlushAggregatedStats])
|
||||
}, [agent, throttledFlushAggregatedStats, feed])
|
||||
|
||||
const sendToFeed = React.useMemo(
|
||||
() =>
|
||||
@@ -96,18 +102,15 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
|
||||
if (!enabled) {
|
||||
return
|
||||
}
|
||||
const slice = getFeedPostSlice(feedItem)
|
||||
if (slice === null) {
|
||||
return
|
||||
}
|
||||
for (const postItem of slice.items) {
|
||||
const items = getItemsForFeedback(feedItem)
|
||||
for (const {item: postItem, feedContext} of items) {
|
||||
if (!history.current.has(postItem)) {
|
||||
history.current.add(postItem)
|
||||
queue.current.add(
|
||||
toString({
|
||||
item: postItem.uri,
|
||||
event: 'app.bsky.feed.defs#interactionSeen',
|
||||
feedContext: slice.feedContext,
|
||||
feedContext,
|
||||
}),
|
||||
)
|
||||
sendToFeed()
|
||||
@@ -155,7 +158,7 @@ export function useFeedFeedbackContext() {
|
||||
// place, we're hardcoding it to the discover feed.
|
||||
// -prf
|
||||
function isDiscoverFeed(feed: FeedDescriptor) {
|
||||
return feed === `feedgen|${PROD_DEFAULT_FEED('whats-hot')}`
|
||||
return FEEDBACK_FEEDS.includes(feed)
|
||||
}
|
||||
|
||||
function toString(interaction: AppBskyFeedDefs.Interaction): string {
|
||||
|
||||
@@ -126,6 +126,7 @@ const schema = z.object({
|
||||
/** @deprecated */
|
||||
mutedThreads: z.array(z.string()),
|
||||
trendingDisabled: z.boolean().optional(),
|
||||
trendingVideoDisabled: z.boolean().optional(),
|
||||
})
|
||||
export type Schema = z.infer<typeof schema>
|
||||
|
||||
@@ -172,6 +173,7 @@ export const defaults: Schema = {
|
||||
hasCheckedForStarterPack: false,
|
||||
subtitlesEnabled: true,
|
||||
trendingDisabled: false,
|
||||
trendingVideoDisabled: false,
|
||||
}
|
||||
|
||||
export function tryParse(rawData: string): Schema | undefined {
|
||||
|
||||
@@ -4,18 +4,27 @@ import * as persisted from '#/state/persisted'
|
||||
|
||||
type StateContext = {
|
||||
trendingDisabled: Exclude<persisted.Schema['trendingDisabled'], undefined>
|
||||
trendingVideoDisabled: Exclude<
|
||||
persisted.Schema['trendingVideoDisabled'],
|
||||
undefined
|
||||
>
|
||||
}
|
||||
type ApiContext = {
|
||||
setTrendingDisabled(
|
||||
hidden: Exclude<persisted.Schema['trendingDisabled'], undefined>,
|
||||
): void
|
||||
setTrendingVideoDisabled(
|
||||
hidden: Exclude<persisted.Schema['trendingVideoDisabled'], undefined>,
|
||||
): void
|
||||
}
|
||||
|
||||
const StateContext = React.createContext<StateContext>({
|
||||
trendingDisabled: Boolean(persisted.defaults.trendingDisabled),
|
||||
trendingVideoDisabled: Boolean(persisted.defaults.trendingVideoDisabled),
|
||||
})
|
||||
const ApiContext = React.createContext<ApiContext>({
|
||||
setTrendingDisabled() {},
|
||||
setTrendingVideoDisabled() {},
|
||||
})
|
||||
|
||||
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<{}>) {
|
||||
const [trendingDisabled, setTrendingDisabled] =
|
||||
usePersistedBooleanValue('trendingDisabled')
|
||||
const [trendingVideoDisabled, setTrendingVideoDisabled] =
|
||||
usePersistedBooleanValue('trendingVideoDisabled')
|
||||
|
||||
/*
|
||||
* Context
|
||||
*/
|
||||
const state = React.useMemo(() => ({trendingDisabled}), [trendingDisabled])
|
||||
const state = React.useMemo(
|
||||
() => ({trendingDisabled, trendingVideoDisabled}),
|
||||
[trendingDisabled, trendingVideoDisabled],
|
||||
)
|
||||
const api = React.useMemo(
|
||||
() => ({setTrendingDisabled}),
|
||||
[setTrendingDisabled],
|
||||
() => ({setTrendingDisabled, setTrendingVideoDisabled}),
|
||||
[setTrendingDisabled, setTrendingVideoDisabled],
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
@@ -48,6 +48,7 @@ export type FeedSourceFeedInfo = {
|
||||
creatorHandle: string
|
||||
likeCount: number | undefined
|
||||
likeUri: string | undefined
|
||||
contentMode: AppBskyFeedDefs.GeneratorView['contentMode']
|
||||
}
|
||||
|
||||
export type FeedSourceListInfo = {
|
||||
@@ -65,6 +66,7 @@ export type FeedSourceListInfo = {
|
||||
description: RichText
|
||||
creatorDid: string
|
||||
creatorHandle: string
|
||||
contentMode: undefined
|
||||
}
|
||||
|
||||
export type FeedSourceInfo = FeedSourceFeedInfo | FeedSourceListInfo
|
||||
@@ -111,6 +113,7 @@ export function hydrateFeedGenerator(
|
||||
creatorHandle: view.creator.handle,
|
||||
likeCount: view.likeCount,
|
||||
likeUri: view.viewer?.like,
|
||||
contentMode: view.contentMode,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +144,7 @@ export function hydrateList(view: AppBskyGraphDefs.ListView): FeedSourceInfo {
|
||||
displayName: view.name
|
||||
? sanitizeDisplayName(view.name)
|
||||
: `User List by ${sanitizeHandle(view.creator.handle, '@')}`,
|
||||
contentMode: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,6 +403,7 @@ const PWI_DISCOVER_FEED_STUB: SavedFeedSourceInfo = {
|
||||
id: 'pwi-discover',
|
||||
...DISCOVER_SAVED_FEED,
|
||||
},
|
||||
contentMode: undefined,
|
||||
}
|
||||
|
||||
const pinnedFeedInfosQueryKeyRoot = 'pinnedFeedsInfos'
|
||||
@@ -485,6 +490,7 @@ export function usePinnedFeedsInfos() {
|
||||
likeCount: 0,
|
||||
likeUri: '',
|
||||
savedFeed: pinnedItem,
|
||||
contentMode: undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
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 {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
|
||||
import {useAgent} from '#/state/session'
|
||||
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(
|
||||
convoId: string | undefined,
|
||||
{
|
||||
onSuccess,
|
||||
onMutate,
|
||||
onError,
|
||||
}: {
|
||||
onMutate?: () => void
|
||||
onSuccess?: (data: ChatBskyConvoLeaveConvo.OutputSchema) => void
|
||||
onError?: (error: Error) => void
|
||||
},
|
||||
@@ -20,6 +31,7 @@ export function useLeaveConvo(
|
||||
const agent = useAgent()
|
||||
|
||||
return useMutation({
|
||||
mutationKey: RQKEY(convoId),
|
||||
mutationFn: async () => {
|
||||
if (!convoId) throw new Error('No convoId provided')
|
||||
|
||||
@@ -51,6 +63,7 @@ export function useLeaveConvo(
|
||||
}
|
||||
},
|
||||
)
|
||||
onMutate?.()
|
||||
return {prevPages}
|
||||
},
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -44,11 +44,12 @@ import {
|
||||
} from './util'
|
||||
|
||||
type ActorDid = string
|
||||
type AuthorFilter =
|
||||
export type AuthorFilter =
|
||||
| 'posts_with_replies'
|
||||
| 'posts_no_replies'
|
||||
| 'posts_and_author_threads'
|
||||
| 'posts_with_media'
|
||||
| 'posts_with_video'
|
||||
type FeedUri = string
|
||||
type ListUri = string
|
||||
|
||||
@@ -61,6 +62,7 @@ export type FeedDescriptor =
|
||||
export interface FeedParams {
|
||||
mergeFeedEnabled?: boolean
|
||||
mergeFeedSources?: string[]
|
||||
feedCacheKey?: 'discover' | 'explore' | undefined
|
||||
}
|
||||
|
||||
type RQPageParam = {cursor: string | undefined; api: FeedAPI} | undefined
|
||||
@@ -88,6 +90,7 @@ export interface FeedPostSlice {
|
||||
isIncompleteThread: boolean
|
||||
isFallbackMarker: boolean
|
||||
feedContext: string | undefined
|
||||
feedPostUri: string
|
||||
reason?:
|
||||
| AppBskyFeedDefs.ReasonRepost
|
||||
| AppBskyFeedDefs.ReasonPin
|
||||
@@ -329,6 +332,7 @@ export function usePostFeedQuery(
|
||||
isFallbackMarker: slice.isFallbackMarker,
|
||||
feedContext: slice.feedContext,
|
||||
reason: slice.reason,
|
||||
feedPostUri: slice.feedPostUri,
|
||||
items: slice.items.map((item, i) => {
|
||||
const feedPostSliceItem: FeedPostSliceItem = {
|
||||
_reactKey: `${slice._reactKey}-${i}-${item.post.uri}`,
|
||||
|
||||
@@ -53,8 +53,9 @@ export type ThreadPost = {
|
||||
uri: string
|
||||
post: AppBskyFeedDefs.PostView
|
||||
record: AppBskyFeedPost.Record
|
||||
parent?: ThreadNode
|
||||
replies?: ThreadNode[]
|
||||
parent: ThreadNode | undefined
|
||||
replies: ThreadNode[] | undefined
|
||||
hasOPLike: boolean | undefined
|
||||
ctx: ThreadCtx
|
||||
}
|
||||
|
||||
@@ -255,8 +256,8 @@ export function sortThread(
|
||||
if (aFetchedAt !== bFetchedAt) {
|
||||
return aFetchedAt - bFetchedAt // older fetches first
|
||||
} else if (opts.sort === 'hotness') {
|
||||
const aHotness = getHotness(a.post, aFetchedAt)
|
||||
const bHotness = getHotness(b.post, bFetchedAt /* same as aFetchedAt */)
|
||||
const aHotness = getHotness(a, aFetchedAt)
|
||||
const bHotness = getHotness(b, bFetchedAt /* same as aFetchedAt */)
|
||||
return bHotness - aHotness
|
||||
} else if (opts.sort === 'oldest') {
|
||||
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)
|
||||
// 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.
|
||||
function getHotness(post: AppBskyFeedDefs.PostView, fetchedAt: number) {
|
||||
function getHotness(threadPost: ThreadPost, fetchedAt: number) {
|
||||
const {post, hasOPLike} = threadPost
|
||||
const hoursAgo = Math.max(
|
||||
0,
|
||||
(new Date(fetchedAt).getTime() - new Date(post.indexedAt).getTime()) /
|
||||
(1000 * 60 * 60),
|
||||
)
|
||||
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 timePenalty = Math.pow(hoursAgo + 2, timePenaltyExponent)
|
||||
const opLikeBoost = hasOPLike ? 0.8 : 1.0
|
||||
const timePenalty = Math.pow(hoursAgo + 2, timePenaltyExponent * opLikeBoost)
|
||||
return likeOrder / timePenalty
|
||||
}
|
||||
|
||||
@@ -356,6 +359,7 @@ function responseToThreadNodes(
|
||||
// do not show blocked posts in replies
|
||||
.filter(node => node.type !== 'blocked')
|
||||
: undefined,
|
||||
hasOPLike: Boolean(node?.threadContext?.rootAuthorLike),
|
||||
ctx: {
|
||||
depth,
|
||||
isHighlightedPost: depth === 0,
|
||||
@@ -552,6 +556,7 @@ function threadNodeToPlaceholderThread(
|
||||
record: node.record,
|
||||
parent: undefined,
|
||||
replies: undefined,
|
||||
hasOPLike: undefined,
|
||||
ctx: {
|
||||
depth: 0,
|
||||
isHighlightedPost: true,
|
||||
@@ -573,6 +578,7 @@ function postViewToPlaceholderThread(
|
||||
record: post.record as AppBskyFeedPost.Record, // validated in notifs
|
||||
parent: undefined,
|
||||
replies: undefined,
|
||||
hasOPLike: undefined,
|
||||
ctx: {
|
||||
depth: 0,
|
||||
isHighlightedPost: true,
|
||||
@@ -594,6 +600,7 @@ function embedViewRecordToPlaceholderThread(
|
||||
record: record.value as AppBskyFeedPost.Record, // validated in getEmbeddedPost
|
||||
parent: undefined,
|
||||
replies: undefined,
|
||||
hasOPLike: undefined,
|
||||
ctx: {
|
||||
depth: 0,
|
||||
isHighlightedPost: true,
|
||||
|
||||
@@ -83,7 +83,12 @@ export async function createAgentAndLogin(
|
||||
) => void,
|
||||
) {
|
||||
const agent = new BskyAppAgent({service})
|
||||
await agent.login({identifier, password, authFactorToken})
|
||||
await agent.login({
|
||||
identifier,
|
||||
password,
|
||||
authFactorToken,
|
||||
allowTakendown: true,
|
||||
})
|
||||
|
||||
const account = agentToSessionAccountOrThrow(agent)
|
||||
const gates = tryFetchGates(account.did, 'prefer-fresh-gates')
|
||||
|
||||
@@ -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
|
||||
|
||||
const agent = state.currentAgentState.agent as BskyAppAgent
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {AppBskyActorDefs} from '@atproto/api'
|
||||
import {AppBskyActorDefs, AppBskyFeedDefs} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {NavigationProp, useNavigation} from '@react-navigation/native'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {VIDEO_FEED_URIS} from '#/lib/constants'
|
||||
import {ComposeIcon2} from '#/lib/icons'
|
||||
import {getRootNavigation, getTabState, TabState} from '#/lib/routes/helpers'
|
||||
import {AllNavigatorParams} from '#/lib/routes/types'
|
||||
@@ -15,6 +16,7 @@ import {isNative} from '#/platform/detection'
|
||||
import {listenSoftReset} from '#/state/events'
|
||||
import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
|
||||
import {useSetHomeBadge} from '#/state/home-badge'
|
||||
import {SavedFeedSourceInfo} from '#/state/queries/feed'
|
||||
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
|
||||
import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed'
|
||||
import {truncateAndInvalidate} from '#/state/queries/util'
|
||||
@@ -39,6 +41,7 @@ export function FeedPage({
|
||||
renderEmptyState,
|
||||
renderEndOfFeed,
|
||||
savedFeedConfig,
|
||||
feedInfo,
|
||||
}: {
|
||||
testID?: string
|
||||
feed: FeedDescriptor
|
||||
@@ -48,6 +51,7 @@ export function FeedPage({
|
||||
renderEmptyState: () => JSX.Element
|
||||
renderEndOfFeed?: () => JSX.Element
|
||||
savedFeedConfig?: AppBskyActorDefs.SavedFeed
|
||||
feedInfo: SavedFeedSourceInfo
|
||||
}) {
|
||||
const {hasSession} = useSession()
|
||||
const {_} = useLingui()
|
||||
@@ -61,6 +65,13 @@ export function FeedPage({
|
||||
const scrollElRef = React.useRef<ListMethods>(null)
|
||||
const [hasNew, setHasNew] = React.useState(false)
|
||||
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(() => {
|
||||
if (isPageFocused) {
|
||||
@@ -134,6 +145,7 @@ export function FeedPage({
|
||||
renderEndOfFeed={renderEndOfFeed}
|
||||
headerOffset={headerOffset}
|
||||
savedFeedConfig={savedFeedConfig}
|
||||
isVideoFeed={isVideoFeed}
|
||||
/>
|
||||
</FeedFeedbackProvider>
|
||||
</MainScrollProvider>
|
||||
|
||||
@@ -17,7 +17,6 @@ import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {s} from '#/lib/styles'
|
||||
import {logger} from '#/logger'
|
||||
import {shouldClickOpenNewTab} from '#/platform/urls'
|
||||
import {FeedSourceInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
|
||||
import {
|
||||
useAddSavedFeedsMutation,
|
||||
@@ -29,6 +28,7 @@ import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {useTheme} from '#/alf'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {shouldClickOpenNewTab} from '#/components/Link'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {RichText} from '#/components/RichText'
|
||||
import {Text} from '../util/text/Text'
|
||||
|
||||
@@ -205,9 +205,7 @@ export const ProfileFeedgens = React.forwardRef<
|
||||
headerOffset={headerOffset}
|
||||
progressViewOffset={ios(0)}
|
||||
contentContainerStyle={isMobile && {paddingBottom: headerOffset + 100}}
|
||||
indicatorStyle={t.name === 'light' ? 'black' : 'white'}
|
||||
removeClippedSubviews={true}
|
||||
// @ts-ignore our .web version only -prf
|
||||
desktopFixedHeight
|
||||
onEndReached={onEndReached}
|
||||
/>
|
||||
|
||||
@@ -203,9 +203,7 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
|
||||
contentContainerStyle={
|
||||
isMobile && {paddingBottom: headerOffset + 100}
|
||||
}
|
||||
indicatorStyle={t.name === 'light' ? 'black' : 'white'}
|
||||
removeClippedSubviews={true}
|
||||
// @ts-ignore our .web version only -prf
|
||||
desktopFixedHeight
|
||||
onEndReached={onEndReached}
|
||||
/>
|
||||
|
||||
@@ -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 {DrawerGestureContext} from 'react-native-drawer-layout'
|
||||
import {Gesture, GestureDetector} from 'react-native-gesture-handler'
|
||||
@@ -17,7 +17,6 @@ import Animated, {
|
||||
} from 'react-native-reanimated'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
|
||||
import {isAndroid} from '#/platform/detection'
|
||||
import {useSetDrawerSwipeDisabled} from '#/state/shell'
|
||||
import {atoms as a, native} from '#/alf'
|
||||
|
||||
@@ -149,11 +148,7 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
|
||||
style={[a.flex_1]}
|
||||
initialPage={initialPage}
|
||||
onPageScroll={handlePageScroll}>
|
||||
{isAndroid
|
||||
? Children.map(children, child => (
|
||||
<CaptureSwipesAndroid>{child}</CaptureSwipesAndroid>
|
||||
))
|
||||
: children}
|
||||
{children}
|
||||
</AnimatedPagerView>
|
||||
</GestureDetector>
|
||||
</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(
|
||||
handlers: {
|
||||
onPageScroll: (e: PagerViewOnPageScrollEventData) => void
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user