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: [
|
ignorePatterns: [
|
||||||
'**/__mocks__/*.ts',
|
'**/__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:
|
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"
|
id: "e2eOpenLoggedOutView"
|
||||||
- tapOn:
|
- tapOn:
|
||||||
id: "createAccountButton"
|
id: "createAccountButton"
|
||||||
- tapOn:
|
- tapOn: "Bluesky Social"
|
||||||
id: "selectServiceButton"
|
|
||||||
- tapOn:
|
- tapOn:
|
||||||
id: "customSelectBtn"
|
id: "customSelectBtn"
|
||||||
- tapOn:
|
- tapOn:
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ appId: xyz.blueskyweb.app
|
|||||||
label: "Feeds button disappears after pinning a feed"
|
label: "Feeds button disappears after pinning a feed"
|
||||||
id: "bottomBarProfileBtn"
|
id: "bottomBarProfileBtn"
|
||||||
- tapOn:
|
- tapOn:
|
||||||
id: "profilePager-selector-4"
|
id: "profilePager-selector-5"
|
||||||
- tapOn: "alice-favs"
|
- tapOn: "alice-favs"
|
||||||
- tapOn: "Pin to Home"
|
- tapOn: "Pin to Home"
|
||||||
- tapOn:
|
- tapOn:
|
||||||
|
|||||||
+21
-3
@@ -75,9 +75,11 @@ module.exports = function (config) {
|
|||||||
'an',
|
'an',
|
||||||
'ast',
|
'ast',
|
||||||
'ca',
|
'ca',
|
||||||
|
'da',
|
||||||
'de',
|
'de',
|
||||||
'el',
|
'el',
|
||||||
'es',
|
'es',
|
||||||
|
'eu',
|
||||||
'fi',
|
'fi',
|
||||||
'fr',
|
'fr',
|
||||||
'ga',
|
'ga',
|
||||||
@@ -95,11 +97,12 @@ module.exports = function (config) {
|
|||||||
'pt-BR',
|
'pt-BR',
|
||||||
'ro',
|
'ro',
|
||||||
'ru',
|
'ru',
|
||||||
|
'sv',
|
||||||
'th',
|
'th',
|
||||||
'tr',
|
'tr',
|
||||||
'uk',
|
'uk',
|
||||||
'vi',
|
'vi',
|
||||||
'yue-Hant',
|
'yue',
|
||||||
'zh-Hans',
|
'zh-Hans',
|
||||||
'zh-Hant',
|
'zh-Hant',
|
||||||
],
|
],
|
||||||
@@ -191,6 +194,7 @@ module.exports = function (config) {
|
|||||||
channel: UPDATES_CHANNEL,
|
channel: UPDATES_CHANNEL,
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
'expo-video',
|
||||||
'expo-localization',
|
'expo-localization',
|
||||||
USE_SENTRY && [
|
USE_SENTRY && [
|
||||||
'@sentry/react-native/expo',
|
'@sentry/react-native/expo',
|
||||||
@@ -225,8 +229,12 @@ module.exports = function (config) {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
'react-native-compressor',
|
'react-native-compressor',
|
||||||
// TODO: Reenable when the build issue is fixed.
|
[
|
||||||
// '@bitdrift/react-native',
|
'@bitdrift/react-native',
|
||||||
|
{
|
||||||
|
networkInstrumentation: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
'./plugins/starterPackAppClipExtension/withStarterPackAppClip.js',
|
'./plugins/starterPackAppClipExtension/withStarterPackAppClip.js',
|
||||||
'./plugins/withAndroidManifestPlugin.js',
|
'./plugins/withAndroidManifestPlugin.js',
|
||||||
'./plugins/withAndroidManifestFCMIconPlugin.js',
|
'./plugins/withAndroidManifestFCMIconPlugin.js',
|
||||||
@@ -347,6 +355,16 @@ module.exports = function (config) {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
['expo-screen-orientation', {initialOrientation: 'PORTRAIT_UP'}],
|
['expo-screen-orientation', {initialOrientation: 'PORTRAIT_UP'}],
|
||||||
|
[
|
||||||
|
'react-native-vision-camera',
|
||||||
|
{
|
||||||
|
enableLocation: false,
|
||||||
|
cameraPermissionText: 'Bluesky needs access to your camera.',
|
||||||
|
enableMicrophonePermission: true,
|
||||||
|
microphonePermissionText:
|
||||||
|
'Bluesky needs access to your microphone.',
|
||||||
|
},
|
||||||
|
],
|
||||||
].filter(Boolean),
|
].filter(Boolean),
|
||||||
extra: {
|
extra: {
|
||||||
eas: {
|
eas: {
|
||||||
|
|||||||
@@ -214,15 +214,21 @@ func serve(cctx *cli.Context) error {
|
|||||||
e.GET("/iframe/youtube.html", echo.WrapHandler(staticHandler))
|
e.GET("/iframe/youtube.html", echo.WrapHandler(staticHandler))
|
||||||
e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler)), func(next echo.HandlerFunc) echo.HandlerFunc {
|
e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler)), func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
path := c.Request().URL.Path
|
c.Response().Before(func() {
|
||||||
maxAge := 1 * (60 * 60) // default is 1 hour
|
if c.Response().Status >= 300 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// all assets in /static/js, /static/css, /static/media are content-hashed and can be cached for a long time
|
path := c.Request().URL.Path
|
||||||
if strings.HasPrefix(path, "/static/js/") || strings.HasPrefix(path, "/static/css/") || strings.HasPrefix(path, "/static/media/") {
|
maxAge := 1 * (60 * 60) // default is 1 hour
|
||||||
maxAge = 365 * (60 * 60 * 24) // 1 year
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Response().Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", maxAge))
|
// all assets in /static/js, /static/css, /static/media are content-hashed and can be cached for a long time
|
||||||
|
if strings.HasPrefix(path, "/static/js/") || strings.HasPrefix(path, "/static/css/") || strings.HasPrefix(path, "/static/media/") {
|
||||||
|
maxAge = 365 * (60 * 60 * 24) // 1 year
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Response().Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", maxAge))
|
||||||
|
})
|
||||||
return next(c)
|
return next(c)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
Preconnect to essential domains
|
Preconnect to essential domains
|
||||||
-->
|
-->
|
||||||
<link rel="preconnect" href="https://bsky.social">
|
<link rel="preconnect" href="https://bsky.social">
|
||||||
<link rel="preconnect" href="https://bsky.network">
|
|
||||||
<title>{%- block head_title -%}Bluesky{%- endblock -%}</title>
|
<title>{%- block head_title -%}Bluesky{%- endblock -%}</title>
|
||||||
|
|
||||||
<!-- Hello Humans! API docs at https://atproto.com -->
|
<!-- Hello Humans! API docs at https://atproto.com -->
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ module.exports = {
|
|||||||
'an',
|
'an',
|
||||||
'ast',
|
'ast',
|
||||||
'ca',
|
'ca',
|
||||||
|
'da',
|
||||||
'de',
|
'de',
|
||||||
'el',
|
'el',
|
||||||
'en-GB',
|
'en-GB',
|
||||||
'es',
|
'es',
|
||||||
|
'eu',
|
||||||
'fi',
|
'fi',
|
||||||
'fr',
|
'fr',
|
||||||
'ga',
|
'ga',
|
||||||
@@ -26,6 +28,7 @@ module.exports = {
|
|||||||
'pt-BR',
|
'pt-BR',
|
||||||
'ro',
|
'ro',
|
||||||
'ru',
|
'ru',
|
||||||
|
'sv',
|
||||||
'th',
|
'th',
|
||||||
'tr',
|
'tr',
|
||||||
'uk',
|
'uk',
|
||||||
|
|||||||
+5
-2
@@ -54,7 +54,8 @@
|
|||||||
"icons:optimize": "svgo -f ./assets/icons"
|
"icons:optimize": "svgo -f ./assets/icons"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atproto/api": "^0.13.21",
|
"@atproto/api": "^0.13.31",
|
||||||
|
"@bitdrift/react-native": "^0.6.2",
|
||||||
"@braintree/sanitize-url": "^6.0.2",
|
"@braintree/sanitize-url": "^6.0.2",
|
||||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||||
"@emoji-mart/react": "^1.1.1",
|
"@emoji-mart/react": "^1.1.1",
|
||||||
@@ -145,6 +146,7 @@
|
|||||||
"expo-system-ui": "^4.0.4",
|
"expo-system-ui": "^4.0.4",
|
||||||
"expo-task-manager": "~12.0.3",
|
"expo-task-manager": "~12.0.3",
|
||||||
"expo-updates": "~0.26.10",
|
"expo-updates": "~0.26.10",
|
||||||
|
"expo-video": "^2.0.5",
|
||||||
"expo-web-browser": "~14.0.1",
|
"expo-web-browser": "~14.0.1",
|
||||||
"fast-text-encoding": "^1.0.6",
|
"fast-text-encoding": "^1.0.6",
|
||||||
"history": "^5.3.0",
|
"history": "^5.3.0",
|
||||||
@@ -178,7 +180,7 @@
|
|||||||
"react-native-ios-context-menu": "^1.15.3",
|
"react-native-ios-context-menu": "^1.15.3",
|
||||||
"react-native-keyboard-controller": "^1.14.5",
|
"react-native-keyboard-controller": "^1.14.5",
|
||||||
"react-native-mmkv": "^2.12.2",
|
"react-native-mmkv": "^2.12.2",
|
||||||
"react-native-pager-view": "6.6.1",
|
"react-native-pager-view": "6.7.0",
|
||||||
"react-native-picker-select": "^9.3.1",
|
"react-native-picker-select": "^9.3.1",
|
||||||
"react-native-progress": "bluesky-social/react-native-progress",
|
"react-native-progress": "bluesky-social/react-native-progress",
|
||||||
"react-native-qrcode-styled": "^0.3.3",
|
"react-native-qrcode-styled": "^0.3.3",
|
||||||
@@ -191,6 +193,7 @@
|
|||||||
"react-native-url-polyfill": "^1.3.0",
|
"react-native-url-polyfill": "^1.3.0",
|
||||||
"react-native-uuid": "^2.0.2",
|
"react-native-uuid": "^2.0.2",
|
||||||
"react-native-view-shot": "^4.0.3",
|
"react-native-view-shot": "^4.0.3",
|
||||||
|
"react-native-vision-camera": "^4.6.3",
|
||||||
"react-native-web": "~0.19.13",
|
"react-native-web": "~0.19.13",
|
||||||
"react-native-web-webview": "^1.0.2",
|
"react-native-web-webview": "^1.0.2",
|
||||||
"react-native-webview": "13.12.3",
|
"react-native-webview": "13.12.3",
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ import {
|
|||||||
StarterPackScreenShort,
|
StarterPackScreenShort,
|
||||||
} from '#/screens/StarterPack/StarterPackScreen'
|
} from '#/screens/StarterPack/StarterPackScreen'
|
||||||
import {Wizard} from '#/screens/StarterPack/Wizard'
|
import {Wizard} from '#/screens/StarterPack/Wizard'
|
||||||
|
import {VideoFeed} from '#/screens/VideoFeed'
|
||||||
import {useTheme} from '#/alf'
|
import {useTheme} from '#/alf'
|
||||||
import {router} from '#/routes'
|
import {router} from '#/routes'
|
||||||
import {Referrer} from '../modules/expo-bluesky-swiss-army'
|
import {Referrer} from '../modules/expo-bluesky-swiss-army'
|
||||||
@@ -422,6 +423,14 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
|||||||
getComponent={() => Wizard}
|
getComponent={() => Wizard}
|
||||||
options={{title: title(msg`Edit your starter pack`), requireAuth: true}}
|
options={{title: title(msg`Edit your starter pack`), requireAuth: true}}
|
||||||
/>
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name="VideoFeed"
|
||||||
|
getComponent={() => VideoFeed}
|
||||||
|
options={{
|
||||||
|
title: title(msg`Video Feed`),
|
||||||
|
requireAuth: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -497,7 +497,7 @@ export function createThemes({
|
|||||||
color: dimPalette.contrast_400,
|
color: dimPalette.contrast_400,
|
||||||
},
|
},
|
||||||
text_contrast_medium: {
|
text_contrast_medium: {
|
||||||
color: dimPalette.contrast_700,
|
color: dimPalette.contrast_600,
|
||||||
},
|
},
|
||||||
text_contrast_high: {
|
text_contrast_high: {
|
||||||
color: dimPalette.contrast_900,
|
color: dimPalette.contrast_900,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {useA11y} from '#/state/a11y'
|
|||||||
import {useDialogStateControlContext} from '#/state/dialogs'
|
import {useDialogStateControlContext} from '#/state/dialogs'
|
||||||
import {List, ListMethods, ListProps} from '#/view/com/util/List'
|
import {List, ListMethods, ListProps} from '#/view/com/util/List'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import {useThemeName} from '#/alf/util/useColorModeTheme'
|
||||||
import {Context, useDialogContext} from '#/components/Dialog/context'
|
import {Context, useDialogContext} from '#/components/Dialog/context'
|
||||||
import {
|
import {
|
||||||
DialogControlProps,
|
DialogControlProps,
|
||||||
@@ -55,7 +56,8 @@ export function Outer({
|
|||||||
nativeOptions,
|
nativeOptions,
|
||||||
testID,
|
testID,
|
||||||
}: React.PropsWithChildren<DialogOuterProps>) {
|
}: React.PropsWithChildren<DialogOuterProps>) {
|
||||||
const t = useTheme()
|
const themeName = useThemeName()
|
||||||
|
const t = useTheme(themeName)
|
||||||
const ref = React.useRef<BottomSheetNativeComponent>(null)
|
const ref = React.useRef<BottomSheetNativeComponent>(null)
|
||||||
const closeCallbacks = React.useRef<(() => void)[]>([])
|
const closeCallbacks = React.useRef<(() => void)[]>([])
|
||||||
const {setDialogIsOpen, setFullyExpandedCount} =
|
const {setDialogIsOpen, setFullyExpandedCount} =
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View} from 'react-native'
|
import {ScrollView, View} from 'react-native'
|
||||||
import {ScrollView} from 'react-native-gesture-handler'
|
|
||||||
import {AppBskyActorDefs, AppBskyFeedDefs, AtUri} from '@atproto/api'
|
import {AppBskyActorDefs, AppBskyFeedDefs, AtUri} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
@@ -17,6 +16,7 @@ import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows
|
|||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import * as userActionHistory from '#/state/userActionHistory'
|
import * as userActionHistory from '#/state/userActionHistory'
|
||||||
import {SeenPost} from '#/state/userActionHistory'
|
import {SeenPost} from '#/state/userActionHistory'
|
||||||
|
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
|
||||||
import {atoms as a, useBreakpoints, useTheme, ViewStyleProp, web} from '#/alf'
|
import {atoms as a, useBreakpoints, useTheme, ViewStyleProp, web} from '#/alf'
|
||||||
import {Button} from '#/components/Button'
|
import {Button} from '#/components/Button'
|
||||||
import * as FeedCard from '#/components/FeedCard'
|
import * as FeedCard from '#/components/FeedCard'
|
||||||
@@ -358,33 +358,37 @@ export function ProfileGrid({
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<ScrollView
|
<BlockDrawerGesture>
|
||||||
horizontal
|
<ScrollView
|
||||||
showsHorizontalScrollIndicator={false}
|
horizontal
|
||||||
snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap}
|
showsHorizontalScrollIndicator={false}
|
||||||
decelerationRate="fast">
|
snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap}
|
||||||
<View style={[a.px_lg, a.pt_sm, a.pb_lg, a.flex_row, a.gap_md]}>
|
decelerationRate="fast">
|
||||||
{content}
|
<View style={[a.px_lg, a.pt_sm, a.pb_lg, a.flex_row, a.gap_md]}>
|
||||||
|
{content}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
label={_(msg`Browse more accounts on the Explore page`)}
|
label={_(msg`Browse more accounts on the Explore page`)}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
navigation.navigate('SearchTab')
|
navigation.navigate('SearchTab')
|
||||||
}}>
|
}}>
|
||||||
<CardOuter style={[a.flex_1, {borderWidth: 0}]}>
|
<CardOuter style={[a.flex_1, {borderWidth: 0}]}>
|
||||||
<View style={[a.flex_1, a.justify_center]}>
|
<View style={[a.flex_1, a.justify_center]}>
|
||||||
<View style={[a.flex_row, a.px_lg]}>
|
<View style={[a.flex_row, a.px_lg]}>
|
||||||
<Text style={[a.pr_xl, a.flex_1, a.leading_snug]}>
|
<Text style={[a.pr_xl, a.flex_1, a.leading_snug]}>
|
||||||
<Trans>Browse more suggestions on the Explore page</Trans>
|
<Trans>
|
||||||
</Text>
|
Browse more suggestions on the Explore page
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
|
||||||
<Arrow size="xl" />
|
<Arrow size="xl" />
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</CardOuter>
|
||||||
</CardOuter>
|
</Button>
|
||||||
</Button>
|
</View>
|
||||||
</View>
|
</ScrollView>
|
||||||
</ScrollView>
|
</BlockDrawerGesture>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
@@ -491,34 +495,38 @@ export function SuggestedFeeds() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<ScrollView
|
<BlockDrawerGesture>
|
||||||
horizontal
|
<ScrollView
|
||||||
showsHorizontalScrollIndicator={false}
|
horizontal
|
||||||
snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap}
|
showsHorizontalScrollIndicator={false}
|
||||||
decelerationRate="fast">
|
snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap}
|
||||||
<View style={[a.px_lg, a.pt_md, a.pb_xl, a.flex_row, a.gap_md]}>
|
decelerationRate="fast">
|
||||||
{content}
|
<View style={[a.px_lg, a.pt_md, a.pb_xl, a.flex_row, a.gap_md]}>
|
||||||
|
{content}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
label={_(msg`Browse more feeds on the Explore page`)}
|
label={_(msg`Browse more feeds on the Explore page`)}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
navigation.navigate('SearchTab')
|
navigation.navigate('SearchTab')
|
||||||
}}
|
}}
|
||||||
style={[a.flex_col]}>
|
style={[a.flex_col]}>
|
||||||
<CardOuter style={[a.flex_1]}>
|
<CardOuter style={[a.flex_1]}>
|
||||||
<View style={[a.flex_1, a.justify_center]}>
|
<View style={[a.flex_1, a.justify_center]}>
|
||||||
<View style={[a.flex_row, a.px_lg]}>
|
<View style={[a.flex_row, a.px_lg]}>
|
||||||
<Text style={[a.pr_xl, a.flex_1, a.leading_snug]}>
|
<Text style={[a.pr_xl, a.flex_1, a.leading_snug]}>
|
||||||
<Trans>Browse more suggestions on the Explore page</Trans>
|
<Trans>
|
||||||
</Text>
|
Browse more suggestions on the Explore page
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
|
||||||
<Arrow size="xl" />
|
<Arrow size="xl" />
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</CardOuter>
|
||||||
</CardOuter>
|
</Button>
|
||||||
</Button>
|
</View>
|
||||||
</View>
|
</ScrollView>
|
||||||
</ScrollView>
|
</BlockDrawerGesture>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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"
|
shape="square"
|
||||||
onPress={onPressBack}
|
onPress={onPressBack}
|
||||||
hitSlop={HITSLOP_30}
|
hitSlop={HITSLOP_30}
|
||||||
style={[{marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET}, style]}
|
style={[
|
||||||
|
{marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET},
|
||||||
|
a.bg_transparent,
|
||||||
|
style,
|
||||||
|
]}
|
||||||
{...props}>
|
{...props}>
|
||||||
<ButtonIcon icon={ArrowLeft} size="lg" />
|
<ButtonIcon icon={ArrowLeft} size="lg" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export const Content = React.memo(function Content({
|
|||||||
contentContainerStyle,
|
contentContainerStyle,
|
||||||
...props
|
...props
|
||||||
}: ContentProps) {
|
}: ContentProps) {
|
||||||
|
const t = useTheme()
|
||||||
const {footerHeight} = useShellLayout()
|
const {footerHeight} = useShellLayout()
|
||||||
const animatedProps = useAnimatedProps(() => {
|
const animatedProps = useAnimatedProps(() => {
|
||||||
return {
|
return {
|
||||||
@@ -73,6 +74,7 @@ export const Content = React.memo(function Content({
|
|||||||
<Animated.ScrollView
|
<Animated.ScrollView
|
||||||
id="content"
|
id="content"
|
||||||
automaticallyAdjustsScrollIndicatorInsets={false}
|
automaticallyAdjustsScrollIndicatorInsets={false}
|
||||||
|
indicatorStyle={t.scheme === 'dark' ? 'white' : 'black'}
|
||||||
// sets the scroll inset to the height of the footer
|
// sets the scroll inset to the height of the footer
|
||||||
animatedProps={animatedProps}
|
animatedProps={animatedProps}
|
||||||
style={[scrollViewStyles.common, style]}
|
style={[scrollViewStyles.common, style]}
|
||||||
|
|||||||
@@ -6,12 +6,18 @@ import {gradients} from '#/alf/tokens'
|
|||||||
|
|
||||||
export function LinearGradientBackground({
|
export function LinearGradientBackground({
|
||||||
style,
|
style,
|
||||||
|
gradient = 'sky',
|
||||||
children,
|
children,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
}: {
|
}: {
|
||||||
style: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
children: React.ReactNode
|
gradient?: keyof typeof gradients
|
||||||
|
children?: React.ReactNode
|
||||||
|
start?: [number, number]
|
||||||
|
end?: [number, number]
|
||||||
}) {
|
}) {
|
||||||
const gradient = gradients.sky.values.map(([_, color]) => {
|
const colors = gradients[gradient].values.map(([_, color]) => {
|
||||||
return color
|
return color
|
||||||
}) as [string, string, ...string[]]
|
}) as [string, string, ...string[]]
|
||||||
|
|
||||||
@@ -20,7 +26,7 @@ export function LinearGradientBackground({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LinearGradient colors={gradient} style={style}>
|
<LinearGradient colors={colors} style={style} start={start} end={end}>
|
||||||
{children}
|
{children}
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
)
|
)
|
||||||
|
|||||||
+120
-24
@@ -15,7 +15,6 @@ import {
|
|||||||
linkRequiresWarning,
|
linkRequiresWarning,
|
||||||
} from '#/lib/strings/url-helpers'
|
} from '#/lib/strings/url-helpers'
|
||||||
import {isNative, isWeb} from '#/platform/detection'
|
import {isNative, isWeb} from '#/platform/detection'
|
||||||
import {shouldClickOpenNewTab} from '#/platform/urls'
|
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
import {atoms as a, flatten, TextStyleProp, useTheme, web} from '#/alf'
|
import {atoms as a, flatten, TextStyleProp, useTheme, web} from '#/alf'
|
||||||
import {Button, ButtonProps} from '#/components/Button'
|
import {Button, ButtonProps} from '#/components/Button'
|
||||||
@@ -55,6 +54,12 @@ type BaseLinkProps = Pick<
|
|||||||
*/
|
*/
|
||||||
onPress?: (e: GestureResponderEvent) => void | false
|
onPress?: (e: GestureResponderEvent) => void | false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for when the link is long pressed (on native). Prevent default
|
||||||
|
* and return `false` to exit early and prevent default long press hander.
|
||||||
|
*/
|
||||||
|
onLongPress?: (e: GestureResponderEvent) => void | false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Web-only attribute. Sets `download` attr on web.
|
* Web-only attribute. Sets `download` attr on web.
|
||||||
*/
|
*/
|
||||||
@@ -72,9 +77,12 @@ export function useLink({
|
|||||||
action = 'push',
|
action = 'push',
|
||||||
disableMismatchWarning,
|
disableMismatchWarning,
|
||||||
onPress: outerOnPress,
|
onPress: outerOnPress,
|
||||||
|
onLongPress: outerOnLongPress,
|
||||||
shareOnLongPress,
|
shareOnLongPress,
|
||||||
|
overridePresentation,
|
||||||
}: BaseLinkProps & {
|
}: BaseLinkProps & {
|
||||||
displayText: string
|
displayText: string
|
||||||
|
overridePresentation?: boolean
|
||||||
}) {
|
}) {
|
||||||
const navigation = useNavigationDeduped()
|
const navigation = useNavigationDeduped()
|
||||||
const {href} = useLinkProps<AllNavigatorParams>({
|
const {href} = useLinkProps<AllNavigatorParams>({
|
||||||
@@ -110,7 +118,7 @@ export function useLink({
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
if (isExternal) {
|
if (isExternal) {
|
||||||
openLink(href)
|
openLink(href, overridePresentation)
|
||||||
} else {
|
} else {
|
||||||
const shouldOpenInNewTab = shouldClickOpenNewTab(e)
|
const shouldOpenInNewTab = shouldClickOpenNewTab(e)
|
||||||
|
|
||||||
@@ -152,6 +160,7 @@ export function useLink({
|
|||||||
closeModal,
|
closeModal,
|
||||||
action,
|
action,
|
||||||
navigation,
|
navigation,
|
||||||
|
overridePresentation,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -175,8 +184,14 @@ export function useLink({
|
|||||||
}
|
}
|
||||||
}, [disableMismatchWarning, displayText, href, isExternal, openModal])
|
}, [disableMismatchWarning, displayText, href, isExternal, openModal])
|
||||||
|
|
||||||
const onLongPress =
|
const onLongPress = React.useCallback(
|
||||||
isNative && isExternal && shareOnLongPress ? handleLongPress : undefined
|
(e: GestureResponderEvent) => {
|
||||||
|
const exitEarlyIfFalse = outerOnLongPress?.(e)
|
||||||
|
if (exitEarlyIfFalse === false) return
|
||||||
|
return isNative && shareOnLongPress ? handleLongPress() : undefined
|
||||||
|
},
|
||||||
|
[outerOnLongPress, handleLongPress, shareOnLongPress],
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isExternal,
|
isExternal,
|
||||||
@@ -202,14 +217,16 @@ export function Link({
|
|||||||
to,
|
to,
|
||||||
action = 'push',
|
action = 'push',
|
||||||
onPress: outerOnPress,
|
onPress: outerOnPress,
|
||||||
|
onLongPress: outerOnLongPress,
|
||||||
download,
|
download,
|
||||||
...rest
|
...rest
|
||||||
}: LinkProps) {
|
}: LinkProps) {
|
||||||
const {href, isExternal, onPress} = useLink({
|
const {href, isExternal, onPress, onLongPress} = useLink({
|
||||||
to,
|
to,
|
||||||
displayText: typeof children === 'string' ? children : '',
|
displayText: typeof children === 'string' ? children : '',
|
||||||
action,
|
action,
|
||||||
onPress: outerOnPress,
|
onPress: outerOnPress,
|
||||||
|
onLongPress: outerOnLongPress,
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -220,6 +237,7 @@ export function Link({
|
|||||||
accessibilityRole="link"
|
accessibilityRole="link"
|
||||||
href={href}
|
href={href}
|
||||||
onPress={download ? undefined : onPress}
|
onPress={download ? undefined : onPress}
|
||||||
|
onLongPress={onLongPress}
|
||||||
{...web({
|
{...web({
|
||||||
hrefAttrs: {
|
hrefAttrs: {
|
||||||
target: download ? undefined : isExternal ? 'blank' : undefined,
|
target: download ? undefined : isExternal ? 'blank' : undefined,
|
||||||
@@ -239,12 +257,13 @@ export function Link({
|
|||||||
export type InlineLinkProps = React.PropsWithChildren<
|
export type InlineLinkProps = React.PropsWithChildren<
|
||||||
BaseLinkProps &
|
BaseLinkProps &
|
||||||
TextStyleProp &
|
TextStyleProp &
|
||||||
Pick<TextProps, 'selectable' | 'numberOfLines'>
|
Pick<TextProps, 'selectable' | 'numberOfLines'> &
|
||||||
> &
|
Pick<ButtonProps, 'label' | 'accessibilityHint'> & {
|
||||||
Pick<ButtonProps, 'label'> & {
|
disableUnderline?: boolean
|
||||||
disableUnderline?: boolean
|
title?: TextProps['title']
|
||||||
title?: TextProps['title']
|
overridePresentation?: boolean
|
||||||
}
|
}
|
||||||
|
>
|
||||||
|
|
||||||
export function InlineLinkText({
|
export function InlineLinkText({
|
||||||
children,
|
children,
|
||||||
@@ -253,11 +272,13 @@ export function InlineLinkText({
|
|||||||
disableMismatchWarning,
|
disableMismatchWarning,
|
||||||
style,
|
style,
|
||||||
onPress: outerOnPress,
|
onPress: outerOnPress,
|
||||||
|
onLongPress: outerOnLongPress,
|
||||||
download,
|
download,
|
||||||
selectable,
|
selectable,
|
||||||
label,
|
label,
|
||||||
shareOnLongPress,
|
shareOnLongPress,
|
||||||
disableUnderline,
|
disableUnderline,
|
||||||
|
overridePresentation,
|
||||||
...rest
|
...rest
|
||||||
}: InlineLinkProps) {
|
}: InlineLinkProps) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
@@ -268,7 +289,9 @@ export function InlineLinkText({
|
|||||||
action,
|
action,
|
||||||
disableMismatchWarning,
|
disableMismatchWarning,
|
||||||
onPress: outerOnPress,
|
onPress: outerOnPress,
|
||||||
|
onLongPress: outerOnLongPress,
|
||||||
shareOnLongPress,
|
shareOnLongPress,
|
||||||
|
overridePresentation,
|
||||||
})
|
})
|
||||||
const {
|
const {
|
||||||
state: hovered,
|
state: hovered,
|
||||||
@@ -319,6 +342,21 @@ export function InlineLinkText({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function WebOnlyInlineLinkText({
|
||||||
|
children,
|
||||||
|
to,
|
||||||
|
onPress,
|
||||||
|
...props
|
||||||
|
}: Omit<InlineLinkProps, 'onLongPress'>) {
|
||||||
|
return isWeb ? (
|
||||||
|
<InlineLinkText {...props} to={to} onPress={onPress}>
|
||||||
|
{children}
|
||||||
|
</InlineLinkText>
|
||||||
|
) : (
|
||||||
|
<Text {...props}>{children}</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility to create a static `onPress` handler for a `Link` that would otherwise link to a URI
|
* Utility to create a static `onPress` handler for a `Link` that would otherwise link to a URI
|
||||||
*
|
*
|
||||||
@@ -327,7 +365,10 @@ export function InlineLinkText({
|
|||||||
*/
|
*/
|
||||||
export function createStaticClick(
|
export function createStaticClick(
|
||||||
onPressHandler: Exclude<BaseLinkProps['onPress'], undefined>,
|
onPressHandler: Exclude<BaseLinkProps['onPress'], undefined>,
|
||||||
): Pick<BaseLinkProps, 'to' | 'onPress'> {
|
): {
|
||||||
|
to: BaseLinkProps['to']
|
||||||
|
onPress: Exclude<BaseLinkProps['onPress'], undefined>
|
||||||
|
} {
|
||||||
return {
|
return {
|
||||||
to: '#',
|
to: '#',
|
||||||
onPress(e: GestureResponderEvent) {
|
onPress(e: GestureResponderEvent) {
|
||||||
@@ -338,17 +379,72 @@ export function createStaticClick(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WebOnlyInlineLinkText({
|
/**
|
||||||
children,
|
* Utility to create a static `onPress` handler for a `Link`, but only if the
|
||||||
to,
|
* click was not modified in some way e.g. `Cmd` or a middle click.
|
||||||
onPress,
|
*
|
||||||
...props
|
* On native, this behaves the same as `createStaticClick` because there are no
|
||||||
}: InlineLinkProps) {
|
* options to "modify" the click in this sense.
|
||||||
return isWeb ? (
|
*
|
||||||
<InlineLinkText {...props} to={to} onPress={onPress}>
|
* Example:
|
||||||
{children}
|
* `<Link {...createStaticClick(e => {...})} />`
|
||||||
</InlineLinkText>
|
*/
|
||||||
) : (
|
export function createStaticClickIfUnmodified(
|
||||||
<Text {...props}>{children}</Text>
|
onPressHandler: Exclude<BaseLinkProps['onPress'], undefined>,
|
||||||
|
): {onPress: Exclude<BaseLinkProps['onPress'], undefined>} {
|
||||||
|
return {
|
||||||
|
onPress(e: GestureResponderEvent) {
|
||||||
|
if (!isWeb || !isModifiedClickEvent(e)) {
|
||||||
|
e.preventDefault()
|
||||||
|
onPressHandler(e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the click event has a meta key pressed, indicating the user
|
||||||
|
* intends to deviate from default behavior.
|
||||||
|
*/
|
||||||
|
export function isClickEventWithMetaKey(e: GestureResponderEvent) {
|
||||||
|
if (!isWeb) return false
|
||||||
|
const event = e as unknown as MouseEvent
|
||||||
|
return event.metaKey || event.altKey || event.ctrlKey || event.shiftKey
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the web click target is anything other than `_self`
|
||||||
|
*/
|
||||||
|
export function isClickTargetExternal(e: GestureResponderEvent) {
|
||||||
|
if (!isWeb) return false
|
||||||
|
const event = e as unknown as MouseEvent
|
||||||
|
const el = event.currentTarget as HTMLAnchorElement
|
||||||
|
return el && el.target && el.target !== '_self'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if a click event has been modified in some way from its default
|
||||||
|
* behavior, e.g. `Cmd` or a middle click.
|
||||||
|
* {@link https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button}
|
||||||
|
*/
|
||||||
|
export function isModifiedClickEvent(e: GestureResponderEvent): boolean {
|
||||||
|
if (!isWeb) return false
|
||||||
|
const event = e as unknown as MouseEvent
|
||||||
|
const isPrimaryButton = event.button === 0
|
||||||
|
return (
|
||||||
|
isClickEventWithMetaKey(e) || isClickTargetExternal(e) || !isPrimaryButton
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if a click event has been modified in a way that should indiciate
|
||||||
|
* that the user intends to open a new tab.
|
||||||
|
* {@link https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button}
|
||||||
|
*/
|
||||||
|
export function shouldClickOpenNewTab(e: GestureResponderEvent) {
|
||||||
|
if (!isWeb) return false
|
||||||
|
const event = e as unknown as MouseEvent
|
||||||
|
const isMiddleClick = isWeb && event.button === 1
|
||||||
|
return isClickEventWithMetaKey(e) || isClickTargetExternal(e) || isMiddleClick
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export function ListFooter({
|
|||||||
style,
|
style,
|
||||||
showEndMessage = false,
|
showEndMessage = false,
|
||||||
endMessageText,
|
endMessageText,
|
||||||
|
renderEndMessage,
|
||||||
}: {
|
}: {
|
||||||
isFetchingNextPage?: boolean
|
isFetchingNextPage?: boolean
|
||||||
hasNextPage?: boolean
|
hasNextPage?: boolean
|
||||||
@@ -29,6 +30,7 @@ export function ListFooter({
|
|||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
showEndMessage?: boolean
|
showEndMessage?: boolean
|
||||||
endMessageText?: string
|
endMessageText?: string
|
||||||
|
renderEndMessage?: () => React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
|
||||||
@@ -48,9 +50,13 @@ export function ListFooter({
|
|||||||
) : error ? (
|
) : error ? (
|
||||||
<ListFooterMaybeError error={error} onRetry={onRetry} />
|
<ListFooterMaybeError error={error} onRetry={onRetry} />
|
||||||
) : !hasNextPage && showEndMessage ? (
|
) : !hasNextPage && showEndMessage ? (
|
||||||
<Text style={[a.text_sm, t.atoms.text_contrast_low]}>
|
renderEndMessage ? (
|
||||||
{endMessageText ?? <Trans>You have reached the end</Trans>}
|
renderEndMessage()
|
||||||
</Text>
|
) : (
|
||||||
|
<Text style={[a.text_sm, t.atoms.text_contrast_low]}>
|
||||||
|
{endMessageText ?? <Trans>You have reached the end</Trans>}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -47,7 +47,12 @@ export function Root({
|
|||||||
return <Context.Provider value={context}>{children}</Context.Provider>
|
return <Context.Provider value={context}>{children}</Context.Provider>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Trigger({children, label, role = 'button'}: TriggerProps) {
|
export function Trigger({
|
||||||
|
children,
|
||||||
|
label,
|
||||||
|
role = 'button',
|
||||||
|
hint,
|
||||||
|
}: TriggerProps) {
|
||||||
const context = useMenuContext()
|
const context = useMenuContext()
|
||||||
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
|
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
|
||||||
const {
|
const {
|
||||||
@@ -65,11 +70,13 @@ export function Trigger({children, label, role = 'button'}: TriggerProps) {
|
|||||||
pressed,
|
pressed,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
ref: null,
|
||||||
onPress: context.control.open,
|
onPress: context.control.open,
|
||||||
onFocus,
|
onFocus,
|
||||||
onBlur,
|
onBlur,
|
||||||
onPressIn,
|
onPressIn,
|
||||||
onPressOut,
|
onPressOut,
|
||||||
|
accessibilityHint: hint,
|
||||||
accessibilityLabel: label,
|
accessibilityLabel: label,
|
||||||
accessibilityRole: role,
|
accessibilityRole: role,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -110,7 +110,12 @@ const RadixTriggerPassThrough = React.forwardRef(
|
|||||||
)
|
)
|
||||||
RadixTriggerPassThrough.displayName = 'RadixTriggerPassThrough'
|
RadixTriggerPassThrough.displayName = 'RadixTriggerPassThrough'
|
||||||
|
|
||||||
export function Trigger({children, label, role = 'button'}: TriggerProps) {
|
export function Trigger({
|
||||||
|
children,
|
||||||
|
label,
|
||||||
|
role = 'button',
|
||||||
|
hint,
|
||||||
|
}: TriggerProps) {
|
||||||
const {control} = useMenuContext()
|
const {control} = useMenuContext()
|
||||||
const {
|
const {
|
||||||
state: hovered,
|
state: hovered,
|
||||||
@@ -153,6 +158,7 @@ export function Trigger({children, label, role = 'button'}: TriggerProps) {
|
|||||||
onBlur: onBlur,
|
onBlur: onBlur,
|
||||||
onMouseEnter,
|
onMouseEnter,
|
||||||
onMouseLeave,
|
onMouseLeave,
|
||||||
|
accessibilityHint: hint,
|
||||||
accessibilityLabel: label,
|
accessibilityLabel: label,
|
||||||
accessibilityRole: role,
|
accessibilityRole: role,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export type ItemContextType = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type RadixPassThroughTriggerProps = {
|
export type RadixPassThroughTriggerProps = {
|
||||||
|
ref: React.RefObject<any>
|
||||||
id: string
|
id: string
|
||||||
type: 'button'
|
type: 'button'
|
||||||
disabled: boolean
|
disabled: boolean
|
||||||
@@ -37,6 +38,7 @@ export type RadixPassThroughTriggerProps = {
|
|||||||
export type TriggerProps = {
|
export type TriggerProps = {
|
||||||
children(props: TriggerChildProps): React.ReactNode
|
children(props: TriggerChildProps): React.ReactNode
|
||||||
label: string
|
label: string
|
||||||
|
hint?: string
|
||||||
role?: AccessibilityRole
|
role?: AccessibilityRole
|
||||||
}
|
}
|
||||||
export type TriggerChildProps =
|
export type TriggerChildProps =
|
||||||
@@ -59,11 +61,13 @@ export type TriggerChildProps =
|
|||||||
* object is empty.
|
* object is empty.
|
||||||
*/
|
*/
|
||||||
props: {
|
props: {
|
||||||
|
ref: null
|
||||||
onPress: () => void
|
onPress: () => void
|
||||||
onFocus: () => void
|
onFocus: () => void
|
||||||
onBlur: () => void
|
onBlur: () => void
|
||||||
onPressIn: () => void
|
onPressIn: () => void
|
||||||
onPressOut: () => void
|
onPressOut: () => void
|
||||||
|
accessibilityHint?: string
|
||||||
accessibilityLabel: string
|
accessibilityLabel: string
|
||||||
accessibilityRole: AccessibilityRole
|
accessibilityRole: AccessibilityRole
|
||||||
}
|
}
|
||||||
@@ -85,6 +89,7 @@ export type TriggerChildProps =
|
|||||||
onBlur: () => void
|
onBlur: () => void
|
||||||
onMouseEnter: () => void
|
onMouseEnter: () => void
|
||||||
onMouseLeave: () => void
|
onMouseLeave: () => void
|
||||||
|
accessibilityHint?: string
|
||||||
accessibilityLabel: string
|
accessibilityLabel: string
|
||||||
accessibilityRole: AccessibilityRole
|
accessibilityRole: AccessibilityRole
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import * as Dialog from '#/components/Dialog'
|
|||||||
import * as Toggle from '#/components/forms/Toggle'
|
import * as Toggle from '#/components/forms/Toggle'
|
||||||
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||||
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
|
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
|
||||||
|
import {PaperPlane_Stroke2_Corner0_Rounded as SendIcon} from '#/components/icons/PaperPlane'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {ReportDialogProps} from './types'
|
import {ReportDialogProps} from './types'
|
||||||
@@ -223,7 +224,7 @@ export function SubmitView({
|
|||||||
<ButtonText>
|
<ButtonText>
|
||||||
<Trans>Send report</Trans>
|
<Trans>Send report</Trans>
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
{submitting && <ButtonIcon icon={Loader} />}
|
<ButtonIcon icon={submitting ? Loader : SendIcon} />
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
{/* Maybe fix this later -h */}
|
{/* Maybe fix this later -h */}
|
||||||
|
|||||||
+13
-91
@@ -1,25 +1,19 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {TextStyle} from 'react-native'
|
import {TextStyle} from 'react-native'
|
||||||
import {AppBskyRichtextFacet, RichText as RichTextAPI} from '@atproto/api'
|
import {AppBskyRichtextFacet, RichText as RichTextAPI} from '@atproto/api'
|
||||||
import {msg} from '@lingui/macro'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
import {useNavigation} from '@react-navigation/native'
|
|
||||||
|
|
||||||
import {NavigationProp} from '#/lib/routes/types'
|
|
||||||
import {toShortUrl} from '#/lib/strings/url-helpers'
|
import {toShortUrl} from '#/lib/strings/url-helpers'
|
||||||
import {isNative} from '#/platform/detection'
|
import {atoms as a, flatten, TextStyleProp} from '#/alf'
|
||||||
import {atoms as a, flatten, native, TextStyleProp, useTheme, web} from '#/alf'
|
|
||||||
import {isOnlyEmoji} from '#/alf/typography'
|
import {isOnlyEmoji} from '#/alf/typography'
|
||||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
|
||||||
import {InlineLinkText, LinkProps} from '#/components/Link'
|
import {InlineLinkText, LinkProps} from '#/components/Link'
|
||||||
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
|
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
|
||||||
import {TagMenu, useTagMenuControl} from '#/components/TagMenu'
|
import {RichTextTag} from '#/components/RichTextTag'
|
||||||
import {Text, TextProps} from '#/components/Typography'
|
import {Text, TextProps} from '#/components/Typography'
|
||||||
|
|
||||||
const WORD_WRAP = {wordWrap: 1}
|
const WORD_WRAP = {wordWrap: 1}
|
||||||
|
|
||||||
export type RichTextProps = TextStyleProp &
|
export type RichTextProps = TextStyleProp &
|
||||||
Pick<TextProps, 'selectable'> & {
|
Pick<TextProps, 'selectable' | 'onLayout' | 'onTextLayout'> & {
|
||||||
value: RichTextAPI | string
|
value: RichTextAPI | string
|
||||||
testID?: string
|
testID?: string
|
||||||
numberOfLines?: number
|
numberOfLines?: number
|
||||||
@@ -43,6 +37,8 @@ export function RichText({
|
|||||||
onLinkPress,
|
onLinkPress,
|
||||||
interactiveStyle,
|
interactiveStyle,
|
||||||
emojiMultiplier = 1.85,
|
emojiMultiplier = 1.85,
|
||||||
|
onLayout,
|
||||||
|
onTextLayout,
|
||||||
}: RichTextProps) {
|
}: RichTextProps) {
|
||||||
const richText = React.useMemo(
|
const richText = React.useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -70,6 +66,8 @@ export function RichText({
|
|||||||
selectable={selectable}
|
selectable={selectable}
|
||||||
testID={testID}
|
testID={testID}
|
||||||
style={[plainStyles, {fontSize}]}
|
style={[plainStyles, {fontSize}]}
|
||||||
|
onLayout={onLayout}
|
||||||
|
onTextLayout={onTextLayout}
|
||||||
// @ts-ignore web only -prf
|
// @ts-ignore web only -prf
|
||||||
dataSet={WORD_WRAP}>
|
dataSet={WORD_WRAP}>
|
||||||
{text}
|
{text}
|
||||||
@@ -83,6 +81,8 @@ export function RichText({
|
|||||||
testID={testID}
|
testID={testID}
|
||||||
style={plainStyles}
|
style={plainStyles}
|
||||||
numberOfLines={numberOfLines}
|
numberOfLines={numberOfLines}
|
||||||
|
onLayout={onLayout}
|
||||||
|
onTextLayout={onTextLayout}
|
||||||
// @ts-ignore web only -prf
|
// @ts-ignore web only -prf
|
||||||
dataSet={WORD_WRAP}>
|
dataSet={WORD_WRAP}>
|
||||||
{text}
|
{text}
|
||||||
@@ -143,10 +143,9 @@ export function RichText({
|
|||||||
els.push(
|
els.push(
|
||||||
<RichTextTag
|
<RichTextTag
|
||||||
key={key}
|
key={key}
|
||||||
text={segment.text}
|
display={segment.text}
|
||||||
tag={tag.tag}
|
tag={tag.tag}
|
||||||
style={interactiveStyles}
|
textStyle={interactiveStyles}
|
||||||
selectable={selectable}
|
|
||||||
authorHandle={authorHandle}
|
authorHandle={authorHandle}
|
||||||
/>,
|
/>,
|
||||||
)
|
)
|
||||||
@@ -163,88 +162,11 @@ export function RichText({
|
|||||||
testID={testID}
|
testID={testID}
|
||||||
style={plainStyles}
|
style={plainStyles}
|
||||||
numberOfLines={numberOfLines}
|
numberOfLines={numberOfLines}
|
||||||
|
onLayout={onLayout}
|
||||||
|
onTextLayout={onTextLayout}
|
||||||
// @ts-ignore web only -prf
|
// @ts-ignore web only -prf
|
||||||
dataSet={WORD_WRAP}>
|
dataSet={WORD_WRAP}>
|
||||||
{els}
|
{els}
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function RichTextTag({
|
|
||||||
text,
|
|
||||||
tag,
|
|
||||||
style,
|
|
||||||
selectable,
|
|
||||||
authorHandle,
|
|
||||||
}: {
|
|
||||||
text: string
|
|
||||||
tag: string
|
|
||||||
selectable?: boolean
|
|
||||||
authorHandle?: string
|
|
||||||
} & TextStyleProp) {
|
|
||||||
const t = useTheme()
|
|
||||||
const {_} = useLingui()
|
|
||||||
const control = useTagMenuControl()
|
|
||||||
const {
|
|
||||||
state: hovered,
|
|
||||||
onIn: onHoverIn,
|
|
||||||
onOut: onHoverOut,
|
|
||||||
} = useInteractionState()
|
|
||||||
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
|
|
||||||
const navigation = useNavigation<NavigationProp>()
|
|
||||||
|
|
||||||
const navigateToPage = React.useCallback(() => {
|
|
||||||
navigation.push('Hashtag', {
|
|
||||||
tag: encodeURIComponent(tag),
|
|
||||||
})
|
|
||||||
}, [navigation, tag])
|
|
||||||
|
|
||||||
const openDialog = React.useCallback(() => {
|
|
||||||
control.open()
|
|
||||||
}, [control])
|
|
||||||
|
|
||||||
/*
|
|
||||||
* N.B. On web, this is wrapped in another pressable comopnent with a11y
|
|
||||||
* labels, etc. That's why only some of these props are applied here.
|
|
||||||
*/
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<TagMenu control={control} tag={tag} authorHandle={authorHandle}>
|
|
||||||
<Text
|
|
||||||
emoji
|
|
||||||
selectable={selectable}
|
|
||||||
{...native({
|
|
||||||
accessibilityLabel: _(msg`Hashtag: #${tag}`),
|
|
||||||
accessibilityHint: _(msg`Long press to open tag menu for #${tag}`),
|
|
||||||
accessibilityRole: isNative ? 'button' : undefined,
|
|
||||||
onPress: navigateToPage,
|
|
||||||
onLongPress: openDialog,
|
|
||||||
})}
|
|
||||||
{...web({
|
|
||||||
onMouseEnter: onHoverIn,
|
|
||||||
onMouseLeave: onHoverOut,
|
|
||||||
})}
|
|
||||||
// @ts-ignore
|
|
||||||
onFocus={onFocus}
|
|
||||||
onBlur={onBlur}
|
|
||||||
style={[
|
|
||||||
web({
|
|
||||||
cursor: 'pointer',
|
|
||||||
}),
|
|
||||||
{color: t.palette.primary_500},
|
|
||||||
(hovered || focused) && {
|
|
||||||
...web({
|
|
||||||
outline: 0,
|
|
||||||
textDecorationLine: 'underline',
|
|
||||||
textDecorationColor: t.palette.primary_500,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
style,
|
|
||||||
]}>
|
|
||||||
{text}
|
|
||||||
</Text>
|
|
||||||
</TagMenu>
|
|
||||||
</React.Fragment>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {StyleProp, Text as RNText, TextStyle} from 'react-native'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {useNavigation} from '@react-navigation/native'
|
||||||
|
|
||||||
|
import {NavigationProp} from '#/lib/routes/types'
|
||||||
|
import {isInvalidHandle} from '#/lib/strings/handles'
|
||||||
|
import {isNative, isWeb} from '#/platform/detection'
|
||||||
|
import {
|
||||||
|
usePreferencesQuery,
|
||||||
|
useRemoveMutedWordsMutation,
|
||||||
|
useUpsertMutedWordsMutation,
|
||||||
|
} from '#/state/queries/preferences'
|
||||||
|
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
|
||||||
|
import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
|
||||||
|
import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person'
|
||||||
|
import {
|
||||||
|
createStaticClick,
|
||||||
|
createStaticClickIfUnmodified,
|
||||||
|
InlineLinkText,
|
||||||
|
} from '#/components/Link'
|
||||||
|
import {Loader} from '#/components/Loader'
|
||||||
|
import * as Menu from '#/components/Menu'
|
||||||
|
|
||||||
|
export function RichTextTag({
|
||||||
|
tag,
|
||||||
|
display,
|
||||||
|
authorHandle,
|
||||||
|
textStyle,
|
||||||
|
}: {
|
||||||
|
tag: string
|
||||||
|
display: string
|
||||||
|
authorHandle?: string
|
||||||
|
textStyle: StyleProp<TextStyle>
|
||||||
|
}) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const {isLoading: isPreferencesLoading, data: preferences} =
|
||||||
|
usePreferencesQuery()
|
||||||
|
const {
|
||||||
|
mutateAsync: upsertMutedWord,
|
||||||
|
variables: optimisticUpsert,
|
||||||
|
reset: resetUpsert,
|
||||||
|
} = useUpsertMutedWordsMutation()
|
||||||
|
const {
|
||||||
|
mutateAsync: removeMutedWords,
|
||||||
|
variables: optimisticRemove,
|
||||||
|
reset: resetRemove,
|
||||||
|
} = useRemoveMutedWordsMutation()
|
||||||
|
const navigation = useNavigation<NavigationProp>()
|
||||||
|
const label = _(msg`Hashtag ${tag}`)
|
||||||
|
const hint = isNative
|
||||||
|
? _(msg`Long press to open tag menu for #${tag}`)
|
||||||
|
: _(msg`Click to open tag menu for ${tag}`)
|
||||||
|
|
||||||
|
const isMuted = Boolean(
|
||||||
|
(preferences?.moderationPrefs.mutedWords?.find(
|
||||||
|
m => m.value === tag && m.targets.includes('tag'),
|
||||||
|
) ??
|
||||||
|
optimisticUpsert?.find(
|
||||||
|
m => m.value === tag && m.targets.includes('tag'),
|
||||||
|
)) &&
|
||||||
|
!optimisticRemove?.find(m => m?.value === tag),
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Mute word records that exactly match the tag in question.
|
||||||
|
*/
|
||||||
|
const removeableMuteWords = React.useMemo(() => {
|
||||||
|
return (
|
||||||
|
preferences?.moderationPrefs.mutedWords?.filter(word => {
|
||||||
|
return word.value === tag
|
||||||
|
}) || []
|
||||||
|
)
|
||||||
|
}, [tag, preferences?.moderationPrefs?.mutedWords])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu.Root>
|
||||||
|
<Menu.Trigger label={label} hint={hint}>
|
||||||
|
{({props: menuProps}) => (
|
||||||
|
<InlineLinkText
|
||||||
|
to={{
|
||||||
|
screen: 'Hashtag',
|
||||||
|
params: {tag: encodeURIComponent(tag)},
|
||||||
|
}}
|
||||||
|
{...menuProps}
|
||||||
|
onPress={e => {
|
||||||
|
if (isWeb) {
|
||||||
|
return createStaticClickIfUnmodified(() => {
|
||||||
|
if (!isNative) {
|
||||||
|
menuProps.onPress()
|
||||||
|
}
|
||||||
|
}).onPress(e)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onLongPress={createStaticClick(menuProps.onPress).onPress}
|
||||||
|
accessibilityHint={hint}
|
||||||
|
label={label}
|
||||||
|
style={textStyle}>
|
||||||
|
{isNative ? (
|
||||||
|
display
|
||||||
|
) : (
|
||||||
|
<RNText ref={menuProps.ref}>{display}</RNText>
|
||||||
|
)}
|
||||||
|
</InlineLinkText>
|
||||||
|
)}
|
||||||
|
</Menu.Trigger>
|
||||||
|
<Menu.Outer>
|
||||||
|
<Menu.Group>
|
||||||
|
<Menu.Item
|
||||||
|
label={_(msg`See ${tag} posts`)}
|
||||||
|
onPress={() => {
|
||||||
|
navigation.push('Hashtag', {
|
||||||
|
tag: encodeURIComponent(tag),
|
||||||
|
})
|
||||||
|
}}>
|
||||||
|
<Menu.ItemText>
|
||||||
|
<Trans>See #{tag} posts</Trans>
|
||||||
|
</Menu.ItemText>
|
||||||
|
<Menu.ItemIcon icon={Search} />
|
||||||
|
</Menu.Item>
|
||||||
|
{authorHandle && !isInvalidHandle(authorHandle) && (
|
||||||
|
<Menu.Item
|
||||||
|
label={_(msg`See ${tag} posts by user`)}
|
||||||
|
onPress={() => {
|
||||||
|
navigation.push('Hashtag', {
|
||||||
|
tag: encodeURIComponent(tag),
|
||||||
|
author: authorHandle,
|
||||||
|
})
|
||||||
|
}}>
|
||||||
|
<Menu.ItemText>
|
||||||
|
<Trans>See #{tag} posts by user</Trans>
|
||||||
|
</Menu.ItemText>
|
||||||
|
<Menu.ItemIcon icon={Person} />
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
</Menu.Group>
|
||||||
|
<Menu.Divider />
|
||||||
|
<Menu.Item
|
||||||
|
label={isMuted ? _(msg`Unmute ${tag}`) : _(msg`Mute ${tag}`)}
|
||||||
|
onPress={() => {
|
||||||
|
if (isMuted) {
|
||||||
|
resetUpsert()
|
||||||
|
removeMutedWords(removeableMuteWords)
|
||||||
|
} else {
|
||||||
|
resetRemove()
|
||||||
|
upsertMutedWord([
|
||||||
|
{value: tag, targets: ['tag'], actorTarget: 'all'},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<Menu.ItemText>
|
||||||
|
{isMuted ? _(msg`Unmute ${tag}`) : _(msg`Mute ${tag}`)}
|
||||||
|
</Menu.ItemText>
|
||||||
|
<Menu.ItemIcon icon={isPreferencesLoading ? Loader : Mute} />
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Outer>
|
||||||
|
</Menu.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -136,7 +136,6 @@ export const ProfileStarterPacks = React.forwardRef<
|
|||||||
headerOffset={headerOffset}
|
headerOffset={headerOffset}
|
||||||
progressViewOffset={ios(0)}
|
progressViewOffset={ios(0)}
|
||||||
contentContainerStyle={{paddingBottom: headerOffset + bottomBarOffset}}
|
contentContainerStyle={{paddingBottom: headerOffset + bottomBarOffset}}
|
||||||
indicatorStyle={t.name === 'light' ? 'black' : 'white'}
|
|
||||||
removeClippedSubviews={true}
|
removeClippedSubviews={true}
|
||||||
desktopFixedHeight
|
desktopFixedHeight
|
||||||
onEndReached={onEndReached}
|
onEndReached={onEndReached}
|
||||||
|
|||||||
@@ -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 ? (
|
{latestReportableMessage ? (
|
||||||
<ReportDialog
|
<ReportDialog
|
||||||
|
currentScreen={currentScreen}
|
||||||
params={{
|
params={{
|
||||||
type: 'convoMessage',
|
type: 'convoMessage',
|
||||||
convoId: convo.id,
|
convoId: convo.id,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useNavigation} from '@react-navigation/native'
|
import {StackActions, useNavigation} from '@react-navigation/native'
|
||||||
|
|
||||||
import {NavigationProp} from '#/lib/routes/types'
|
import {NavigationProp} from '#/lib/routes/types'
|
||||||
import {isNative} from '#/platform/detection'
|
import {isNative} from '#/platform/detection'
|
||||||
@@ -22,15 +22,10 @@ export function LeaveConvoPrompt({
|
|||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
|
|
||||||
const {mutate: leaveConvo} = useLeaveConvo(convoId, {
|
const {mutate: leaveConvo} = useLeaveConvo(convoId, {
|
||||||
onSuccess: () => {
|
onMutate: () => {
|
||||||
if (currentScreen === 'conversation') {
|
if (currentScreen === 'conversation') {
|
||||||
navigation.replace(
|
navigation.dispatch(
|
||||||
'Messages',
|
StackActions.replace('Messages', isNative ? {animation: 'pop'} : {}),
|
||||||
isNative
|
|
||||||
? {
|
|
||||||
animation: 'pop',
|
|
||||||
}
|
|
||||||
: {},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ export let MessageMenu = ({
|
|||||||
</Menu.Root>
|
</Menu.Root>
|
||||||
|
|
||||||
<ReportDialog
|
<ReportDialog
|
||||||
|
currentScreen="conversation"
|
||||||
params={{type: 'convoMessage', convoId: convo.convo.id, message}}
|
params={{type: 'convoMessage', convoId: convo.convo.id, message}}
|
||||||
control={reportControl}
|
control={reportControl}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,27 +1,39 @@
|
|||||||
import React, {memo, useMemo, useState} from 'react'
|
import React, {memo, useMemo, useState} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {
|
import {
|
||||||
|
AppBskyActorDefs,
|
||||||
ChatBskyConvoDefs,
|
ChatBskyConvoDefs,
|
||||||
ComAtprotoModerationCreateReport,
|
ComAtprotoModerationCreateReport,
|
||||||
RichText as RichTextAPI,
|
RichText as RichTextAPI,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {StackActions, useNavigation} from '@react-navigation/native'
|
||||||
import {useMutation} from '@tanstack/react-query'
|
import {useMutation} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {ReportOption} from '#/lib/moderation/useReportOptions'
|
import {ReportOption} from '#/lib/moderation/useReportOptions'
|
||||||
|
import {NavigationProp} from '#/lib/routes/types'
|
||||||
|
import {isNative} from '#/platform/detection'
|
||||||
|
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||||
|
import {useLeaveConvo} from '#/state/queries/messages/leave-conversation'
|
||||||
|
import {
|
||||||
|
useProfileBlockMutationQueue,
|
||||||
|
useProfileQuery,
|
||||||
|
} from '#/state/queries/profile'
|
||||||
import {useAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
|
import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
import {atoms as a, platform, useBreakpoints, useTheme, web} from '#/alf'
|
||||||
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import {Button, ButtonIcon, ButtonText} from '../Button'
|
import {Divider} from '#/components/Divider'
|
||||||
import {Divider} from '../Divider'
|
import * as Toggle from '#/components/forms/Toggle'
|
||||||
import {ChevronLeft_Stroke2_Corner0_Rounded as Chevron} from '../icons/Chevron'
|
import {ChevronLeft_Stroke2_Corner0_Rounded as Chevron} from '#/components/icons/Chevron'
|
||||||
import {Loader} from '../Loader'
|
import {PaperPlane_Stroke2_Corner0_Rounded as SendIcon} from '#/components/icons/PaperPlane'
|
||||||
import {SelectReportOptionView} from '../ReportDialog/SelectReportOptionView'
|
import {Loader} from '#/components/Loader'
|
||||||
import {RichText} from '../RichText'
|
import {SelectReportOptionView} from '#/components/ReportDialog/SelectReportOptionView'
|
||||||
import {Text} from '../Typography'
|
import {RichText} from '#/components/RichText'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
import {MessageItemMetadata} from './MessageItem'
|
import {MessageItemMetadata} from './MessageItem'
|
||||||
|
|
||||||
type ReportDialogParams = {
|
type ReportDialogParams = {
|
||||||
@@ -33,16 +45,18 @@ type ReportDialogParams = {
|
|||||||
let ReportDialog = ({
|
let ReportDialog = ({
|
||||||
control,
|
control,
|
||||||
params,
|
params,
|
||||||
|
currentScreen,
|
||||||
}: {
|
}: {
|
||||||
control: Dialog.DialogControlProps
|
control: Dialog.DialogControlProps
|
||||||
params: ReportDialogParams
|
params: ReportDialogParams
|
||||||
|
currentScreen: 'list' | 'conversation'
|
||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
return (
|
return (
|
||||||
<Dialog.Outer control={control}>
|
<Dialog.Outer control={control}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
<Dialog.ScrollableInner label={_(msg`Report this message`)}>
|
<Dialog.ScrollableInner label={_(msg`Report this message`)}>
|
||||||
<DialogInner params={params} />
|
<DialogInner params={params} currentScreen={currentScreen} />
|
||||||
<Dialog.Close />
|
<Dialog.Close />
|
||||||
</Dialog.ScrollableInner>
|
</Dialog.ScrollableInner>
|
||||||
</Dialog.Outer>
|
</Dialog.Outer>
|
||||||
@@ -51,14 +65,44 @@ let ReportDialog = ({
|
|||||||
ReportDialog = memo(ReportDialog)
|
ReportDialog = memo(ReportDialog)
|
||||||
export {ReportDialog}
|
export {ReportDialog}
|
||||||
|
|
||||||
function DialogInner({params}: {params: ReportDialogParams}) {
|
function DialogInner({
|
||||||
|
params,
|
||||||
|
currentScreen,
|
||||||
|
}: {
|
||||||
|
params: ReportDialogParams
|
||||||
|
currentScreen: 'list' | 'conversation'
|
||||||
|
}) {
|
||||||
|
const {data: profile, isError} = useProfileQuery({
|
||||||
|
did: params.message.sender.did,
|
||||||
|
})
|
||||||
const [reportOption, setReportOption] = useState<ReportOption | null>(null)
|
const [reportOption, setReportOption] = useState<ReportOption | null>(null)
|
||||||
|
const [done, setDone] = useState(false)
|
||||||
|
const control = Dialog.useDialogContext()
|
||||||
|
|
||||||
return reportOption ? (
|
return done ? (
|
||||||
|
profile ? (
|
||||||
|
<DoneStep
|
||||||
|
convoId={params.convoId}
|
||||||
|
currentScreen={currentScreen}
|
||||||
|
profile={profile}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<View style={[a.w_full, a.py_5xl, a.align_center]}>
|
||||||
|
<Loader />
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
) : reportOption ? (
|
||||||
<SubmitStep
|
<SubmitStep
|
||||||
params={params}
|
params={params}
|
||||||
reportOption={reportOption}
|
reportOption={reportOption}
|
||||||
goBack={() => setReportOption(null)}
|
goBack={() => setReportOption(null)}
|
||||||
|
onComplete={() => {
|
||||||
|
if (isError) {
|
||||||
|
control.close()
|
||||||
|
} else {
|
||||||
|
setDone(true)
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ReasonStep params={params} setReportOption={setReportOption} />
|
<ReasonStep params={params} setReportOption={setReportOption} />
|
||||||
@@ -89,16 +133,17 @@ function SubmitStep({
|
|||||||
params,
|
params,
|
||||||
reportOption,
|
reportOption,
|
||||||
goBack,
|
goBack,
|
||||||
|
onComplete,
|
||||||
}: {
|
}: {
|
||||||
params: ReportDialogParams
|
params: ReportDialogParams
|
||||||
reportOption: ReportOption
|
reportOption: ReportOption
|
||||||
goBack: () => void
|
goBack: () => void
|
||||||
|
onComplete: () => void
|
||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const [details, setDetails] = useState('')
|
const [details, setDetails] = useState('')
|
||||||
const control = Dialog.useDialogContext()
|
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -124,11 +169,7 @@ function SubmitStep({
|
|||||||
await agent.createModerationReport(report)
|
await agent.createModerationReport(report)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: onComplete,
|
||||||
control.close(() => {
|
|
||||||
Toast.show(_(msg`Thank you. Your report has been sent.`))
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const copy = useMemo(() => {
|
const copy = useMemo(() => {
|
||||||
@@ -181,13 +222,12 @@ function SubmitStep({
|
|||||||
<View style={[a.relative, a.w_full]}>
|
<View style={[a.relative, a.w_full]}>
|
||||||
<Dialog.Input
|
<Dialog.Input
|
||||||
multiline
|
multiline
|
||||||
value={details}
|
defaultValue={details}
|
||||||
onChangeText={setDetails}
|
onChangeText={setDetails}
|
||||||
label="Text field"
|
label={_(msg`Text field`)}
|
||||||
style={{paddingRight: 60}}
|
style={{paddingRight: 60}}
|
||||||
numberOfLines={6}
|
numberOfLines={5}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.absolute,
|
a.absolute,
|
||||||
@@ -231,7 +271,115 @@ function SubmitStep({
|
|||||||
<ButtonText>
|
<ButtonText>
|
||||||
<Trans>Send report</Trans>
|
<Trans>Send report</Trans>
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
{submitting && <ButtonIcon icon={Loader} />}
|
<ButtonIcon icon={submitting ? Loader : SendIcon} />
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DoneStep({
|
||||||
|
convoId,
|
||||||
|
currentScreen,
|
||||||
|
profile,
|
||||||
|
}: {
|
||||||
|
convoId: string
|
||||||
|
currentScreen: 'list' | 'conversation'
|
||||||
|
profile: AppBskyActorDefs.ProfileViewBasic
|
||||||
|
}) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const navigation = useNavigation<NavigationProp>()
|
||||||
|
const control = Dialog.useDialogContext()
|
||||||
|
const {gtMobile} = useBreakpoints()
|
||||||
|
const t = useTheme()
|
||||||
|
const [actions, setActions] = useState<string[]>(['block', 'leave'])
|
||||||
|
const shadow = useProfileShadow(profile)
|
||||||
|
const [queueBlock] = useProfileBlockMutationQueue(shadow)
|
||||||
|
|
||||||
|
const {mutate: leaveConvo} = useLeaveConvo(convoId, {
|
||||||
|
onMutate: () => {
|
||||||
|
if (currentScreen === 'conversation') {
|
||||||
|
navigation.dispatch(
|
||||||
|
StackActions.replace('Messages', isNative ? {animation: 'pop'} : {}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
Toast.show(_(msg`Could not leave chat`), 'xmark')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const onPressPrimaryAction = () => {
|
||||||
|
control.close(() => {
|
||||||
|
if (actions.includes('block')) {
|
||||||
|
queueBlock()
|
||||||
|
}
|
||||||
|
if (actions.includes('leave')) {
|
||||||
|
leaveConvo()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let btnText = _(msg`Done`)
|
||||||
|
if (actions.includes('leave') && actions.includes('block')) {
|
||||||
|
btnText = _(msg`Block and Delete`)
|
||||||
|
} else if (actions.includes('leave')) {
|
||||||
|
btnText = _(msg`Delete Conversation`)
|
||||||
|
} else if (actions.includes('block')) {
|
||||||
|
btnText = _(msg`Block User`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={a.gap_2xl}>
|
||||||
|
<View style={[a.justify_center, gtMobile ? a.gap_sm : a.gap_xs]}>
|
||||||
|
<Text style={[a.text_2xl, a.font_bold]}>
|
||||||
|
<Trans>Report submitted</Trans>
|
||||||
|
</Text>
|
||||||
|
<Text style={[a.text_md, t.atoms.text_contrast_medium]}>
|
||||||
|
<Trans>Our moderation team has recieved your report.</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Toggle.Group
|
||||||
|
label={_(msg`Block and/or delete this conversation`)}
|
||||||
|
values={actions}
|
||||||
|
onChange={setActions}>
|
||||||
|
<View style={[a.gap_md]}>
|
||||||
|
<Toggle.Item name="block" label={_(msg`Block user`)}>
|
||||||
|
<Toggle.Checkbox />
|
||||||
|
<Toggle.LabelText style={[a.text_md]}>
|
||||||
|
<Trans>Block user</Trans>
|
||||||
|
</Toggle.LabelText>
|
||||||
|
</Toggle.Item>
|
||||||
|
<Toggle.Item name="leave" label={_(msg`Delete conversation`)}>
|
||||||
|
<Toggle.Checkbox />
|
||||||
|
<Toggle.LabelText style={[a.text_md]}>
|
||||||
|
<Trans>Delete conversation</Trans>
|
||||||
|
</Toggle.LabelText>
|
||||||
|
</Toggle.Item>
|
||||||
|
</View>
|
||||||
|
</Toggle.Group>
|
||||||
|
|
||||||
|
<View style={[a.gap_md, web([a.flex_row_reverse])]}>
|
||||||
|
<Button
|
||||||
|
label={btnText}
|
||||||
|
onPress={onPressPrimaryAction}
|
||||||
|
size="large"
|
||||||
|
variant="solid"
|
||||||
|
color={actions.length > 0 ? 'negative' : 'primary'}>
|
||||||
|
<ButtonText>{btnText}</ButtonText>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
label={_(msg`Close`)}
|
||||||
|
onPress={() => control.close()}
|
||||||
|
size={platform({native: 'small', web: 'large'})}
|
||||||
|
variant={platform({
|
||||||
|
native: 'ghost',
|
||||||
|
web: 'solid',
|
||||||
|
})}
|
||||||
|
color="secondary">
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Close</Trans>
|
||||||
|
</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import {View} from 'react-native'
|
||||||
|
import {AppBskyEmbedVideo} from '@atproto/api'
|
||||||
|
|
||||||
|
import {logEvent} from '#/lib/statsig/statsig'
|
||||||
|
import {FeedPostSliceItem} from '#/state/queries/post-feed'
|
||||||
|
import {VideoFeedSourceContext} from '#/screens/VideoFeed/types'
|
||||||
|
import {atoms as a, useGutters} from '#/alf'
|
||||||
|
import * as Grid from '#/components/Grid'
|
||||||
|
import {
|
||||||
|
VideoPostCard,
|
||||||
|
VideoPostCardPlaceholder,
|
||||||
|
} from '#/components/VideoPostCard'
|
||||||
|
|
||||||
|
export function PostFeedVideoGridRow({
|
||||||
|
items: slices,
|
||||||
|
sourceContext,
|
||||||
|
}: {
|
||||||
|
items: FeedPostSliceItem[]
|
||||||
|
sourceContext: VideoFeedSourceContext
|
||||||
|
}) {
|
||||||
|
const gutters = useGutters(['base', 'base', 0, 'base'])
|
||||||
|
const posts = slices
|
||||||
|
.filter(slice => AppBskyEmbedVideo.isView(slice.post.embed))
|
||||||
|
.map(slice => ({
|
||||||
|
post: slice.post,
|
||||||
|
moderation: slice.moderation,
|
||||||
|
}))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This should not happen because we should be filtering out posts without
|
||||||
|
* videos within the `PostFeed` component.
|
||||||
|
*/
|
||||||
|
if (posts.length !== slices.length) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[gutters]}>
|
||||||
|
<View style={[a.flex_row, a.gap_sm]}>
|
||||||
|
<Grid.Row gap={a.gap_sm.gap}>
|
||||||
|
{posts.map(post => (
|
||||||
|
<Grid.Col key={post.post.uri} width={1 / 2}>
|
||||||
|
<VideoPostCard
|
||||||
|
post={post.post}
|
||||||
|
sourceContext={sourceContext}
|
||||||
|
moderation={post.moderation}
|
||||||
|
onInteract={() => {
|
||||||
|
logEvent('videoCard:click', {context: 'feed'})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
))}
|
||||||
|
</Grid.Row>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PostFeedVideoGridRowPlaceholder() {
|
||||||
|
const gutters = useGutters(['base', 'base', 0, 'base'])
|
||||||
|
return (
|
||||||
|
<View style={[gutters]}>
|
||||||
|
<View style={[a.flex_row, a.gap_sm]}>
|
||||||
|
<VideoPostCardPlaceholder />
|
||||||
|
<VideoPostCardPlaceholder />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -11,10 +11,12 @@ import React from 'react'
|
|||||||
import {formatDistance, Locale} from 'date-fns'
|
import {formatDistance, Locale} from 'date-fns'
|
||||||
import {
|
import {
|
||||||
ca,
|
ca,
|
||||||
|
da,
|
||||||
de,
|
de,
|
||||||
el,
|
el,
|
||||||
enGB,
|
enGB,
|
||||||
es,
|
es,
|
||||||
|
eu,
|
||||||
fi,
|
fi,
|
||||||
fr,
|
fr,
|
||||||
gl,
|
gl,
|
||||||
@@ -30,6 +32,7 @@ import {
|
|||||||
ptBR,
|
ptBR,
|
||||||
ro,
|
ro,
|
||||||
ru,
|
ru,
|
||||||
|
sv,
|
||||||
th,
|
th,
|
||||||
tr,
|
tr,
|
||||||
uk,
|
uk,
|
||||||
@@ -50,10 +53,12 @@ const locales: Record<AppLanguage, Locale | undefined> = {
|
|||||||
an: undefined,
|
an: undefined,
|
||||||
ast: undefined,
|
ast: undefined,
|
||||||
ca,
|
ca,
|
||||||
|
da,
|
||||||
de,
|
de,
|
||||||
el,
|
el,
|
||||||
['en-GB']: enGB,
|
['en-GB']: enGB,
|
||||||
es,
|
es,
|
||||||
|
eu,
|
||||||
fi,
|
fi,
|
||||||
fr,
|
fr,
|
||||||
ga: undefined,
|
ga: undefined,
|
||||||
@@ -71,6 +76,7 @@ const locales: Record<AppLanguage, Locale | undefined> = {
|
|||||||
['pt-BR']: ptBR,
|
['pt-BR']: ptBR,
|
||||||
ro,
|
ro,
|
||||||
ru,
|
ru,
|
||||||
|
sv,
|
||||||
th,
|
th,
|
||||||
tr,
|
tr,
|
||||||
uk,
|
uk,
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import React, {useContext} from 'react'
|
import React from 'react'
|
||||||
import {ScrollView, View} from 'react-native'
|
import {ScrollView, View} from 'react-native'
|
||||||
import {DrawerGestureContext} from 'react-native-drawer-layout'
|
|
||||||
import {Gesture, GestureDetector} from 'react-native-gesture-handler'
|
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
@@ -13,6 +11,7 @@ import {
|
|||||||
import {useTrendingTopics} from '#/state/queries/trending/useTrendingTopics'
|
import {useTrendingTopics} from '#/state/queries/trending/useTrendingTopics'
|
||||||
import {useTrendingConfig} from '#/state/trending-config'
|
import {useTrendingConfig} from '#/state/trending-config'
|
||||||
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||||
|
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
|
||||||
import {atoms as a, useGutters, useTheme} from '#/alf'
|
import {atoms as a, useGutters, useTheme} from '#/alf'
|
||||||
import {Button, ButtonIcon} from '#/components/Button'
|
import {Button, ButtonIcon} from '#/components/Button'
|
||||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||||
@@ -41,13 +40,9 @@ export function Inner() {
|
|||||||
setTrendingDisabled(true)
|
setTrendingDisabled(true)
|
||||||
}, [setTrendingDisabled])
|
}, [setTrendingDisabled])
|
||||||
|
|
||||||
const drawerGesture = useContext(DrawerGestureContext) ?? Gesture.Native() // noop for web
|
|
||||||
const trendingScrollGesture =
|
|
||||||
Gesture.Native().blocksExternalGesture(drawerGesture)
|
|
||||||
|
|
||||||
return error || noTopics ? null : (
|
return error || noTopics ? null : (
|
||||||
<View style={[t.atoms.border_contrast_low, a.border_t]}>
|
<View style={[t.atoms.border_contrast_low, a.border_t]}>
|
||||||
<GestureDetector gesture={trendingScrollGesture}>
|
<BlockDrawerGesture>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
horizontal
|
horizontal
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
@@ -127,7 +122,7 @@ export function Inner() {
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</GestureDetector>
|
</BlockDrawerGesture>
|
||||||
|
|
||||||
<Prompt.Basic
|
<Prompt.Basic
|
||||||
control={trendingPrompt}
|
control={trendingPrompt}
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
import React, {useEffect} from 'react'
|
||||||
|
import {ScrollView, View} from 'react-native'
|
||||||
|
import {AppBskyEmbedVideo, AtUri} from '@atproto/api'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {VIDEO_FEED_URI} from '#/lib/constants'
|
||||||
|
import {makeCustomFeedLink} from '#/lib/routes/links'
|
||||||
|
import {logEvent} from '#/lib/statsig/statsig'
|
||||||
|
import {useTrendingSettingsApi} from '#/state/preferences/trending'
|
||||||
|
import {usePostFeedQuery} from '#/state/queries/post-feed'
|
||||||
|
import {RQKEY} from '#/state/queries/post-feed'
|
||||||
|
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
|
||||||
|
import {atoms as a, useGutters, useTheme} from '#/alf'
|
||||||
|
import {Button, ButtonIcon} from '#/components/Button'
|
||||||
|
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
||||||
|
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||||
|
import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending2'
|
||||||
|
import {Link} from '#/components/Link'
|
||||||
|
import * as Prompt from '#/components/Prompt'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
import {
|
||||||
|
CompactVideoPostCard,
|
||||||
|
CompactVideoPostCardPlaceholder,
|
||||||
|
} from '#/components/VideoPostCard'
|
||||||
|
|
||||||
|
const CARD_WIDTH = 100
|
||||||
|
|
||||||
|
const FEED_DESC = `feedgen|${VIDEO_FEED_URI}`
|
||||||
|
const FEED_PARAMS: {
|
||||||
|
feedCacheKey: 'discover'
|
||||||
|
} = {
|
||||||
|
feedCacheKey: 'discover',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TrendingVideos() {
|
||||||
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
|
const gutters = useGutters([0, 'base'])
|
||||||
|
const {data, isLoading, error} = usePostFeedQuery(FEED_DESC, FEED_PARAMS)
|
||||||
|
|
||||||
|
// Refetch on unmount if nothing else is using this query.
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
const query = queryClient
|
||||||
|
.getQueryCache()
|
||||||
|
.find({queryKey: RQKEY(FEED_DESC, FEED_PARAMS)})
|
||||||
|
if (query && query.getObserversCount() <= 1) {
|
||||||
|
query.fetch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [queryClient])
|
||||||
|
|
||||||
|
const {setTrendingVideoDisabled} = useTrendingSettingsApi()
|
||||||
|
const trendingPrompt = Prompt.usePromptControl()
|
||||||
|
|
||||||
|
const onConfirmHide = React.useCallback(() => {
|
||||||
|
setTrendingVideoDisabled(true)
|
||||||
|
logEvent('trendingVideos:hide', {context: 'interstitial:discover'})
|
||||||
|
}, [setTrendingVideoDisabled])
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.pt_lg,
|
||||||
|
a.pb_lg,
|
||||||
|
a.border_t,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
gutters,
|
||||||
|
a.pb_sm,
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.justify_between,
|
||||||
|
]}>
|
||||||
|
<View style={[a.flex_1, a.flex_row, a.align_center, a.gap_xs]}>
|
||||||
|
<Graph />
|
||||||
|
<Text style={[a.text_md, a.font_bold, a.leading_snug]}>
|
||||||
|
<Trans>Trending Videos</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Button
|
||||||
|
label={_(msg`Dismiss this section`)}
|
||||||
|
size="tiny"
|
||||||
|
variant="ghost"
|
||||||
|
color="secondary"
|
||||||
|
shape="round"
|
||||||
|
onPress={() => trendingPrompt.open()}>
|
||||||
|
<ButtonIcon icon={X} />
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<BlockDrawerGesture>
|
||||||
|
<ScrollView
|
||||||
|
horizontal
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
decelerationRate="fast"
|
||||||
|
snapToInterval={CARD_WIDTH + a.gap_sm.gap}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_row,
|
||||||
|
a.gap_sm,
|
||||||
|
{
|
||||||
|
paddingLeft: gutters.paddingLeft,
|
||||||
|
paddingRight: gutters.paddingRight,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
{isLoading ? (
|
||||||
|
Array(10)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, i) => (
|
||||||
|
<View key={i} style={[{width: CARD_WIDTH}]}>
|
||||||
|
<CompactVideoPostCardPlaceholder />
|
||||||
|
</View>
|
||||||
|
))
|
||||||
|
) : error || !data ? (
|
||||||
|
<Text>
|
||||||
|
<Trans>Whoops! Trending videos failed to load.</Trans>
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<VideoCards data={data} />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</BlockDrawerGesture>
|
||||||
|
|
||||||
|
<Prompt.Basic
|
||||||
|
control={trendingPrompt}
|
||||||
|
title={_(msg`Hide trending videos?`)}
|
||||||
|
description={_(msg`You can update this later from your settings.`)}
|
||||||
|
confirmButtonCta={_(msg`Hide`)}
|
||||||
|
onConfirm={onConfirmHide}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function VideoCards({
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
data: Exclude<ReturnType<typeof usePostFeedQuery>['data'], undefined>
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
|
const items = React.useMemo(() => {
|
||||||
|
return data.pages
|
||||||
|
.flatMap(page => page.slices)
|
||||||
|
.map(slice => slice.items[0])
|
||||||
|
.filter(Boolean)
|
||||||
|
.filter(item => AppBskyEmbedVideo.isView(item.post.embed))
|
||||||
|
.slice(0, 8)
|
||||||
|
}, [data])
|
||||||
|
const href = React.useMemo(() => {
|
||||||
|
const urip = new AtUri(VIDEO_FEED_URI)
|
||||||
|
return makeCustomFeedLink(urip.host, urip.rkey, undefined, 'discover')
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{items.map(item => (
|
||||||
|
<View key={item.post.uri} style={[{width: CARD_WIDTH}]}>
|
||||||
|
<CompactVideoPostCard
|
||||||
|
post={item.post}
|
||||||
|
moderation={item.moderation}
|
||||||
|
sourceContext={{
|
||||||
|
type: 'feedgen',
|
||||||
|
uri: VIDEO_FEED_URI,
|
||||||
|
sourceInterstitial: 'discover',
|
||||||
|
}}
|
||||||
|
onInteract={() => {
|
||||||
|
logEvent('videoCard:click', {
|
||||||
|
context: 'interstitial:discover',
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<View style={[{width: CARD_WIDTH * 2}]}>
|
||||||
|
<Link
|
||||||
|
to={href}
|
||||||
|
label={_(msg`View more`)}
|
||||||
|
style={[
|
||||||
|
a.justify_center,
|
||||||
|
a.align_center,
|
||||||
|
a.flex_1,
|
||||||
|
a.rounded_md,
|
||||||
|
t.atoms.bg,
|
||||||
|
]}>
|
||||||
|
{({pressed}) => (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.gap_md,
|
||||||
|
{
|
||||||
|
opacity: pressed ? 0.6 : 1,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<Text style={[a.text_md]}>
|
||||||
|
<Trans>View more</Trans>
|
||||||
|
</Text>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.align_center,
|
||||||
|
a.justify_center,
|
||||||
|
a.rounded_full,
|
||||||
|
{
|
||||||
|
width: 34,
|
||||||
|
height: 34,
|
||||||
|
backgroundColor: t.palette.primary_500,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<ButtonIcon icon={ChevronRight} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -41,6 +41,7 @@ export class FeedViewPostsSlice {
|
|||||||
isFallbackMarker: boolean
|
isFallbackMarker: boolean
|
||||||
isOrphan: boolean
|
isOrphan: boolean
|
||||||
rootUri: string
|
rootUri: string
|
||||||
|
feedPostUri: string
|
||||||
|
|
||||||
constructor(feedPost: FeedViewPost) {
|
constructor(feedPost: FeedViewPost) {
|
||||||
const {post, reply, reason} = feedPost
|
const {post, reply, reason} = feedPost
|
||||||
@@ -48,6 +49,7 @@ export class FeedViewPostsSlice {
|
|||||||
this.isIncompleteThread = false
|
this.isIncompleteThread = false
|
||||||
this.isFallbackMarker = false
|
this.isFallbackMarker = false
|
||||||
this.isOrphan = false
|
this.isOrphan = false
|
||||||
|
this.feedPostUri = post.uri
|
||||||
if (AppBskyFeedDefs.isPostView(reply?.root)) {
|
if (AppBskyFeedDefs.isPostView(reply?.root)) {
|
||||||
this.rootUri = reply.root.uri
|
this.rootUri = reply.root.uri
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ export class AuthorFeedAPI implements FeedAPI {
|
|||||||
|
|
||||||
get params() {
|
get params() {
|
||||||
const params = {...this._params}
|
const params = {...this._params}
|
||||||
params.includePins = params.filter !== 'posts_with_media'
|
params.includePins =
|
||||||
|
params.filter === 'posts_with_replies' ||
|
||||||
|
params.filter === 'posts_and_author_threads'
|
||||||
return params
|
return params
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -193,7 +193,12 @@ export async function post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function resolveRT(agent: BskyAgent, richtext: RichText) {
|
async function resolveRT(agent: BskyAgent, richtext: RichText) {
|
||||||
let rt = new RichText({text: richtext.text.trimEnd()}, {cleanNewlines: true})
|
const trimmedText = richtext.text
|
||||||
|
// Trim leading whitespace-only lines (but don't break ASCII art).
|
||||||
|
.replace(/^(\s*\n)+/, '')
|
||||||
|
// Trim any trailing whitespace.
|
||||||
|
.trimEnd()
|
||||||
|
let rt = new RichText({text: trimmedText}, {cleanNewlines: true})
|
||||||
await rt.detectFacets(agent)
|
await rt.detectFacets(agent)
|
||||||
|
|
||||||
rt = shortenLinks(rt)
|
rt = shortenLinks(rt)
|
||||||
|
|||||||
+20
-24
@@ -1,27 +1,23 @@
|
|||||||
// import {init} from '@bitdrift/react-native'
|
import {init, SessionStrategy} from '@bitdrift/react-native'
|
||||||
// import {Statsig} from 'statsig-react-native-expo'
|
import {Statsig} from 'statsig-react-native-expo'
|
||||||
// export {debug, error, info, warn} from '@bitdrift/react-native'
|
export {debug, error, info, warn} from '@bitdrift/react-native'
|
||||||
|
|
||||||
// import {initPromise} from './statsig/statsig'
|
import {initPromise} from './statsig/statsig'
|
||||||
|
|
||||||
// const BITDRIFT_API_KEY = process.env.BITDRIFT_API_KEY
|
const BITDRIFT_API_KEY = process.env.BITDRIFT_API_KEY
|
||||||
|
|
||||||
// initPromise.then(() => {
|
initPromise.then(() => {
|
||||||
// let isEnabled = false
|
let isEnabled = false
|
||||||
// try {
|
try {
|
||||||
// if (Statsig.checkGate('enable_bitdrift')) {
|
if (Statsig.checkGate('enable_bitdrift')) {
|
||||||
// isEnabled = true
|
isEnabled = true
|
||||||
// }
|
}
|
||||||
// } catch (e) {
|
} catch (e) {
|
||||||
// // Statsig may complain about it being called too early.
|
// Statsig may complain about it being called too early.
|
||||||
// }
|
}
|
||||||
// if (isEnabled && BITDRIFT_API_KEY) {
|
if (isEnabled && BITDRIFT_API_KEY) {
|
||||||
// init(BITDRIFT_API_KEY, {url: 'https://api-bsky.bitdrift.io'})
|
init(BITDRIFT_API_KEY, SessionStrategy.Activity, {
|
||||||
// }
|
url: 'https://api-bsky.bitdrift.io',
|
||||||
// })
|
})
|
||||||
|
}
|
||||||
// TODO: Reenable when the build issue is fixed.
|
})
|
||||||
export function debug(_message: string) {}
|
|
||||||
export function error(_message: string) {}
|
|
||||||
export function info(_message: string) {}
|
|
||||||
export function warn(_message: string) {}
|
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ export const MAX_DM_GRAPHEME_LENGTH = 1000
|
|||||||
// but increasing limit per user feedback
|
// but increasing limit per user feedback
|
||||||
export const MAX_ALT_TEXT = 2000
|
export const MAX_ALT_TEXT = 2000
|
||||||
|
|
||||||
|
export const MAX_REPORT_REASON_GRAPHEME_LENGTH = 2000
|
||||||
|
|
||||||
export function IS_TEST_USER(handle?: string) {
|
export function IS_TEST_USER(handle?: string) {
|
||||||
return handle && handle?.endsWith('.test')
|
return handle && handle?.endsWith('.test')
|
||||||
}
|
}
|
||||||
@@ -63,6 +65,21 @@ export function IS_PROD_SERVICE(url?: string) {
|
|||||||
export const PROD_DEFAULT_FEED = (rkey: string) =>
|
export const PROD_DEFAULT_FEED = (rkey: string) =>
|
||||||
`at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/${rkey}`
|
`at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/${rkey}`
|
||||||
|
|
||||||
|
export const STAGING_DEFAULT_FEED = (rkey: string) =>
|
||||||
|
`at://did:plc:yofh3kx63drvfljkibw5zuxo/app.bsky.feed.generator/${rkey}`
|
||||||
|
|
||||||
|
export const PROD_FEEDS = [
|
||||||
|
`feedgen|${PROD_DEFAULT_FEED('whats-hot')}`,
|
||||||
|
`feedgen|${PROD_DEFAULT_FEED('thevids')}`,
|
||||||
|
]
|
||||||
|
|
||||||
|
export const STAGING_FEEDS = [
|
||||||
|
`feedgen|${STAGING_DEFAULT_FEED('whats-hot')}`,
|
||||||
|
`feedgen|${STAGING_DEFAULT_FEED('thevids')}`,
|
||||||
|
]
|
||||||
|
|
||||||
|
export const FEEDBACK_FEEDS = [...PROD_FEEDS, ...STAGING_FEEDS]
|
||||||
|
|
||||||
export const POST_IMG_MAX = {
|
export const POST_IMG_MAX = {
|
||||||
width: 2000,
|
width: 2000,
|
||||||
height: 2000,
|
height: 2000,
|
||||||
@@ -109,6 +126,11 @@ export const BSKY_FEED_OWNER_DIDS = [
|
|||||||
|
|
||||||
export const DISCOVER_FEED_URI =
|
export const DISCOVER_FEED_URI =
|
||||||
'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot'
|
'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot'
|
||||||
|
export const VIDEO_FEED_URI =
|
||||||
|
'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/thevids'
|
||||||
|
export const STAGING_VIDEO_FEED_URI =
|
||||||
|
'at://did:plc:yofh3kx63drvfljkibw5zuxo/app.bsky.feed.generator/thevids'
|
||||||
|
export const VIDEO_FEED_URIS = [VIDEO_FEED_URI, STAGING_VIDEO_FEED_URI]
|
||||||
export const DISCOVER_SAVED_FEED = {
|
export const DISCOVER_SAVED_FEED = {
|
||||||
type: 'feed',
|
type: 'feed',
|
||||||
value: DISCOVER_FEED_URI,
|
value: DISCOVER_FEED_URI,
|
||||||
@@ -119,6 +141,11 @@ export const TIMELINE_SAVED_FEED = {
|
|||||||
value: 'following',
|
value: 'following',
|
||||||
pinned: true,
|
pinned: true,
|
||||||
}
|
}
|
||||||
|
export const VIDEO_SAVED_FEED = {
|
||||||
|
type: 'feed',
|
||||||
|
value: VIDEO_FEED_URI,
|
||||||
|
pinned: true,
|
||||||
|
}
|
||||||
|
|
||||||
export const RECOMMENDED_SAVED_FEEDS: Pick<
|
export const RECOMMENDED_SAVED_FEEDS: Pick<
|
||||||
AppBskyActorDefs.SavedFeed,
|
AppBskyActorDefs.SavedFeed,
|
||||||
|
|||||||
@@ -19,9 +19,13 @@ export function makeProfileLink(
|
|||||||
export function makeCustomFeedLink(
|
export function makeCustomFeedLink(
|
||||||
did: string,
|
did: string,
|
||||||
rkey: string,
|
rkey: string,
|
||||||
...segments: string[]
|
segment?: string | undefined,
|
||||||
|
feedCacheKey?: 'discover' | 'explore' | undefined,
|
||||||
) {
|
) {
|
||||||
return [`/profile`, did, 'feed', rkey, ...segments].join('/')
|
return (
|
||||||
|
[`/profile`, did, 'feed', rkey, ...(segment ? [segment] : [])].join('/') +
|
||||||
|
(feedCacheKey ? `?feedCacheKey=${encodeURIComponent(feedCacheKey)}` : '')
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeListLink(did: string, rkey: string, ...segments: string[]) {
|
export function makeListLink(did: string, rkey: string, ...segments: string[]) {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import {NavigationState, PartialState} from '@react-navigation/native'
|
import {NavigationState, PartialState} from '@react-navigation/native'
|
||||||
import type {NativeStackNavigationProp} from '@react-navigation/native-stack'
|
import type {NativeStackNavigationProp} from '@react-navigation/native-stack'
|
||||||
|
|
||||||
|
import {VideoFeedSourceContext} from '#/screens/VideoFeed/types'
|
||||||
|
|
||||||
export type {NativeStackScreenProps} from '@react-navigation/native-stack'
|
export type {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||||
|
|
||||||
export type CommonNavigatorParams = {
|
export type CommonNavigatorParams = {
|
||||||
@@ -20,7 +22,11 @@ export type CommonNavigatorParams = {
|
|||||||
PostLikedBy: {name: string; rkey: string}
|
PostLikedBy: {name: string; rkey: string}
|
||||||
PostRepostedBy: {name: string; rkey: string}
|
PostRepostedBy: {name: string; rkey: string}
|
||||||
PostQuotes: {name: string; rkey: string}
|
PostQuotes: {name: string; rkey: string}
|
||||||
ProfileFeed: {name: string; rkey: string}
|
ProfileFeed: {
|
||||||
|
name: string
|
||||||
|
rkey: string
|
||||||
|
feedCacheKey?: 'discover' | 'explore' | undefined
|
||||||
|
}
|
||||||
ProfileFeedLikedBy: {name: string; rkey: string}
|
ProfileFeedLikedBy: {name: string; rkey: string}
|
||||||
ProfileLabelerLikedBy: {name: string}
|
ProfileLabelerLikedBy: {name: string}
|
||||||
Debug: undefined
|
Debug: undefined
|
||||||
@@ -57,6 +63,7 @@ export type CommonNavigatorParams = {
|
|||||||
StarterPackShort: {code: string}
|
StarterPackShort: {code: string}
|
||||||
StarterPackWizard: undefined
|
StarterPackWizard: undefined
|
||||||
StarterPackEdit: {rkey?: string}
|
StarterPackEdit: {rkey?: string}
|
||||||
|
VideoFeed: VideoFeedSourceContext
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BottomTabNavigatorParams = CommonNavigatorParams & {
|
export type BottomTabNavigatorParams = CommonNavigatorParams & {
|
||||||
|
|||||||
@@ -13,7 +13,12 @@ export type LogEvents = {
|
|||||||
withPassword: boolean
|
withPassword: boolean
|
||||||
}
|
}
|
||||||
'account:loggedOut': {
|
'account:loggedOut': {
|
||||||
logContext: 'SwitchAccount' | 'Settings' | 'SignupQueued' | 'Deactivated'
|
logContext:
|
||||||
|
| 'SwitchAccount'
|
||||||
|
| 'Settings'
|
||||||
|
| 'SignupQueued'
|
||||||
|
| 'Deactivated'
|
||||||
|
| 'Takendown'
|
||||||
scope: 'current' | 'every'
|
scope: 'current' | 'every'
|
||||||
}
|
}
|
||||||
'notifications:openApp': {}
|
'notifications:openApp': {}
|
||||||
@@ -131,16 +136,16 @@ export type LogEvents = {
|
|||||||
doesPosterFollowLiker: boolean | undefined
|
doesPosterFollowLiker: boolean | undefined
|
||||||
likerClout: number | undefined
|
likerClout: number | undefined
|
||||||
postClout: number | undefined
|
postClout: number | undefined
|
||||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
|
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||||
}
|
}
|
||||||
'post:repost': {
|
'post:repost': {
|
||||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
|
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||||
}
|
}
|
||||||
'post:unlike': {
|
'post:unlike': {
|
||||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
|
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||||
}
|
}
|
||||||
'post:unrepost': {
|
'post:unrepost': {
|
||||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
|
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||||
}
|
}
|
||||||
'post:mute': {}
|
'post:mute': {}
|
||||||
'post:unmute': {}
|
'post:unmute': {}
|
||||||
@@ -163,6 +168,7 @@ export type LogEvents = {
|
|||||||
| 'FeedInterstitial'
|
| 'FeedInterstitial'
|
||||||
| 'ProfileHeaderSuggestedFollows'
|
| 'ProfileHeaderSuggestedFollows'
|
||||||
| 'PostOnboardingFindFollows'
|
| 'PostOnboardingFindFollows'
|
||||||
|
| 'ImmersiveVideo'
|
||||||
}
|
}
|
||||||
'suggestedUser:follow': {
|
'suggestedUser:follow': {
|
||||||
logContext:
|
logContext:
|
||||||
@@ -199,6 +205,7 @@ export type LogEvents = {
|
|||||||
| 'FeedInterstitial'
|
| 'FeedInterstitial'
|
||||||
| 'ProfileHeaderSuggestedFollows'
|
| 'ProfileHeaderSuggestedFollows'
|
||||||
| 'PostOnboardingFindFollows'
|
| 'PostOnboardingFindFollows'
|
||||||
|
| 'ImmersiveVideo'
|
||||||
}
|
}
|
||||||
'chat:create': {
|
'chat:create': {
|
||||||
logContext: 'ProfileHeader' | 'NewChatDialog' | 'SendViaChatDialog'
|
logContext: 'ProfileHeader' | 'NewChatDialog' | 'SendViaChatDialog'
|
||||||
@@ -269,6 +276,15 @@ export type LogEvents = {
|
|||||||
'recommendedTopic:click': {
|
'recommendedTopic:click': {
|
||||||
context: 'explore'
|
context: 'explore'
|
||||||
}
|
}
|
||||||
|
'trendingVideos:show': {
|
||||||
|
context: 'settings'
|
||||||
|
}
|
||||||
|
'trendingVideos:hide': {
|
||||||
|
context: 'settings' | 'interstitial:discover' | 'interstitial:explore'
|
||||||
|
}
|
||||||
|
'videoCard:click': {
|
||||||
|
context: 'interstitial:discover' | 'interstitial:explore' | 'feed'
|
||||||
|
}
|
||||||
|
|
||||||
'progressGuide:hide': {}
|
'progressGuide:hide': {}
|
||||||
'progressGuide:followDialog:open': {}
|
'progressGuide:followDialog:open': {}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export type Gate =
|
|||||||
| 'debug_show_feedcontext'
|
| 'debug_show_feedcontext'
|
||||||
| 'debug_subscriptions'
|
| 'debug_subscriptions'
|
||||||
| 'new_postonboarding'
|
| 'new_postonboarding'
|
||||||
|
| 'onboarding_add_video_feed'
|
||||||
| 'remove_show_latest_button'
|
| 'remove_show_latest_button'
|
||||||
| 'test_gate_1'
|
| 'test_gate_1'
|
||||||
| 'test_gate_2'
|
| 'test_gate_2'
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import {sha256} from 'js-sha256'
|
|||||||
import {Statsig, StatsigProvider} from 'statsig-react-native-expo'
|
import {Statsig, StatsigProvider} from 'statsig-react-native-expo'
|
||||||
|
|
||||||
import {BUNDLE_DATE, BUNDLE_IDENTIFIER, IS_TESTFLIGHT} from '#/lib/app-info'
|
import {BUNDLE_DATE, BUNDLE_IDENTIFIER, IS_TESTFLIGHT} from '#/lib/app-info'
|
||||||
// TODO: Reenable when the build issue is fixed.
|
import * as bitdrift from '#/lib/bitdrift'
|
||||||
// import * as bitdrift from '#/lib/bitdrift'
|
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {isWeb} from '#/platform/detection'
|
import {isWeb} from '#/platform/detection'
|
||||||
import * as persisted from '#/state/persisted'
|
import * as persisted from '#/state/persisted'
|
||||||
@@ -108,8 +107,7 @@ export function logEvent<E extends keyof LogEvents>(
|
|||||||
console.groupCollapsed(eventName)
|
console.groupCollapsed(eventName)
|
||||||
console.log(fullMetadata)
|
console.log(fullMetadata)
|
||||||
console.groupEnd()
|
console.groupEnd()
|
||||||
// TODO: Reenable when the build issue is fixed.
|
bitdrift.info(eventName, fullMetadata)
|
||||||
// bitdrift.info(eventName, fullMetadata)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// A log should never interrupt the calling code, whatever happens.
|
// A log should never interrupt the calling code, whatever happens.
|
||||||
logger.error('Failed to log an event', {message: e})
|
logger.error('Failed to log an event', {message: e})
|
||||||
|
|||||||
@@ -161,6 +161,8 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
|
|||||||
return AppLanguage.ast
|
return AppLanguage.ast
|
||||||
case 'ca':
|
case 'ca':
|
||||||
return AppLanguage.ca
|
return AppLanguage.ca
|
||||||
|
case 'da':
|
||||||
|
return AppLanguage.da
|
||||||
case 'de':
|
case 'de':
|
||||||
return AppLanguage.de
|
return AppLanguage.de
|
||||||
case 'el':
|
case 'el':
|
||||||
@@ -169,6 +171,8 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
|
|||||||
return AppLanguage.en_GB
|
return AppLanguage.en_GB
|
||||||
case 'es':
|
case 'es':
|
||||||
return AppLanguage.es
|
return AppLanguage.es
|
||||||
|
case 'eu':
|
||||||
|
return AppLanguage.eu
|
||||||
case 'fi':
|
case 'fi':
|
||||||
return AppLanguage.fi
|
return AppLanguage.fi
|
||||||
case 'fr':
|
case 'fr':
|
||||||
@@ -203,6 +207,8 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
|
|||||||
return AppLanguage.ro
|
return AppLanguage.ro
|
||||||
case 'ru':
|
case 'ru':
|
||||||
return AppLanguage.ru
|
return AppLanguage.ru
|
||||||
|
case 'sv':
|
||||||
|
return AppLanguage.sv
|
||||||
case 'th':
|
case 'th':
|
||||||
return AppLanguage.th
|
return AppLanguage.th
|
||||||
case 'tr':
|
case 'tr':
|
||||||
|
|||||||
@@ -14,11 +14,13 @@ import {AppLanguage} from '#/locale/languages'
|
|||||||
import {messages as messagesAn} from '#/locale/locales/an/messages'
|
import {messages as messagesAn} from '#/locale/locales/an/messages'
|
||||||
import {messages as messagesAst} from '#/locale/locales/ast/messages'
|
import {messages as messagesAst} from '#/locale/locales/ast/messages'
|
||||||
import {messages as messagesCa} from '#/locale/locales/ca/messages'
|
import {messages as messagesCa} from '#/locale/locales/ca/messages'
|
||||||
|
import {messages as messagesDa} from '#/locale/locales/da/messages'
|
||||||
import {messages as messagesDe} from '#/locale/locales/de/messages'
|
import {messages as messagesDe} from '#/locale/locales/de/messages'
|
||||||
import {messages as messagesEl} from '#/locale/locales/el/messages'
|
import {messages as messagesEl} from '#/locale/locales/el/messages'
|
||||||
import {messages as messagesEn} from '#/locale/locales/en/messages'
|
import {messages as messagesEn} from '#/locale/locales/en/messages'
|
||||||
import {messages as messagesEn_GB} from '#/locale/locales/en-GB/messages'
|
import {messages as messagesEn_GB} from '#/locale/locales/en-GB/messages'
|
||||||
import {messages as messagesEs} from '#/locale/locales/es/messages'
|
import {messages as messagesEs} from '#/locale/locales/es/messages'
|
||||||
|
import {messages as messagesEu} from '#/locale/locales/eu/messages'
|
||||||
import {messages as messagesFi} from '#/locale/locales/fi/messages'
|
import {messages as messagesFi} from '#/locale/locales/fi/messages'
|
||||||
import {messages as messagesFr} from '#/locale/locales/fr/messages'
|
import {messages as messagesFr} from '#/locale/locales/fr/messages'
|
||||||
import {messages as messagesGa} from '#/locale/locales/ga/messages'
|
import {messages as messagesGa} from '#/locale/locales/ga/messages'
|
||||||
@@ -36,6 +38,7 @@ import {messages as messagesPl} from '#/locale/locales/pl/messages'
|
|||||||
import {messages as messagesPt_BR} from '#/locale/locales/pt-BR/messages'
|
import {messages as messagesPt_BR} from '#/locale/locales/pt-BR/messages'
|
||||||
import {messages as messagesRo} from '#/locale/locales/ro/messages'
|
import {messages as messagesRo} from '#/locale/locales/ro/messages'
|
||||||
import {messages as messagesRu} from '#/locale/locales/ru/messages'
|
import {messages as messagesRu} from '#/locale/locales/ru/messages'
|
||||||
|
import {messages as messagesSv} from '#/locale/locales/sv/messages'
|
||||||
import {messages as messagesTh} from '#/locale/locales/th/messages'
|
import {messages as messagesTh} from '#/locale/locales/th/messages'
|
||||||
import {messages as messagesTr} from '#/locale/locales/tr/messages'
|
import {messages as messagesTr} from '#/locale/locales/tr/messages'
|
||||||
import {messages as messagesUk} from '#/locale/locales/uk/messages'
|
import {messages as messagesUk} from '#/locale/locales/uk/messages'
|
||||||
@@ -74,6 +77,14 @@ export async function dynamicActivate(locale: AppLanguage) {
|
|||||||
])
|
])
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case AppLanguage.da: {
|
||||||
|
i18n.loadAndActivate({locale, messages: messagesDa})
|
||||||
|
await Promise.all([
|
||||||
|
import('@formatjs/intl-pluralrules/locale-data/da'),
|
||||||
|
import('@formatjs/intl-numberformat/locale-data/da'),
|
||||||
|
])
|
||||||
|
break
|
||||||
|
}
|
||||||
case AppLanguage.de: {
|
case AppLanguage.de: {
|
||||||
i18n.loadAndActivate({locale, messages: messagesDe})
|
i18n.loadAndActivate({locale, messages: messagesDe})
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@@ -106,6 +117,14 @@ export async function dynamicActivate(locale: AppLanguage) {
|
|||||||
])
|
])
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case AppLanguage.eu: {
|
||||||
|
i18n.loadAndActivate({locale, messages: messagesEu})
|
||||||
|
await Promise.all([
|
||||||
|
import('@formatjs/intl-pluralrules/locale-data/eu'),
|
||||||
|
import('@formatjs/intl-numberformat/locale-data/eu'),
|
||||||
|
])
|
||||||
|
break
|
||||||
|
}
|
||||||
case AppLanguage.fi: {
|
case AppLanguage.fi: {
|
||||||
i18n.loadAndActivate({locale, messages: messagesFi})
|
i18n.loadAndActivate({locale, messages: messagesFi})
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@@ -238,6 +257,14 @@ export async function dynamicActivate(locale: AppLanguage) {
|
|||||||
])
|
])
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case AppLanguage.sv: {
|
||||||
|
i18n.loadAndActivate({locale, messages: messagesSv})
|
||||||
|
await Promise.all([
|
||||||
|
import('@formatjs/intl-pluralrules/locale-data/sv'),
|
||||||
|
import('@formatjs/intl-numberformat/locale-data/sv'),
|
||||||
|
])
|
||||||
|
break
|
||||||
|
}
|
||||||
case AppLanguage.th: {
|
case AppLanguage.th: {
|
||||||
i18n.loadAndActivate({locale, messages: messagesTh})
|
i18n.loadAndActivate({locale, messages: messagesTh})
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ export async function dynamicActivate(locale: AppLanguage) {
|
|||||||
mod = await import(`./locales/ca/messages`)
|
mod = await import(`./locales/ca/messages`)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case AppLanguage.da: {
|
||||||
|
mod = await import(`./locales/da/messages`)
|
||||||
|
break
|
||||||
|
}
|
||||||
case AppLanguage.de: {
|
case AppLanguage.de: {
|
||||||
mod = await import(`./locales/de/messages`)
|
mod = await import(`./locales/de/messages`)
|
||||||
break
|
break
|
||||||
@@ -40,6 +44,10 @@ export async function dynamicActivate(locale: AppLanguage) {
|
|||||||
mod = await import(`./locales/es/messages`)
|
mod = await import(`./locales/es/messages`)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case AppLanguage.eu: {
|
||||||
|
mod = await import(`./locales/eu/messages`)
|
||||||
|
break
|
||||||
|
}
|
||||||
case AppLanguage.fi: {
|
case AppLanguage.fi: {
|
||||||
mod = await import(`./locales/fi/messages`)
|
mod = await import(`./locales/fi/messages`)
|
||||||
break
|
break
|
||||||
@@ -108,6 +116,10 @@ export async function dynamicActivate(locale: AppLanguage) {
|
|||||||
mod = await import(`./locales/ru/messages`)
|
mod = await import(`./locales/ru/messages`)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case AppLanguage.sv: {
|
||||||
|
mod = await import(`./locales/sv/messages`)
|
||||||
|
break
|
||||||
|
}
|
||||||
case AppLanguage.th: {
|
case AppLanguage.th: {
|
||||||
mod = await import(`./locales/th/messages`)
|
mod = await import(`./locales/th/messages`)
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ export enum AppLanguage {
|
|||||||
an = 'an',
|
an = 'an',
|
||||||
ast = 'ast',
|
ast = 'ast',
|
||||||
ca = 'ca',
|
ca = 'ca',
|
||||||
|
da = 'da',
|
||||||
de = 'de',
|
de = 'de',
|
||||||
el = 'el',
|
el = 'el',
|
||||||
en_GB = 'en-GB',
|
en_GB = 'en-GB',
|
||||||
es = 'es',
|
es = 'es',
|
||||||
|
eu = 'eu',
|
||||||
fi = 'fi',
|
fi = 'fi',
|
||||||
fr = 'fr',
|
fr = 'fr',
|
||||||
ga = 'ga',
|
ga = 'ga',
|
||||||
@@ -30,6 +32,7 @@ export enum AppLanguage {
|
|||||||
pt_BR = 'pt-BR',
|
pt_BR = 'pt-BR',
|
||||||
ro = 'ro',
|
ro = 'ro',
|
||||||
ru = 'ru',
|
ru = 'ru',
|
||||||
|
sv = 'sv',
|
||||||
th = 'th',
|
th = 'th',
|
||||||
tr = 'tr',
|
tr = 'tr',
|
||||||
uk = 'uk',
|
uk = 'uk',
|
||||||
@@ -49,10 +52,12 @@ export const APP_LANGUAGES: AppLanguageConfig[] = [
|
|||||||
{code2: AppLanguage.an, name: 'Aragonés – Aragonese'},
|
{code2: AppLanguage.an, name: 'Aragonés – Aragonese'},
|
||||||
{code2: AppLanguage.ast, name: 'Asturianu – Asturian'},
|
{code2: AppLanguage.ast, name: 'Asturianu – Asturian'},
|
||||||
{code2: AppLanguage.ca, name: 'Català – Catalan'},
|
{code2: AppLanguage.ca, name: 'Català – Catalan'},
|
||||||
|
{code2: AppLanguage.da, name: 'Dansk – Danish'},
|
||||||
{code2: AppLanguage.de, name: 'Deutsch – German'},
|
{code2: AppLanguage.de, name: 'Deutsch – German'},
|
||||||
{code2: AppLanguage.el, name: 'Ελληνικά – Greek'},
|
{code2: AppLanguage.el, name: 'Ελληνικά – Greek'},
|
||||||
{code2: AppLanguage.en_GB, name: 'English (UK)'},
|
{code2: AppLanguage.en_GB, name: 'English (UK)'},
|
||||||
{code2: AppLanguage.es, name: 'Español – Spanish'},
|
{code2: AppLanguage.es, name: 'Español – Spanish'},
|
||||||
|
{code2: AppLanguage.eu, name: 'Euskera – Basque'},
|
||||||
{code2: AppLanguage.fi, name: 'Suomi – Finnish'},
|
{code2: AppLanguage.fi, name: 'Suomi – Finnish'},
|
||||||
{code2: AppLanguage.fr, name: 'Français – French'},
|
{code2: AppLanguage.fr, name: 'Français – French'},
|
||||||
{code2: AppLanguage.ga, name: 'Gaeilge – Irish'},
|
{code2: AppLanguage.ga, name: 'Gaeilge – Irish'},
|
||||||
@@ -70,6 +75,7 @@ export const APP_LANGUAGES: AppLanguageConfig[] = [
|
|||||||
{code2: AppLanguage.pt_BR, name: 'Português (BR) – Portuguese (BR)'},
|
{code2: AppLanguage.pt_BR, name: 'Português (BR) – Portuguese (BR)'},
|
||||||
{code2: AppLanguage.ro, name: 'Română – Romanian'},
|
{code2: AppLanguage.ro, name: 'Română – Romanian'},
|
||||||
{code2: AppLanguage.ru, name: 'Русский – Russian'},
|
{code2: AppLanguage.ru, name: 'Русский – Russian'},
|
||||||
|
{code2: AppLanguage.sv, name: 'Svenska – Swedish'},
|
||||||
{code2: AppLanguage.th, name: 'ภาษาไทย – Thai'},
|
{code2: AppLanguage.th, name: 'ภาษาไทย – Thai'},
|
||||||
{code2: AppLanguage.tr, name: 'Türkçe – Turkish'},
|
{code2: AppLanguage.tr, name: 'Türkçe – Turkish'},
|
||||||
{code2: AppLanguage.uk, name: 'Українська – Ukrainian'},
|
{code2: AppLanguage.uk, name: 'Українська – Ukrainian'},
|
||||||
|
|||||||
@@ -1936,7 +1936,7 @@ msgstr "Tanca el diàleg"
|
|||||||
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:137
|
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:137
|
||||||
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:173
|
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:173
|
||||||
msgid "Close emoji picker"
|
msgid "Close emoji picker"
|
||||||
msgstr ""
|
msgstr "Tanca el selector d'emojis"
|
||||||
|
|
||||||
#: src/components/dialogs/GifSelect.tsx:169
|
#: src/components/dialogs/GifSelect.tsx:169
|
||||||
msgid "Close GIF dialog"
|
msgid "Close GIF dialog"
|
||||||
@@ -2429,7 +2429,7 @@ msgstr "Creat {0}"
|
|||||||
|
|
||||||
#: src/screens/List/ListHiddenScreen.tsx:127
|
#: src/screens/List/ListHiddenScreen.tsx:127
|
||||||
msgid "Creator has been blocked"
|
msgid "Creator has been blocked"
|
||||||
msgstr ""
|
msgstr "S'ha bloquejat al creador"
|
||||||
|
|
||||||
#: src/screens/Onboarding/index.tsx:26
|
#: src/screens/Onboarding/index.tsx:26
|
||||||
#: src/screens/Onboarding/state.ts:99
|
#: src/screens/Onboarding/state.ts:99
|
||||||
@@ -3049,7 +3049,7 @@ msgstr "Ensenyament"
|
|||||||
|
|
||||||
#: src/screens/List/ListHiddenScreen.tsx:141
|
#: src/screens/List/ListHiddenScreen.tsx:141
|
||||||
msgid "Either the creator of this list has blocked you or you have blocked the creator."
|
msgid "Either the creator of this list has blocked you or you have blocked the creator."
|
||||||
msgstr ""
|
msgstr "El creador d'aquesta llista t'ha bloquejat o l'has bloquejat tu."
|
||||||
|
|
||||||
#: src/screens/Settings/AccountSettings.tsx:60
|
#: src/screens/Settings/AccountSettings.tsx:60
|
||||||
#: src/screens/Signup/StepInfo/index.tsx:170
|
#: src/screens/Signup/StepInfo/index.tsx:170
|
||||||
@@ -4837,7 +4837,7 @@ msgstr "Llista per <0/>"
|
|||||||
|
|
||||||
#: src/view/com/profile/ProfileSubpageHeader.tsx:154
|
#: src/view/com/profile/ProfileSubpageHeader.tsx:154
|
||||||
msgid "List by you"
|
msgid "List by you"
|
||||||
msgstr ""
|
msgstr "Llista teva"
|
||||||
|
|
||||||
#: src/view/screens/ProfileList.tsx:455
|
#: src/view/screens/ProfileList.tsx:455
|
||||||
msgid "List deleted"
|
msgid "List deleted"
|
||||||
@@ -8675,7 +8675,7 @@ msgstr "Toca per canviar la icona de l'aplicació"
|
|||||||
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:138
|
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:138
|
||||||
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:174
|
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:174
|
||||||
msgid "Tap to close the emoji picker"
|
msgid "Tap to close the emoji picker"
|
||||||
msgstr ""
|
msgstr "Toca per a tancar el selector d'emojis"
|
||||||
|
|
||||||
#: src/components/ProgressGuide/Toast.tsx:156
|
#: src/components/ProgressGuide/Toast.tsx:156
|
||||||
msgid "Tap to dismiss"
|
msgid "Tap to dismiss"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+115
-115
@@ -1,6 +1,5 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr "POT-Creation-Date: 2023-12-22 01:56+0530\n"
|
||||||
"POT-Creation-Date: 2023-12-22 01:56+0530\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=utf-8\n"
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
@@ -9,13 +8,14 @@ msgstr ""
|
|||||||
"Project-Id-Version: \n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: \n"
|
"PO-Revision-Date: \n"
|
||||||
"Last-Translator: brodieavoult\n"
|
"Last-Translator: hogaza\n"
|
||||||
"Language-Team: brodieavoult\n"
|
"Language-Team: brodieavoult\n"
|
||||||
"Plural-Forms: \n"
|
"Plural-Forms:\n"
|
||||||
|
" \n"
|
||||||
|
|
||||||
#: src/components/ProgressGuide/FollowDialog.tsx:567
|
#: src/components/ProgressGuide/FollowDialog.tsx:567
|
||||||
msgid "(active)"
|
msgid "(active)"
|
||||||
msgstr ""
|
msgstr "(activo)"
|
||||||
|
|
||||||
#: src/screens/Messages/components/ChatListItem.tsx:130
|
#: src/screens/Messages/components/ChatListItem.tsx:130
|
||||||
msgid "(contains embedded content)"
|
msgid "(contains embedded content)"
|
||||||
@@ -37,11 +37,11 @@ msgstr "{0, plural, one {# día} other {# días}}"
|
|||||||
|
|
||||||
#: src/screens/Profile/ProfileFollowers.tsx:40
|
#: src/screens/Profile/ProfileFollowers.tsx:40
|
||||||
msgid "{0, plural, one {# follower} other {# followers}}"
|
msgid "{0, plural, one {# follower} other {# followers}}"
|
||||||
msgstr ""
|
msgstr "{0, plural, one {# seguidor} other {# seguidores}}"
|
||||||
|
|
||||||
#: src/screens/Profile/ProfileFollows.tsx:40
|
#: src/screens/Profile/ProfileFollows.tsx:40
|
||||||
msgid "{0, plural, one {# following} other {# following}}"
|
msgid "{0, plural, one {# following} other {# following}}"
|
||||||
msgstr ""
|
msgstr "{0, plural, one {# siguiendo} other {# siguiendo}}"
|
||||||
|
|
||||||
#: src/lib/hooks/useTimeAgo.ts:146
|
#: src/lib/hooks/useTimeAgo.ts:146
|
||||||
msgid "{0, plural, one {# hour} other {# hours}}"
|
msgid "{0, plural, one {# hour} other {# hours}}"
|
||||||
@@ -65,15 +65,15 @@ msgstr "{0, plural, one {# hora} other {# horas}}"
|
|||||||
|
|
||||||
#: src/components/moderation/LabelsOnMe.tsx:53
|
#: src/components/moderation/LabelsOnMe.tsx:53
|
||||||
msgid "{0, plural, one {# label has} other {# labels have}} been placed on this account"
|
msgid "{0, plural, one {# label has} other {# labels have}} been placed on this account"
|
||||||
msgstr ""
|
msgstr "Se ha marcado esta cuenta con {0, plural, one {# etiqueta} other {# etiquetas}}"
|
||||||
|
|
||||||
#: src/components/moderation/LabelsOnMe.tsx:62
|
#: src/components/moderation/LabelsOnMe.tsx:62
|
||||||
msgid "{0, plural, one {# label has} other {# labels have}} been placed on this content"
|
msgid "{0, plural, one {# label has} other {# labels have}} been placed on this content"
|
||||||
msgstr ""
|
msgstr "Se ha marcado este contenido con {0, plural, one {# etiqueta} other {# etiquetas}}"
|
||||||
|
|
||||||
#: src/screens/Post/PostLikedBy.tsx:41
|
#: src/screens/Post/PostLikedBy.tsx:41
|
||||||
msgid "{0, plural, one {# like} other {# likes}}"
|
msgid "{0, plural, one {# like} other {# likes}}"
|
||||||
msgstr ""
|
msgstr "{0, plural, one {# me gusta} other {# me gusta}}"
|
||||||
|
|
||||||
#: src/lib/hooks/useTimeAgo.ts:136
|
#: src/lib/hooks/useTimeAgo.ts:136
|
||||||
msgid "{0, plural, one {# minute} other {# minutes}}"
|
msgid "{0, plural, one {# minute} other {# minutes}}"
|
||||||
@@ -85,7 +85,7 @@ msgstr "{0, plural, one {# mes} other {# meses}}"
|
|||||||
|
|
||||||
#: src/screens/Post/PostQuotes.tsx:41
|
#: src/screens/Post/PostQuotes.tsx:41
|
||||||
msgid "{0, plural, one {# quote} other {# quotes}}"
|
msgid "{0, plural, one {# quote} other {# quotes}}"
|
||||||
msgstr ""
|
msgstr "{0, plural, one {# cita} other {# citas}}"
|
||||||
|
|
||||||
#: src/screens/Post/PostRepostedBy.tsx:41
|
#: src/screens/Post/PostRepostedBy.tsx:41
|
||||||
msgid "{0, plural, one {# repost} other {# reposts}}"
|
msgid "{0, plural, one {# repost} other {# reposts}}"
|
||||||
@@ -115,7 +115,7 @@ msgstr "{0, plural, one {siguiendo} other {siguiendo}}"
|
|||||||
|
|
||||||
#: src/view/com/post-thread/PostThreadItem.tsx:447
|
#: src/view/com/post-thread/PostThreadItem.tsx:447
|
||||||
msgid "{0, plural, one {like} other {likes}}"
|
msgid "{0, plural, one {like} other {likes}}"
|
||||||
msgstr "{0, plural, one {\"me gusta\"} other {\"me gusta\"}}"
|
msgstr "{0, plural, one {me gusta} other {me gusta}}"
|
||||||
|
|
||||||
#: src/components/FeedCard.tsx:213
|
#: src/components/FeedCard.tsx:213
|
||||||
#: src/view/com/feeds/FeedSourceCard.tsx:303
|
#: src/view/com/feeds/FeedSourceCard.tsx:303
|
||||||
@@ -264,11 +264,11 @@ msgstr "{firstAuthorLink} y <0>{additionalAuthorsCount, plural, one {{formattedA
|
|||||||
|
|
||||||
#: src/view/com/notifications/NotificationFeedItem.tsx:327
|
#: src/view/com/notifications/NotificationFeedItem.tsx:327
|
||||||
msgid "{firstAuthorLink} and <0>{additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}}</0> liked your custom feed"
|
msgid "{firstAuthorLink} and <0>{additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}}</0> liked your custom feed"
|
||||||
msgstr "{firstAuthorLink} y <0>{additionalAuthorsCount, plural, one {{formattedAuthorsCount} más} other {{formattedAuthorsCount} más}}</0> dieron \"me gusta\" a tu feed"
|
msgstr "A {firstAuthorLink} y <0>{additionalAuthorsCount, plural, one {{formattedAuthorsCount} más} other {{formattedAuthorsCount} más}}</0> les gusta tu feed"
|
||||||
|
|
||||||
#: src/view/com/notifications/NotificationFeedItem.tsx:223
|
#: src/view/com/notifications/NotificationFeedItem.tsx:223
|
||||||
msgid "{firstAuthorLink} and <0>{additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}}</0> liked your post"
|
msgid "{firstAuthorLink} and <0>{additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}}</0> liked your post"
|
||||||
msgstr "{firstAuthorLink} y <0>{additionalAuthorsCount, plural, one {{formattedAuthorsCount} más} other {{formattedAuthorsCount} más}}</0> dieron \"me gusta\" a tu publicación"
|
msgstr "A {firstAuthorLink} y <0>{additionalAuthorsCount, plural, one {{formattedAuthorsCount} más} other {{formattedAuthorsCount} más}}</0> les gusta tu publicación"
|
||||||
|
|
||||||
#: src/view/com/notifications/NotificationFeedItem.tsx:247
|
#: src/view/com/notifications/NotificationFeedItem.tsx:247
|
||||||
msgid "{firstAuthorLink} and <0>{additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}}</0> reposted your post"
|
msgid "{firstAuthorLink} and <0>{additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}}</0> reposted your post"
|
||||||
@@ -288,11 +288,11 @@ msgstr "{firstAuthorLink} te siguió de vuelta"
|
|||||||
|
|
||||||
#: src/view/com/notifications/NotificationFeedItem.tsx:339
|
#: src/view/com/notifications/NotificationFeedItem.tsx:339
|
||||||
msgid "{firstAuthorLink} liked your custom feed"
|
msgid "{firstAuthorLink} liked your custom feed"
|
||||||
msgstr "{firstAuthorLink} dio \"me gusta\" a tu feed personalizado"
|
msgstr "A {firstAuthorLink} le gusta tu feed personalizado"
|
||||||
|
|
||||||
#: src/view/com/notifications/NotificationFeedItem.tsx:235
|
#: src/view/com/notifications/NotificationFeedItem.tsx:235
|
||||||
msgid "{firstAuthorLink} liked your post"
|
msgid "{firstAuthorLink} liked your post"
|
||||||
msgstr "{firstAuthorLink} dio \"me gusta\" a tu publicación"
|
msgstr "A {firstAuthorLink} le gusta tu publicación"
|
||||||
|
|
||||||
#: src/view/com/notifications/NotificationFeedItem.tsx:259
|
#: src/view/com/notifications/NotificationFeedItem.tsx:259
|
||||||
msgid "{firstAuthorLink} reposted your post"
|
msgid "{firstAuthorLink} reposted your post"
|
||||||
@@ -308,11 +308,11 @@ msgstr "{firstAuthorName} y {formattedAuthorsCount} más te siguieron"
|
|||||||
|
|
||||||
#: src/view/com/notifications/NotificationFeedItem.tsx:320
|
#: src/view/com/notifications/NotificationFeedItem.tsx:320
|
||||||
msgid "{firstAuthorName} and {additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}} liked your custom feed"
|
msgid "{firstAuthorName} and {additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}} liked your custom feed"
|
||||||
msgstr "{firstAuthorName} y {formattedAuthorsCount} más dieron \"me gusta\" a tu feed personalizado"
|
msgstr "A {firstAuthorName} y {formattedAuthorsCount} más les gusta tu feed personalizado"
|
||||||
|
|
||||||
#: src/view/com/notifications/NotificationFeedItem.tsx:216
|
#: src/view/com/notifications/NotificationFeedItem.tsx:216
|
||||||
msgid "{firstAuthorName} and {additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}} liked your post"
|
msgid "{firstAuthorName} and {additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}} liked your post"
|
||||||
msgstr "{firstAuthorName} y {formattedAuthorsCount} más dieron \"me gusta\" a tu publicación"
|
msgstr "A {firstAuthorName} y {formattedAuthorsCount} les gusta tu publicación"
|
||||||
|
|
||||||
#: src/view/com/notifications/NotificationFeedItem.tsx:240
|
#: src/view/com/notifications/NotificationFeedItem.tsx:240
|
||||||
msgid "{firstAuthorName} and {additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}} reposted your post"
|
msgid "{firstAuthorName} and {additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}} reposted your post"
|
||||||
@@ -332,11 +332,11 @@ msgstr "{firstAuthorName} te siguió de vuelta"
|
|||||||
|
|
||||||
#: src/view/com/notifications/NotificationFeedItem.tsx:325
|
#: src/view/com/notifications/NotificationFeedItem.tsx:325
|
||||||
msgid "{firstAuthorName} liked your custom feed"
|
msgid "{firstAuthorName} liked your custom feed"
|
||||||
msgstr "{firstAuthorName} dio \"me gusta\" a tu feed personalizado"
|
msgstr "A {firstAuthorName} le gusta tu feed personalizado"
|
||||||
|
|
||||||
#: src/view/com/notifications/NotificationFeedItem.tsx:221
|
#: src/view/com/notifications/NotificationFeedItem.tsx:221
|
||||||
msgid "{firstAuthorName} liked your post"
|
msgid "{firstAuthorName} liked your post"
|
||||||
msgstr "{firstAuthorName} dio \"me gusta\" a tu publicación"
|
msgstr "A {firstAuthorName} le gusta tu publicación"
|
||||||
|
|
||||||
#: src/view/com/notifications/NotificationFeedItem.tsx:245
|
#: src/view/com/notifications/NotificationFeedItem.tsx:245
|
||||||
msgid "{firstAuthorName} reposted your post"
|
msgid "{firstAuthorName} reposted your post"
|
||||||
@@ -363,7 +363,7 @@ msgstr "No se puede enviar un mensaje a {handle}"
|
|||||||
|
|
||||||
#: src/view/shell/bottom-bar/BottomBarWeb.tsx:260
|
#: src/view/shell/bottom-bar/BottomBarWeb.tsx:260
|
||||||
msgid "{notificationCount} unread items"
|
msgid "{notificationCount} unread items"
|
||||||
msgstr ""
|
msgstr "{notificationCount} elementos sin leer"
|
||||||
|
|
||||||
#: src/view/shell/Drawer.tsx:448
|
#: src/view/shell/Drawer.tsx:448
|
||||||
msgid "{numUnreadNotifications} unread"
|
msgid "{numUnreadNotifications} unread"
|
||||||
@@ -532,16 +532,16 @@ msgstr "Cuenta seguida"
|
|||||||
|
|
||||||
#: src/view/com/profile/ProfileMenu.tsx:108
|
#: src/view/com/profile/ProfileMenu.tsx:108
|
||||||
msgid "Account muted"
|
msgid "Account muted"
|
||||||
msgstr "Cuenta muteada"
|
msgstr "Cuenta silenciada"
|
||||||
|
|
||||||
#: src/components/moderation/ModerationDetailsDialog.tsx:101
|
#: src/components/moderation/ModerationDetailsDialog.tsx:101
|
||||||
#: src/lib/moderation/useModerationCauseDescription.ts:96
|
#: src/lib/moderation/useModerationCauseDescription.ts:96
|
||||||
msgid "Account Muted"
|
msgid "Account Muted"
|
||||||
msgstr "Cuenta muteada"
|
msgstr "Cuenta silenciada"
|
||||||
|
|
||||||
#: src/components/moderation/ModerationDetailsDialog.tsx:87
|
#: src/components/moderation/ModerationDetailsDialog.tsx:87
|
||||||
msgid "Account Muted by List"
|
msgid "Account Muted by List"
|
||||||
msgstr "Cuenta muteada por lista"
|
msgstr "Cuenta silenciada por lista"
|
||||||
|
|
||||||
#: src/screens/Settings/Settings.tsx:428
|
#: src/screens/Settings/Settings.tsx:428
|
||||||
msgid "Account options"
|
msgid "Account options"
|
||||||
@@ -645,7 +645,7 @@ msgstr "Agregar nueva publicación"
|
|||||||
#: src/view/screens/ProfileList.tsx:903
|
#: src/view/screens/ProfileList.tsx:903
|
||||||
#: src/view/screens/ProfileList.tsx:921
|
#: src/view/screens/ProfileList.tsx:921
|
||||||
msgid "Add people"
|
msgid "Add people"
|
||||||
msgstr ""
|
msgstr "Añadir personas"
|
||||||
|
|
||||||
#: src/screens/StarterPack/Wizard/index.tsx:197
|
#: src/screens/StarterPack/Wizard/index.tsx:197
|
||||||
#~ msgid "Add people to your starter pack that you think others will enjoy following"
|
#~ msgid "Add people to your starter pack that you think others will enjoy following"
|
||||||
@@ -710,7 +710,7 @@ msgstr "El contenido para adultos solo se puede habilitar a través de la web en
|
|||||||
|
|
||||||
#: src/components/moderation/LabelPreference.tsx:242
|
#: src/components/moderation/LabelPreference.tsx:242
|
||||||
msgid "Adult content is disabled."
|
msgid "Adult content is disabled."
|
||||||
msgstr "El contenido adulto esta desactivado."
|
msgstr "El contenido adulto está desactivado."
|
||||||
|
|
||||||
#: src/view/com/composer/labels/LabelsBtn.tsx:139
|
#: src/view/com/composer/labels/LabelsBtn.tsx:139
|
||||||
#: src/view/com/composer/labels/LabelsBtn.tsx:197
|
#: src/view/com/composer/labels/LabelsBtn.tsx:197
|
||||||
@@ -727,7 +727,7 @@ msgstr "Avanzado"
|
|||||||
|
|
||||||
#: src/view/screens/Notifications.tsx:86
|
#: src/view/screens/Notifications.tsx:86
|
||||||
msgid "All"
|
msgid "All"
|
||||||
msgstr ""
|
msgstr "Todas"
|
||||||
|
|
||||||
#: src/screens/StarterPack/StarterPackScreen.tsx:381
|
#: src/screens/StarterPack/StarterPackScreen.tsx:381
|
||||||
msgid "All accounts have been followed!"
|
msgid "All accounts have been followed!"
|
||||||
@@ -754,7 +754,7 @@ msgstr "Permitir nuevos mensajes de"
|
|||||||
|
|
||||||
#: src/components/dialogs/PostInteractionSettingsDialog.tsx:314
|
#: src/components/dialogs/PostInteractionSettingsDialog.tsx:314
|
||||||
msgid "Allow quote posts"
|
msgid "Allow quote posts"
|
||||||
msgstr ""
|
msgstr "Permitir citas"
|
||||||
|
|
||||||
#: src/components/dialogs/PostInteractionSettingsDialog.tsx:359
|
#: src/components/dialogs/PostInteractionSettingsDialog.tsx:359
|
||||||
msgid "Allow replies from:"
|
msgid "Allow replies from:"
|
||||||
@@ -933,7 +933,7 @@ msgstr "Cualquiera puede interactuar"
|
|||||||
#: src/screens/Settings/AppIconSettings/SettingsListItem.tsx:18
|
#: src/screens/Settings/AppIconSettings/SettingsListItem.tsx:18
|
||||||
#: src/screens/Settings/AppIconSettings/SettingsListItem.tsx:23
|
#: src/screens/Settings/AppIconSettings/SettingsListItem.tsx:23
|
||||||
msgid "App Icon"
|
msgid "App Icon"
|
||||||
msgstr ""
|
msgstr "Icono de la app"
|
||||||
|
|
||||||
#: src/screens/Settings/LanguageSettings.tsx:80
|
#: src/screens/Settings/LanguageSettings.tsx:80
|
||||||
msgid "App Language"
|
msgid "App Language"
|
||||||
@@ -1165,7 +1165,7 @@ msgstr "Antes de contactar a otro usuario, debes verificar tu correo electrónic
|
|||||||
|
|
||||||
#: src/screens/Search/components/ExploreTrendingTopics.tsx:74
|
#: src/screens/Search/components/ExploreTrendingTopics.tsx:74
|
||||||
msgid "BETA"
|
msgid "BETA"
|
||||||
msgstr ""
|
msgstr "BETA"
|
||||||
|
|
||||||
#: src/components/dialogs/BirthDateSettings.tsx:106
|
#: src/components/dialogs/BirthDateSettings.tsx:106
|
||||||
#: src/screens/Settings/AccountSettings.tsx:109
|
#: src/screens/Settings/AccountSettings.tsx:109
|
||||||
@@ -1289,11 +1289,11 @@ msgstr "Bluesky no mostrará tu perfil o posts a usuarios que no hayan iniciado
|
|||||||
|
|
||||||
#: src/screens/Settings/AppIconSettings/index.tsx:99
|
#: src/screens/Settings/AppIconSettings/index.tsx:99
|
||||||
msgid "Bluesky+"
|
msgid "Bluesky+"
|
||||||
msgstr ""
|
msgstr "Bluesky+"
|
||||||
|
|
||||||
#: src/screens/Settings/AppIconSettings/index.tsx:102
|
#: src/screens/Settings/AppIconSettings/index.tsx:102
|
||||||
msgid "Bluesky+ icons"
|
msgid "Bluesky+ icons"
|
||||||
msgstr ""
|
msgstr "Iconos Bluesky+"
|
||||||
|
|
||||||
#: src/lib/moderation/useLabelBehaviorDescription.ts:53
|
#: src/lib/moderation/useLabelBehaviorDescription.ts:53
|
||||||
msgid "Blur images"
|
msgid "Blur images"
|
||||||
@@ -1339,15 +1339,15 @@ msgstr "Explorar otros feeds"
|
|||||||
|
|
||||||
#: src/components/TrendingTopics.tsx:176
|
#: src/components/TrendingTopics.tsx:176
|
||||||
msgid "Browse posts about {displayName}"
|
msgid "Browse posts about {displayName}"
|
||||||
msgstr ""
|
msgstr "Explorar posts sobre {displayName}"
|
||||||
|
|
||||||
#: src/components/TrendingTopics.tsx:184
|
#: src/components/TrendingTopics.tsx:184
|
||||||
msgid "Browse posts tagged with {displayName}"
|
msgid "Browse posts tagged with {displayName}"
|
||||||
msgstr ""
|
msgstr "Explorar posts etiquetados con {displayName}"
|
||||||
|
|
||||||
#: src/components/TrendingTopics.tsx:222
|
#: src/components/TrendingTopics.tsx:222
|
||||||
msgid "Browse topic {displayName}"
|
msgid "Browse topic {displayName}"
|
||||||
msgstr ""
|
msgstr "Explorar el tema {displayName}"
|
||||||
|
|
||||||
#: src/view/com/auth/SplashScreen.web.tsx:168
|
#: src/view/com/auth/SplashScreen.web.tsx:168
|
||||||
msgid "Business"
|
msgid "Business"
|
||||||
@@ -1371,7 +1371,7 @@ msgstr "Por {0}"
|
|||||||
|
|
||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:447
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:447
|
||||||
msgid "By <0>{0}</0>"
|
msgid "By <0>{0}</0>"
|
||||||
msgstr ""
|
msgstr "Por <0>{0}</0>"
|
||||||
|
|
||||||
#: src/screens/Signup/StepInfo/Policies.tsx:80
|
#: src/screens/Signup/StepInfo/Policies.tsx:80
|
||||||
#~ msgid "By creating an account you agree to the {els}."
|
#~ msgid "By creating an account you agree to the {els}."
|
||||||
@@ -1504,12 +1504,12 @@ msgstr "Cambiar"
|
|||||||
|
|
||||||
#: src/screens/Settings/AppIconSettings/index.tsx:39
|
#: src/screens/Settings/AppIconSettings/index.tsx:39
|
||||||
msgid "Change app icon"
|
msgid "Change app icon"
|
||||||
msgstr ""
|
msgstr "Cambiar icono de la app"
|
||||||
|
|
||||||
#: src/screens/Settings/AppIconSettings/index.tsx:38
|
#: src/screens/Settings/AppIconSettings/index.tsx:38
|
||||||
#: src/screens/Settings/AppIconSettings/index.tsx:221
|
#: src/screens/Settings/AppIconSettings/index.tsx:221
|
||||||
msgid "Change app icon to \"{0}\""
|
msgid "Change app icon to \"{0}\""
|
||||||
msgstr ""
|
msgstr "Cambiar icono de la app a \"{0}\""
|
||||||
|
|
||||||
#: src/screens/Settings/AccountSettings.tsx:97
|
#: src/screens/Settings/AccountSettings.tsx:97
|
||||||
#: src/screens/Settings/AccountSettings.tsx:101
|
#: src/screens/Settings/AccountSettings.tsx:101
|
||||||
@@ -1578,7 +1578,7 @@ msgstr "Configuración de chat"
|
|||||||
|
|
||||||
#: src/components/dms/ConvoMenu.tsx:84
|
#: src/components/dms/ConvoMenu.tsx:84
|
||||||
msgid "Chat unmuted"
|
msgid "Chat unmuted"
|
||||||
msgstr "Chat demuteado"
|
msgstr "Chat no silenciado"
|
||||||
|
|
||||||
#: src/screens/SignupQueued.tsx:78
|
#: src/screens/SignupQueued.tsx:78
|
||||||
#: src/screens/SignupQueued.tsx:82
|
#: src/screens/SignupQueued.tsx:82
|
||||||
@@ -1649,7 +1649,7 @@ msgstr "Elige este color como tu avatar"
|
|||||||
#: src/view/screens/Feeds.tsx:728
|
#: src/view/screens/Feeds.tsx:728
|
||||||
#: src/view/screens/Search/Explore.tsx:411
|
#: src/view/screens/Search/Explore.tsx:411
|
||||||
msgid "Choose your own timeline! Feeds built by the community help you find content you love."
|
msgid "Choose your own timeline! Feeds built by the community help you find content you love."
|
||||||
msgstr ""
|
msgstr "Elige tu propia cronología: encuentra el contenido que más te gusta gracias a los feeds de la comunidad."
|
||||||
|
|
||||||
#: src/screens/Signup/StepInfo/index.tsx:201
|
#: src/screens/Signup/StepInfo/index.tsx:201
|
||||||
msgid "Choose your password"
|
msgid "Choose your password"
|
||||||
@@ -1761,11 +1761,11 @@ msgstr "Cerrar ventana"
|
|||||||
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:137
|
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:137
|
||||||
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:173
|
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:173
|
||||||
msgid "Close emoji picker"
|
msgid "Close emoji picker"
|
||||||
msgstr ""
|
msgstr "Cerrar el selector de emoji"
|
||||||
|
|
||||||
#: src/components/dialogs/GifSelect.tsx:169
|
#: src/components/dialogs/GifSelect.tsx:169
|
||||||
msgid "Close GIF dialog"
|
msgid "Close GIF dialog"
|
||||||
msgstr "Cerrar ventana de GIF"
|
msgstr "Cerrar la ventana de GIF"
|
||||||
|
|
||||||
#: src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx:31
|
#: src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx:31
|
||||||
msgid "Close image"
|
msgid "Close image"
|
||||||
@@ -1934,7 +1934,7 @@ msgstr "Contactar a soporte"
|
|||||||
|
|
||||||
#: src/screens/Settings/ContentAndMediaSettings.tsx:49
|
#: src/screens/Settings/ContentAndMediaSettings.tsx:49
|
||||||
msgid "Content & Media"
|
msgid "Content & Media"
|
||||||
msgstr ""
|
msgstr "Contenido y medios"
|
||||||
|
|
||||||
#: src/screens/Settings/AccessibilitySettings.tsx:109
|
#: src/screens/Settings/AccessibilitySettings.tsx:109
|
||||||
#: src/screens/Settings/Settings.tsx:175
|
#: src/screens/Settings/Settings.tsx:175
|
||||||
@@ -2212,7 +2212,7 @@ msgstr "Creado {0}"
|
|||||||
|
|
||||||
#: src/screens/List/ListHiddenScreen.tsx:127
|
#: src/screens/List/ListHiddenScreen.tsx:127
|
||||||
msgid "Creator has been blocked"
|
msgid "Creator has been blocked"
|
||||||
msgstr ""
|
msgstr "Se ha bloqueado al creador"
|
||||||
|
|
||||||
#: src/screens/Onboarding/index.tsx:26
|
#: src/screens/Onboarding/index.tsx:26
|
||||||
#: src/screens/Onboarding/state.ts:99
|
#: src/screens/Onboarding/state.ts:99
|
||||||
@@ -2286,7 +2286,7 @@ msgstr "Por Defecto"
|
|||||||
|
|
||||||
#: src/screens/Settings/AppIconSettings/index.tsx:75
|
#: src/screens/Settings/AppIconSettings/index.tsx:75
|
||||||
msgid "Default icons"
|
msgid "Default icons"
|
||||||
msgstr ""
|
msgstr "Iconos por defecto"
|
||||||
|
|
||||||
#: src/components/dms/MessageMenu.tsx:151
|
#: src/components/dms/MessageMenu.tsx:151
|
||||||
#: src/screens/Settings/AppPasswords.tsx:212
|
#: src/screens/Settings/AppPasswords.tsx:212
|
||||||
@@ -2720,7 +2720,7 @@ msgstr "Editar los detalles de la lista"
|
|||||||
|
|
||||||
#: src/view/com/modals/CreateOrEditList.tsx:230
|
#: src/view/com/modals/CreateOrEditList.tsx:230
|
||||||
msgid "Edit Moderation List"
|
msgid "Edit Moderation List"
|
||||||
msgstr "Editar lista de Moderación"
|
msgstr "Editar lista de moderación"
|
||||||
|
|
||||||
#: src/Navigation.tsx:296
|
#: src/Navigation.tsx:296
|
||||||
#: src/view/screens/Feeds.tsx:515
|
#: src/view/screens/Feeds.tsx:515
|
||||||
@@ -2792,7 +2792,7 @@ msgstr "Educación"
|
|||||||
|
|
||||||
#: src/screens/List/ListHiddenScreen.tsx:141
|
#: src/screens/List/ListHiddenScreen.tsx:141
|
||||||
msgid "Either the creator of this list has blocked you or you have blocked the creator."
|
msgid "Either the creator of this list has blocked you or you have blocked the creator."
|
||||||
msgstr ""
|
msgstr "La persona que creó esta lista te ha bloqueado, o tú la has bloqueado a ella."
|
||||||
|
|
||||||
#: src/screens/Settings/AccountSettings.tsx:60
|
#: src/screens/Settings/AccountSettings.tsx:60
|
||||||
#: src/screens/Signup/StepInfo/index.tsx:170
|
#: src/screens/Signup/StepInfo/index.tsx:170
|
||||||
@@ -2910,7 +2910,7 @@ msgstr "Habilitar solo esta fuente"
|
|||||||
#: src/screens/Settings/ContentAndMediaSettings.tsx:122
|
#: src/screens/Settings/ContentAndMediaSettings.tsx:122
|
||||||
#: src/screens/Settings/ContentAndMediaSettings.tsx:136
|
#: src/screens/Settings/ContentAndMediaSettings.tsx:136
|
||||||
msgid "Enable trending topics"
|
msgid "Enable trending topics"
|
||||||
msgstr ""
|
msgstr "Activar las tendencias"
|
||||||
|
|
||||||
#: src/screens/Messages/Settings.tsx:130
|
#: src/screens/Messages/Settings.tsx:130
|
||||||
#: src/screens/Messages/Settings.tsx:133
|
#: src/screens/Messages/Settings.tsx:133
|
||||||
@@ -3261,7 +3261,7 @@ msgstr "Feed por {0}"
|
|||||||
|
|
||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:353
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:353
|
||||||
msgid "Feed menu"
|
msgid "Feed menu"
|
||||||
msgstr ""
|
msgstr "Menú de feed"
|
||||||
|
|
||||||
#: src/view/screens/Feeds.tsx:709
|
#: src/view/screens/Feeds.tsx:709
|
||||||
#~ msgid "Feed offline"
|
#~ msgid "Feed offline"
|
||||||
@@ -3308,7 +3308,7 @@ msgstr "¡Feeds actualizados!"
|
|||||||
|
|
||||||
#: src/screens/Search/components/ExploreRecommendations.tsx:54
|
#: src/screens/Search/components/ExploreRecommendations.tsx:54
|
||||||
msgid "Feeds we think you might like."
|
msgid "Feeds we think you might like."
|
||||||
msgstr ""
|
msgstr "Creemos que estos estos feeds te gustarán."
|
||||||
|
|
||||||
#: src/view/com/modals/ChangeHandle.tsx:468
|
#: src/view/com/modals/ChangeHandle.tsx:468
|
||||||
#~ msgid "File Contents"
|
#~ msgid "File Contents"
|
||||||
@@ -3340,7 +3340,7 @@ msgstr "Buscar cuentas para seguir"
|
|||||||
#: src/components/ProgressGuide/FollowDialog.tsx:88
|
#: src/components/ProgressGuide/FollowDialog.tsx:88
|
||||||
#: src/components/ProgressGuide/FollowDialog.tsx:417
|
#: src/components/ProgressGuide/FollowDialog.tsx:417
|
||||||
msgid "Find people to follow"
|
msgid "Find people to follow"
|
||||||
msgstr ""
|
msgstr "Buscar a gente para seguir"
|
||||||
|
|
||||||
#: src/view/screens/Search/Search.tsx:595
|
#: src/view/screens/Search/Search.tsx:595
|
||||||
msgid "Find posts and users on Bluesky"
|
msgid "Find posts and users on Bluesky"
|
||||||
@@ -3405,7 +3405,7 @@ msgstr "Seguir {name}"
|
|||||||
#: src/components/ProgressGuide/List.tsx:52
|
#: src/components/ProgressGuide/List.tsx:52
|
||||||
#: src/state/shell/progress-guide.tsx:229
|
#: src/state/shell/progress-guide.tsx:229
|
||||||
msgid "Follow 10 accounts"
|
msgid "Follow 10 accounts"
|
||||||
msgstr ""
|
msgstr "Sigue 10 cuentas"
|
||||||
|
|
||||||
#: src/components/ProgressGuide/List.tsx:69
|
#: src/components/ProgressGuide/List.tsx:69
|
||||||
msgid "Follow 7 accounts"
|
msgid "Follow 7 accounts"
|
||||||
@@ -3856,13 +3856,13 @@ msgstr "¿Ocultar esta respuesta?"
|
|||||||
#: src/screens/Search/components/ExploreTrendingTopics.tsx:83
|
#: src/screens/Search/components/ExploreTrendingTopics.tsx:83
|
||||||
#: src/view/shell/desktop/SidebarTrendingTopics.tsx:62
|
#: src/view/shell/desktop/SidebarTrendingTopics.tsx:62
|
||||||
msgid "Hide trending topics"
|
msgid "Hide trending topics"
|
||||||
msgstr ""
|
msgstr "Ocultar las tendencias"
|
||||||
|
|
||||||
#: src/components/interstitials/Trending.tsx:99
|
#: src/components/interstitials/Trending.tsx:99
|
||||||
#: src/screens/Search/components/ExploreTrendingTopics.tsx:135
|
#: src/screens/Search/components/ExploreTrendingTopics.tsx:135
|
||||||
#: src/view/shell/desktop/SidebarTrendingTopics.tsx:108
|
#: src/view/shell/desktop/SidebarTrendingTopics.tsx:108
|
||||||
msgid "Hide trending topics?"
|
msgid "Hide trending topics?"
|
||||||
msgstr ""
|
msgstr "¿Quieres ocultar las tendencias?"
|
||||||
|
|
||||||
#: src/view/com/notifications/NotificationFeedItem.tsx:592
|
#: src/view/com/notifications/NotificationFeedItem.tsx:592
|
||||||
msgid "Hide user list"
|
msgid "Hide user list"
|
||||||
@@ -4322,11 +4322,11 @@ msgstr "Claro"
|
|||||||
|
|
||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:514
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:514
|
||||||
msgid "Like"
|
msgid "Like"
|
||||||
msgstr ""
|
msgstr "Me gusta"
|
||||||
|
|
||||||
#: src/view/com/util/post-ctrls/PostCtrls.tsx:311
|
#: src/view/com/util/post-ctrls/PostCtrls.tsx:311
|
||||||
msgid "Like ({0, plural, one {# like} other {# likes}})"
|
msgid "Like ({0, plural, one {# like} other {# likes}})"
|
||||||
msgstr ""
|
msgstr "Like ({0, plural, one {# me gusta} other {# me gusta}})"
|
||||||
|
|
||||||
#: src/components/ProgressGuide/List.tsx:63
|
#: src/components/ProgressGuide/List.tsx:63
|
||||||
msgid "Like 10 posts"
|
msgid "Like 10 posts"
|
||||||
@@ -4339,7 +4339,7 @@ msgstr "Dale \"me gusta\" a 10 publicaciones para entrenar el feed de Descubrimi
|
|||||||
|
|
||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:501
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:501
|
||||||
msgid "Like feed"
|
msgid "Like feed"
|
||||||
msgstr ""
|
msgstr "Me gusta este feed"
|
||||||
|
|
||||||
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:275
|
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:275
|
||||||
msgid "Like this feed"
|
msgid "Like this feed"
|
||||||
@@ -4349,18 +4349,18 @@ msgstr "Dar \"me gusta\" a este feed"
|
|||||||
#: src/Navigation.tsx:236
|
#: src/Navigation.tsx:236
|
||||||
#: src/Navigation.tsx:241
|
#: src/Navigation.tsx:241
|
||||||
msgid "Liked by"
|
msgid "Liked by"
|
||||||
msgstr "Le ha gustado a"
|
msgstr "Le gusta a"
|
||||||
|
|
||||||
#: src/screens/Post/PostLikedBy.tsx:38
|
#: src/screens/Post/PostLikedBy.tsx:38
|
||||||
#: src/screens/Profile/ProfileLabelerLikedBy.tsx:29
|
#: src/screens/Profile/ProfileLabelerLikedBy.tsx:29
|
||||||
#: src/view/screens/ProfileFeedLikedBy.tsx:30
|
#: src/view/screens/ProfileFeedLikedBy.tsx:30
|
||||||
msgid "Liked By"
|
msgid "Liked By"
|
||||||
msgstr "Le ha gustado a"
|
msgstr "Le gusta a"
|
||||||
|
|
||||||
#: src/components/FeedCard.tsx:213
|
#: src/components/FeedCard.tsx:213
|
||||||
#: src/view/com/feeds/FeedSourceCard.tsx:303
|
#: src/view/com/feeds/FeedSourceCard.tsx:303
|
||||||
msgid "Liked by {0, plural, one {# user} other {# users}}"
|
msgid "Liked by {0, plural, one {# user} other {# users}}"
|
||||||
msgstr ""
|
msgstr "Le gusta a {0, plural, one {# usuario} other {# usuarios}}"
|
||||||
|
|
||||||
#: src/view/com/feeds/FeedSourceCard.tsx:268
|
#: src/view/com/feeds/FeedSourceCard.tsx:268
|
||||||
#~ msgid "Liked by {0} {1}"
|
#~ msgid "Liked by {0} {1}"
|
||||||
@@ -4375,7 +4375,7 @@ msgstr ""
|
|||||||
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:295
|
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:295
|
||||||
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:309
|
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:309
|
||||||
msgid "Liked by {likeCount, plural, one {# user} other {# users}}"
|
msgid "Liked by {likeCount, plural, one {# user} other {# users}}"
|
||||||
msgstr ""
|
msgstr "Le gusta a {likeCount, plural, one {# usuario} other {# usuarios}}"
|
||||||
|
|
||||||
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:287
|
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:287
|
||||||
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:301
|
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:301
|
||||||
@@ -4393,7 +4393,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: src/view/screens/Profile.tsx:234
|
#: src/view/screens/Profile.tsx:234
|
||||||
msgid "Likes"
|
msgid "Likes"
|
||||||
msgstr "\"Me gusta\""
|
msgstr "Me gusta"
|
||||||
|
|
||||||
#: src/view/com/post-thread/PostThreadItem.tsx:217
|
#: src/view/com/post-thread/PostThreadItem.tsx:217
|
||||||
msgid "Likes on this post"
|
msgid "Likes on this post"
|
||||||
@@ -4402,7 +4402,7 @@ msgstr "\"Me gusta\" en este post"
|
|||||||
#: src/view/com/post-thread/PostThread.tsx:626
|
#: src/view/com/post-thread/PostThread.tsx:626
|
||||||
#: src/view/com/post-thread/PostThread.tsx:631
|
#: src/view/com/post-thread/PostThread.tsx:631
|
||||||
msgid "Linear"
|
msgid "Linear"
|
||||||
msgstr ""
|
msgstr "Lineal"
|
||||||
|
|
||||||
#: src/Navigation.tsx:198
|
#: src/Navigation.tsx:198
|
||||||
msgid "List"
|
msgid "List"
|
||||||
@@ -4423,11 +4423,11 @@ msgstr "Lista por {0}"
|
|||||||
|
|
||||||
#: src/view/com/profile/ProfileSubpageHeader.tsx:156
|
#: src/view/com/profile/ProfileSubpageHeader.tsx:156
|
||||||
msgid "List by <0/>"
|
msgid "List by <0/>"
|
||||||
msgstr ""
|
msgstr "Lista de {0}"
|
||||||
|
|
||||||
#: src/view/com/profile/ProfileSubpageHeader.tsx:154
|
#: src/view/com/profile/ProfileSubpageHeader.tsx:154
|
||||||
msgid "List by you"
|
msgid "List by you"
|
||||||
msgstr ""
|
msgstr "Lista tuya"
|
||||||
|
|
||||||
#: src/view/screens/ProfileList.tsx:455
|
#: src/view/screens/ProfileList.tsx:455
|
||||||
msgid "List deleted"
|
msgid "List deleted"
|
||||||
@@ -4523,7 +4523,7 @@ msgstr "Acceder a una cuenta que no está en la lista"
|
|||||||
|
|
||||||
#: src/view/shell/desktop/RightNav.tsx:121
|
#: src/view/shell/desktop/RightNav.tsx:121
|
||||||
msgid "Logo by @sawaratsuki.bsky.social"
|
msgid "Logo by @sawaratsuki.bsky.social"
|
||||||
msgstr ""
|
msgstr "Logo de @sawaratsuki.bsky.social</0>"
|
||||||
|
|
||||||
#: src/view/shell/desktop/RightNav.tsx:103
|
#: src/view/shell/desktop/RightNav.tsx:103
|
||||||
#~ msgid "Logo by <0/>"
|
#~ msgid "Logo by <0/>"
|
||||||
@@ -4532,7 +4532,7 @@ msgstr ""
|
|||||||
#: src/view/shell/desktop/RightNav.tsx:118
|
#: src/view/shell/desktop/RightNav.tsx:118
|
||||||
#: src/view/shell/Drawer.tsx:629
|
#: src/view/shell/Drawer.tsx:629
|
||||||
msgid "Logo by <0>@sawaratsuki.bsky.social</0>"
|
msgid "Logo by <0>@sawaratsuki.bsky.social</0>"
|
||||||
msgstr "Logo por <0>@sawaratsuki.bsky.social</0>"
|
msgstr "Logo de <0>@sawaratsuki.bsky.social</0>"
|
||||||
|
|
||||||
#: src/components/RichText.tsx:219
|
#: src/components/RichText.tsx:219
|
||||||
msgid "Long press to open tag menu for #{tag}"
|
msgid "Long press to open tag menu for #{tag}"
|
||||||
@@ -4540,7 +4540,7 @@ msgstr "Mantén presionado para abrir el menú de etiquetas para #{tag}"
|
|||||||
|
|
||||||
#: src/screens/Login/SetNewPasswordForm.tsx:110
|
#: src/screens/Login/SetNewPasswordForm.tsx:110
|
||||||
msgid "Looks like XXXXX-XXXXX"
|
msgid "Looks like XXXXX-XXXXX"
|
||||||
msgstr "Es como XXXXX-XXXXX"
|
msgstr "Es algo así: XXXXX-XXXXX"
|
||||||
|
|
||||||
#: src/screens/Feeds/NoSavedFeedsOfAnyType.tsx:39
|
#: src/screens/Feeds/NoSavedFeedsOfAnyType.tsx:39
|
||||||
msgid "Looks like you haven't saved any feeds! Use our recommendations or browse more below."
|
msgid "Looks like you haven't saved any feeds! Use our recommendations or browse more below."
|
||||||
@@ -4598,7 +4598,7 @@ msgstr "Usuarios mencionados"
|
|||||||
|
|
||||||
#: src/view/screens/Notifications.tsx:99
|
#: src/view/screens/Notifications.tsx:99
|
||||||
msgid "Mentions"
|
msgid "Mentions"
|
||||||
msgstr ""
|
msgstr "Menciones"
|
||||||
|
|
||||||
#: src/components/Menu/index.tsx:96
|
#: src/components/Menu/index.tsx:96
|
||||||
#: src/view/screens/Search/Search.tsx:863
|
#: src/view/screens/Search/Search.tsx:863
|
||||||
@@ -4972,7 +4972,7 @@ msgstr "Nuevo nombre de usuario"
|
|||||||
#: src/view/screens/Lists.tsx:66
|
#: src/view/screens/Lists.tsx:66
|
||||||
#: src/view/screens/ModerationModlists.tsx:66
|
#: src/view/screens/ModerationModlists.tsx:66
|
||||||
msgid "New list"
|
msgid "New list"
|
||||||
msgstr ""
|
msgstr "Nueva lista"
|
||||||
|
|
||||||
#: src/components/dms/NewMessagesPill.tsx:92
|
#: src/components/dms/NewMessagesPill.tsx:92
|
||||||
msgid "New messages"
|
msgid "New messages"
|
||||||
@@ -5394,31 +5394,31 @@ msgstr "Abrir el creador de avatares"
|
|||||||
|
|
||||||
#: src/screens/Settings/AccountSettings.tsx:127
|
#: src/screens/Settings/AccountSettings.tsx:127
|
||||||
msgid "Open change handle dialog"
|
msgid "Open change handle dialog"
|
||||||
msgstr "Abrir diálogo de cambio de nombre de usuario"
|
msgstr "Abrir el diálogo de cambio de nombre de usuario"
|
||||||
|
|
||||||
#: src/screens/Messages/components/ChatListItem.tsx:272
|
#: src/screens/Messages/components/ChatListItem.tsx:272
|
||||||
#: src/screens/Messages/components/ChatListItem.tsx:273
|
#: src/screens/Messages/components/ChatListItem.tsx:273
|
||||||
msgid "Open conversation options"
|
msgid "Open conversation options"
|
||||||
msgstr "Abrir opciones de conversación"
|
msgstr "Abrir las opciones de conversación"
|
||||||
|
|
||||||
#: src/components/Layout/Header/index.tsx:145
|
#: src/components/Layout/Header/index.tsx:145
|
||||||
msgid "Open drawer menu"
|
msgid "Open drawer menu"
|
||||||
msgstr ""
|
msgstr "Abrir el menú lateral"
|
||||||
|
|
||||||
#: src/screens/Messages/components/MessageInput.web.tsx:181
|
#: src/screens/Messages/components/MessageInput.web.tsx:181
|
||||||
#: src/view/com/composer/Composer.tsx:1228
|
#: src/view/com/composer/Composer.tsx:1228
|
||||||
#: src/view/com/composer/Composer.tsx:1229
|
#: src/view/com/composer/Composer.tsx:1229
|
||||||
msgid "Open emoji picker"
|
msgid "Open emoji picker"
|
||||||
msgstr "Abrir selector de emoji"
|
msgstr "Abrir el selector de emoji"
|
||||||
|
|
||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:190
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:190
|
||||||
msgid "Open feed info screen"
|
msgid "Open feed info screen"
|
||||||
msgstr ""
|
msgstr "Abrir la pantalla de información del feed"
|
||||||
|
|
||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:290
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:290
|
||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:295
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:295
|
||||||
msgid "Open feed options menu"
|
msgid "Open feed options menu"
|
||||||
msgstr "Abrir menú de opciones del feed"
|
msgstr "Abrir el menú de opciones del feed"
|
||||||
|
|
||||||
#: src/screens/Settings/Settings.tsx:208
|
#: src/screens/Settings/Settings.tsx:208
|
||||||
msgid "Open helpdesk in browser"
|
msgid "Open helpdesk in browser"
|
||||||
@@ -5745,7 +5745,7 @@ msgstr "Imágenes destinadas a adultos."
|
|||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:519
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:519
|
||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:526
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:526
|
||||||
msgid "Pin feed"
|
msgid "Pin feed"
|
||||||
msgstr ""
|
msgstr "Anclar el feed"
|
||||||
|
|
||||||
#: src/view/screens/ProfileList.tsx:685
|
#: src/view/screens/ProfileList.tsx:685
|
||||||
msgid "Pin to home"
|
msgid "Pin to home"
|
||||||
@@ -5767,7 +5767,7 @@ msgstr "Anclado"
|
|||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:162
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:162
|
||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:174
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:174
|
||||||
msgid "Pinned {0} to Home"
|
msgid "Pinned {0} to Home"
|
||||||
msgstr ""
|
msgstr "{0} anclados en inicio"
|
||||||
|
|
||||||
#: src/view/screens/SavedFeeds.tsx:123
|
#: src/view/screens/SavedFeeds.tsx:123
|
||||||
msgid "Pinned Feeds"
|
msgid "Pinned Feeds"
|
||||||
@@ -6097,11 +6097,11 @@ msgstr "Público"
|
|||||||
|
|
||||||
#: src/view/com/lists/MyLists.tsx:77
|
#: src/view/com/lists/MyLists.tsx:77
|
||||||
msgid "Public, sharable lists of users to mute or block in bulk."
|
msgid "Public, sharable lists of users to mute or block in bulk."
|
||||||
msgstr ""
|
msgstr "Listas públicas y compartibles para bloquear o silenciar a usuarios en grupo."
|
||||||
|
|
||||||
#: src/view/com/lists/MyLists.tsx:72
|
#: src/view/com/lists/MyLists.tsx:72
|
||||||
msgid "Public, sharable lists which can be used to drive feeds."
|
msgid "Public, sharable lists which can be used to drive feeds."
|
||||||
msgstr ""
|
msgstr "Listas públicas y compartibles que se pueden usar para impulsar feeds."
|
||||||
|
|
||||||
#: src/view/screens/ModerationModlists.tsx:75
|
#: src/view/screens/ModerationModlists.tsx:75
|
||||||
#~ msgid "Public, shareable lists of users to mute or block in bulk."
|
#~ msgid "Public, shareable lists of users to mute or block in bulk."
|
||||||
@@ -6230,7 +6230,7 @@ msgstr "Búsquedas Recientes"
|
|||||||
|
|
||||||
#: src/screens/Search/components/ExploreRecommendations.tsx:50
|
#: src/screens/Search/components/ExploreRecommendations.tsx:50
|
||||||
msgid "Recommended"
|
msgid "Recommended"
|
||||||
msgstr ""
|
msgstr "Recomendaciones"
|
||||||
|
|
||||||
#: src/screens/Messages/components/MessageListError.tsx:20
|
#: src/screens/Messages/components/MessageListError.tsx:20
|
||||||
msgid "Reconnect"
|
msgid "Reconnect"
|
||||||
@@ -6426,7 +6426,7 @@ msgstr "Responder"
|
|||||||
|
|
||||||
#: src/view/com/util/post-ctrls/PostCtrls.tsx:263
|
#: src/view/com/util/post-ctrls/PostCtrls.tsx:263
|
||||||
msgid "Reply ({0, plural, one {# reply} other {# replies}})"
|
msgid "Reply ({0, plural, one {# reply} other {# replies}})"
|
||||||
msgstr ""
|
msgstr "Responder ({0, plural, one {# respuesta} other {# respuestas}})"
|
||||||
|
|
||||||
#: src/view/screens/PreferencesFollowingFeed.tsx:142
|
#: src/view/screens/PreferencesFollowingFeed.tsx:142
|
||||||
#~ msgid "Reply Filters"
|
#~ msgid "Reply Filters"
|
||||||
@@ -6452,7 +6452,7 @@ msgstr "La configuración de respuestas es elegida por el autor del hilo"
|
|||||||
|
|
||||||
#: src/view/com/post-thread/PostThread.tsx:648
|
#: src/view/com/post-thread/PostThread.tsx:648
|
||||||
msgid "Reply sorting"
|
msgid "Reply sorting"
|
||||||
msgstr ""
|
msgstr "Orden de las respuestas"
|
||||||
|
|
||||||
#: src/view/com/post/Post.tsx:204
|
#: src/view/com/post/Post.tsx:204
|
||||||
#: src/view/com/posts/PostFeedItem.tsx:553
|
#: src/view/com/posts/PostFeedItem.tsx:553
|
||||||
@@ -6576,7 +6576,7 @@ msgstr "Republicar"
|
|||||||
|
|
||||||
#: src/view/com/util/post-ctrls/RepostButton.tsx:74
|
#: src/view/com/util/post-ctrls/RepostButton.tsx:74
|
||||||
msgid "Repost ({0, plural, one {# repost} other {# reposts}})"
|
msgid "Repost ({0, plural, one {# repost} other {# reposts}})"
|
||||||
msgstr ""
|
msgstr "Republicar ({0, plural, one {# repost} other {# reposts}})"
|
||||||
|
|
||||||
#: src/screens/StarterPack/StarterPackScreen.tsx:548
|
#: src/screens/StarterPack/StarterPackScreen.tsx:548
|
||||||
#: src/view/com/util/post-ctrls/RepostButton.tsx:148
|
#: src/view/com/util/post-ctrls/RepostButton.tsx:148
|
||||||
@@ -6860,15 +6860,15 @@ msgstr "Buscar"
|
|||||||
|
|
||||||
#: src/components/ProgressGuide/FollowDialog.tsx:745
|
#: src/components/ProgressGuide/FollowDialog.tsx:745
|
||||||
msgid "Search by name or interest"
|
msgid "Search by name or interest"
|
||||||
msgstr ""
|
msgstr "Buscar por nombre o interés"
|
||||||
|
|
||||||
#: src/view/screens/Feeds.tsx:441
|
#: src/view/screens/Feeds.tsx:441
|
||||||
msgid "Search feeds"
|
msgid "Search feeds"
|
||||||
msgstr ""
|
msgstr "Buscar feeds"
|
||||||
|
|
||||||
#: src/components/ProgressGuide/FollowDialog.tsx:575
|
#: src/components/ProgressGuide/FollowDialog.tsx:575
|
||||||
msgid "Search for \"{interestsDisplayName}\"{activeText}"
|
msgid "Search for \"{interestsDisplayName}\"{activeText}"
|
||||||
msgstr ""
|
msgstr "Buscar \"{interestsDisplayName}\"{activeText}"
|
||||||
|
|
||||||
#: src/view/shell/desktop/Search.tsx:201
|
#: src/view/shell/desktop/Search.tsx:201
|
||||||
msgid "Search for \"{query}\""
|
msgid "Search for \"{query}\""
|
||||||
@@ -7148,7 +7148,7 @@ msgstr "Dirección del servidor"
|
|||||||
|
|
||||||
#: src/screens/Settings/AppIconSettings/index.tsx:178
|
#: src/screens/Settings/AppIconSettings/index.tsx:178
|
||||||
msgid "Set app icon to {0}"
|
msgid "Set app icon to {0}"
|
||||||
msgstr ""
|
msgstr "Cambiar el icono de la app a {0}"
|
||||||
|
|
||||||
#: src/screens/Moderation/index.tsx:290
|
#: src/screens/Moderation/index.tsx:290
|
||||||
msgid "Set birthdate"
|
msgid "Set birthdate"
|
||||||
@@ -7305,7 +7305,7 @@ msgstr "Compartir código QR"
|
|||||||
|
|
||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:470
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:470
|
||||||
msgid "Share this feed"
|
msgid "Share this feed"
|
||||||
msgstr ""
|
msgstr "Compartir este feed"
|
||||||
|
|
||||||
#: src/screens/StarterPack/StarterPackScreen.tsx:416
|
#: src/screens/StarterPack/StarterPackScreen.tsx:416
|
||||||
msgid "Share this starter pack"
|
msgid "Share this starter pack"
|
||||||
@@ -7436,11 +7436,11 @@ msgstr "Mostrar respuestas"
|
|||||||
|
|
||||||
#: src/view/com/post-thread/PostThread.tsx:622
|
#: src/view/com/post-thread/PostThread.tsx:622
|
||||||
msgid "Show replies as"
|
msgid "Show replies as"
|
||||||
msgstr ""
|
msgstr "Mostrar las respuestas como"
|
||||||
|
|
||||||
#: src/screens/Settings/ThreadPreferences.tsx:151
|
#: src/screens/Settings/ThreadPreferences.tsx:151
|
||||||
msgid "Show replies as threaded"
|
msgid "Show replies as threaded"
|
||||||
msgstr ""
|
msgstr "Mostrar las respuestas como hilos"
|
||||||
|
|
||||||
#: src/screens/Settings/ThreadPreferences.tsx:126
|
#: src/screens/Settings/ThreadPreferences.tsx:126
|
||||||
msgid "Show replies by people you follow before all other replies"
|
msgid "Show replies by people you follow before all other replies"
|
||||||
@@ -7570,7 +7570,7 @@ msgstr "¿Cerrar sesión?"
|
|||||||
#: src/view/shell/NavSignupCard.tsx:47
|
#: src/view/shell/NavSignupCard.tsx:47
|
||||||
#: src/view/shell/NavSignupCard.tsx:52
|
#: src/view/shell/NavSignupCard.tsx:52
|
||||||
msgid "Create account"
|
msgid "Create account"
|
||||||
msgstr "Crear cuenta"
|
msgstr "Crear una cuenta"
|
||||||
|
|
||||||
#: src/view/shell/NavSignupCard.tsx:47
|
#: src/view/shell/NavSignupCard.tsx:47
|
||||||
#~ msgid "Sign up or sign in to join the conversation"
|
#~ msgid "Sign up or sign in to join the conversation"
|
||||||
@@ -7655,7 +7655,7 @@ msgstr "¡Se produjo un error!"
|
|||||||
|
|
||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:538
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:538
|
||||||
msgid "Something wrong? Let us know."
|
msgid "Something wrong? Let us know."
|
||||||
msgstr ""
|
msgstr "¿Algo va mal? Avísanos."
|
||||||
|
|
||||||
#: src/App.native.tsx:122
|
#: src/App.native.tsx:122
|
||||||
#: src/App.web.tsx:97
|
#: src/App.web.tsx:97
|
||||||
@@ -7719,12 +7719,12 @@ msgstr "Iniciar un nuevo chat"
|
|||||||
#: src/view/screens/ProfileList.tsx:817
|
#: src/view/screens/ProfileList.tsx:817
|
||||||
#: src/view/screens/ProfileList.tsx:938
|
#: src/view/screens/ProfileList.tsx:938
|
||||||
msgid "Start adding people"
|
msgid "Start adding people"
|
||||||
msgstr ""
|
msgstr "Empieza a añadir personas"
|
||||||
|
|
||||||
#: src/view/screens/ProfileList.tsx:824
|
#: src/view/screens/ProfileList.tsx:824
|
||||||
#: src/view/screens/ProfileList.tsx:945
|
#: src/view/screens/ProfileList.tsx:945
|
||||||
msgid "Start adding people!"
|
msgid "Start adding people!"
|
||||||
msgstr ""
|
msgstr "¡Empieza a añadir personas!"
|
||||||
|
|
||||||
#: src/components/dms/dialogs/SearchablePeopleList.tsx:353
|
#: src/components/dms/dialogs/SearchablePeopleList.tsx:353
|
||||||
msgid "Start chat with {displayName}"
|
msgid "Start chat with {displayName}"
|
||||||
@@ -7750,7 +7750,7 @@ msgstr "Paquete de inicio de {0}"
|
|||||||
|
|
||||||
#: src/view/com/profile/ProfileSubpageHeader.tsx:182
|
#: src/view/com/profile/ProfileSubpageHeader.tsx:182
|
||||||
msgid "Starter pack by <0/>"
|
msgid "Starter pack by <0/>"
|
||||||
msgstr ""
|
msgstr "Paquete de inicio de <0/>"
|
||||||
|
|
||||||
#: src/components/StarterPack/StarterPackCard.tsx:80
|
#: src/components/StarterPack/StarterPackCard.tsx:80
|
||||||
#: src/view/com/profile/ProfileSubpageHeader.tsx:180
|
#: src/view/com/profile/ProfileSubpageHeader.tsx:180
|
||||||
@@ -7902,12 +7902,12 @@ msgstr "Solo etiquetas"
|
|||||||
|
|
||||||
#: src/screens/Settings/AppIconSettings/index.tsx:216
|
#: src/screens/Settings/AppIconSettings/index.tsx:216
|
||||||
msgid "Tap to change app icon"
|
msgid "Tap to change app icon"
|
||||||
msgstr ""
|
msgstr "Toca para cambiar el icono de la app"
|
||||||
|
|
||||||
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:138
|
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:138
|
||||||
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:174
|
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:174
|
||||||
msgid "Tap to close the emoji picker"
|
msgid "Tap to close the emoji picker"
|
||||||
msgstr ""
|
msgstr "Toca para cerrar el selector de emoji"
|
||||||
|
|
||||||
#: src/components/ProgressGuide/Toast.tsx:156
|
#: src/components/ProgressGuide/Toast.tsx:156
|
||||||
msgid "Tap to dismiss"
|
msgid "Tap to dismiss"
|
||||||
@@ -7936,11 +7936,11 @@ msgstr "Toca para ver la imagen completa"
|
|||||||
|
|
||||||
#: src/state/shell/progress-guide.tsx:233
|
#: src/state/shell/progress-guide.tsx:233
|
||||||
msgid "Task complete - 10 follows!"
|
msgid "Task complete - 10 follows!"
|
||||||
msgstr ""
|
msgstr "¡Tarea completada: 10 cuentas seguidas!"
|
||||||
|
|
||||||
#: src/state/shell/progress-guide.tsx:223
|
#: src/state/shell/progress-guide.tsx:223
|
||||||
msgid "Task complete - 10 likes!"
|
msgid "Task complete - 10 likes!"
|
||||||
msgstr "¡Tarea completada - 10 \"me gusta\"!"
|
msgstr "¡Tarea completada: 10 me gusta!"
|
||||||
|
|
||||||
#: src/components/ProgressGuide/List.tsx:64
|
#: src/components/ProgressGuide/List.tsx:64
|
||||||
msgid "Teach our algorithm what you like"
|
msgid "Teach our algorithm what you like"
|
||||||
@@ -8051,7 +8051,7 @@ msgstr "La cuenta podrá interactuar contigo tras desbloquearla."
|
|||||||
#: src/screens/Settings/AppIconSettings/index.tsx:41
|
#: src/screens/Settings/AppIconSettings/index.tsx:41
|
||||||
#: src/screens/Settings/AppIconSettings/index.tsx:222
|
#: src/screens/Settings/AppIconSettings/index.tsx:222
|
||||||
msgid "The app will be restarted"
|
msgid "The app will be restarted"
|
||||||
msgstr ""
|
msgstr "Se reiniciará la app"
|
||||||
|
|
||||||
#: src/components/moderation/ModerationDetailsDialog.tsx:127
|
#: src/components/moderation/ModerationDetailsDialog.tsx:127
|
||||||
#~ msgid "the author"
|
#~ msgid "the author"
|
||||||
@@ -8417,7 +8417,7 @@ msgstr "Esta lista - creada por <0>{0}</0> - contiene posibles violaciones de la
|
|||||||
|
|
||||||
#: src/view/screens/ProfileList.tsx:933
|
#: src/view/screens/ProfileList.tsx:933
|
||||||
msgid "This list is empty."
|
msgid "This list is empty."
|
||||||
msgstr ""
|
msgstr "Esta lista está vacía"
|
||||||
|
|
||||||
#: src/screens/Profile/ErrorState.tsx:40
|
#: src/screens/Profile/ErrorState.tsx:40
|
||||||
msgid "This moderation service is unavailable. See below for more details. If this issue persists, contact us."
|
msgid "This moderation service is unavailable. See below for more details. If this issue persists, contact us."
|
||||||
@@ -8524,7 +8524,7 @@ msgstr "Esto eliminará tu publicación de esta cita para todos los usuarios y l
|
|||||||
#: src/view/com/post-thread/PostThread.tsx:606
|
#: src/view/com/post-thread/PostThread.tsx:606
|
||||||
#: src/view/com/post-thread/PostThread.tsx:609
|
#: src/view/com/post-thread/PostThread.tsx:609
|
||||||
msgid "Thread options"
|
msgid "Thread options"
|
||||||
msgstr ""
|
msgstr "Opciones de hilos"
|
||||||
|
|
||||||
#: src/screens/Settings/ContentAndMediaSettings.tsx:66
|
#: src/screens/Settings/ContentAndMediaSettings.tsx:66
|
||||||
#: src/screens/Settings/ContentAndMediaSettings.tsx:69
|
#: src/screens/Settings/ContentAndMediaSettings.tsx:69
|
||||||
@@ -8542,7 +8542,7 @@ msgstr "Preferencias de hilos"
|
|||||||
#: src/view/com/post-thread/PostThread.tsx:636
|
#: src/view/com/post-thread/PostThread.tsx:636
|
||||||
#: src/view/com/post-thread/PostThread.tsx:641
|
#: src/view/com/post-thread/PostThread.tsx:641
|
||||||
msgid "Threaded"
|
msgid "Threaded"
|
||||||
msgstr ""
|
msgstr "En hilos"
|
||||||
|
|
||||||
#: src/screens/Settings/ThreadPreferences.tsx:142
|
#: src/screens/Settings/ThreadPreferences.tsx:142
|
||||||
msgid "Threaded mode"
|
msgid "Threaded mode"
|
||||||
@@ -8604,7 +8604,7 @@ msgstr "Top"
|
|||||||
|
|
||||||
#: src/Navigation.tsx:383
|
#: src/Navigation.tsx:383
|
||||||
msgid "Topic"
|
msgid "Topic"
|
||||||
msgstr ""
|
msgstr "Tema"
|
||||||
|
|
||||||
#: src/view/com/modals/EditImage.tsx:272
|
#: src/view/com/modals/EditImage.tsx:272
|
||||||
#~ msgid "Transformations"
|
#~ msgid "Transformations"
|
||||||
@@ -8622,7 +8622,7 @@ msgstr "Traducir"
|
|||||||
#: src/screens/Search/components/ExploreTrendingTopics.tsx:69
|
#: src/screens/Search/components/ExploreTrendingTopics.tsx:69
|
||||||
#: src/view/shell/desktop/SidebarTrendingTopics.tsx:59
|
#: src/view/shell/desktop/SidebarTrendingTopics.tsx:59
|
||||||
msgid "Trending"
|
msgid "Trending"
|
||||||
msgstr ""
|
msgstr "Tendencias"
|
||||||
|
|
||||||
#: src/view/com/util/error/ErrorScreen.tsx:103
|
#: src/view/com/util/error/ErrorScreen.tsx:103
|
||||||
msgctxt "action"
|
msgctxt "action"
|
||||||
@@ -8716,7 +8716,7 @@ msgstr "Deshacer repost"
|
|||||||
|
|
||||||
#: src/view/com/util/post-ctrls/RepostButton.tsx:68
|
#: src/view/com/util/post-ctrls/RepostButton.tsx:68
|
||||||
msgid "Undo repost ({0, plural, one {# repost} other {# reposts}})"
|
msgid "Undo repost ({0, plural, one {# repost} other {# reposts}})"
|
||||||
msgstr ""
|
msgstr "Deshacer republicación ({0, plural, one {# repost} other {# reposts}})"
|
||||||
|
|
||||||
#: src/view/com/profile/FollowButton.tsx:60
|
#: src/view/com/profile/FollowButton.tsx:60
|
||||||
msgctxt "action"
|
msgctxt "action"
|
||||||
@@ -8742,7 +8742,7 @@ msgstr "No me gusta"
|
|||||||
|
|
||||||
#: src/view/com/util/post-ctrls/PostCtrls.tsx:305
|
#: src/view/com/util/post-ctrls/PostCtrls.tsx:305
|
||||||
msgid "Unlike ({0, plural, one {# like} other {# likes}})"
|
msgid "Unlike ({0, plural, one {# like} other {# likes}})"
|
||||||
msgstr ""
|
msgstr "No me gusta ({0, plural, one {# me gusta} other {# me gusta}})"
|
||||||
|
|
||||||
#: src/view/screens/ProfileFeed.tsx:576
|
#: src/view/screens/ProfileFeed.tsx:576
|
||||||
#~ msgid "Unlike this feed"
|
#~ msgid "Unlike this feed"
|
||||||
@@ -8800,7 +8800,7 @@ msgstr "Desfijar"
|
|||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:519
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:519
|
||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:526
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:526
|
||||||
msgid "Unpin feed"
|
msgid "Unpin feed"
|
||||||
msgstr ""
|
msgstr "Desfijar feed"
|
||||||
|
|
||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:309
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:309
|
||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:311
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:311
|
||||||
@@ -8818,7 +8818,7 @@ msgstr "Desfijar lista de moderación"
|
|||||||
|
|
||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:164
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:164
|
||||||
msgid "Unpinned {0} from Home"
|
msgid "Unpinned {0} from Home"
|
||||||
msgstr ""
|
msgstr "{0} desfijado de inicio"
|
||||||
|
|
||||||
#: src/view/screens/ProfileList.tsx:352
|
#: src/view/screens/ProfileList.tsx:352
|
||||||
msgid "Unpinned from your feeds"
|
msgid "Unpinned from your feeds"
|
||||||
@@ -9284,7 +9284,7 @@ msgstr "No pudimos encontrar resultados para ese hashtag."
|
|||||||
|
|
||||||
#: src/screens/Topic.tsx:178
|
#: src/screens/Topic.tsx:178
|
||||||
msgid "We couldn't find any results for that topic."
|
msgid "We couldn't find any results for that topic."
|
||||||
msgstr ""
|
msgstr "No hemos encontrado resultados para ese tema."
|
||||||
|
|
||||||
#: src/screens/Messages/Conversation.tsx:103
|
#: src/screens/Messages/Conversation.tsx:103
|
||||||
msgid "We couldn't load this conversation"
|
msgid "We couldn't load this conversation"
|
||||||
@@ -9398,7 +9398,7 @@ msgstr "¿Cómo quieres llamar a tu paquete de inicio?"
|
|||||||
|
|
||||||
#: src/screens/Search/components/ExploreTrendingTopics.tsx:79
|
#: src/screens/Search/components/ExploreTrendingTopics.tsx:79
|
||||||
msgid "What people are posting about."
|
msgid "What people are posting about."
|
||||||
msgstr ""
|
msgstr "Los temas del momento."
|
||||||
|
|
||||||
#: src/view/com/auth/SplashScreen.tsx:38
|
#: src/view/com/auth/SplashScreen.tsx:38
|
||||||
#: src/view/com/auth/SplashScreen.web.tsx:99
|
#: src/view/com/auth/SplashScreen.web.tsx:99
|
||||||
@@ -9586,7 +9586,7 @@ msgstr "Puedes reactivar tu cuenta para continuar iniciando sesión. Tu perfil y
|
|||||||
#: src/screens/Search/components/ExploreTrendingTopics.tsx:136
|
#: src/screens/Search/components/ExploreTrendingTopics.tsx:136
|
||||||
#: src/view/shell/desktop/SidebarTrendingTopics.tsx:109
|
#: src/view/shell/desktop/SidebarTrendingTopics.tsx:109
|
||||||
msgid "You can update this later from your settings."
|
msgid "You can update this later from your settings."
|
||||||
msgstr ""
|
msgstr "Puedes cambiar esta opción en Ajustes."
|
||||||
|
|
||||||
#: src/view/com/profile/ProfileFollowers.tsx:94
|
#: src/view/com/profile/ProfileFollowers.tsx:94
|
||||||
msgid "You do not have any followers."
|
msgid "You do not have any followers."
|
||||||
@@ -9832,7 +9832,7 @@ msgstr "Has elegido ocultar una palabra o etiqueta dentro de esta publicación."
|
|||||||
|
|
||||||
#: src/state/shell/progress-guide.tsx:234
|
#: src/state/shell/progress-guide.tsx:234
|
||||||
msgid "You've found some people to follow"
|
msgid "You've found some people to follow"
|
||||||
msgstr ""
|
msgstr "Has encontrado algunas personas a las que seguir"
|
||||||
|
|
||||||
#: src/view/com/posts/FollowingEndOfFeed.tsx:44
|
#: src/view/com/posts/FollowingEndOfFeed.tsx:44
|
||||||
msgid "You've reached the end of your feed! Find some more accounts to follow."
|
msgid "You've reached the end of your feed! Find some more accounts to follow."
|
||||||
@@ -9880,7 +9880,7 @@ msgstr "Tu elección será guardada. Puedes cambiar esto en los ajustes luego."
|
|||||||
|
|
||||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:496
|
#: src/screens/Settings/components/ChangeHandleDialog.tsx:496
|
||||||
msgid "Your current handle <0>{0}</0> will automatically remain reserved for you. You can switch back to it at any time from this account."
|
msgid "Your current handle <0>{0}</0> will automatically remain reserved for you. You can switch back to it at any time from this account."
|
||||||
msgstr ""
|
msgstr "Tu nombre de usuario actual, <0>{0}</0>, quedará automáticamente reservado para ti. Puedes volver a él cuando quieras desde esta cuenta."
|
||||||
|
|
||||||
#: src/screens/Onboarding/StepFollowingFeed.tsx:62
|
#: src/screens/Onboarding/StepFollowingFeed.tsx:62
|
||||||
#~ msgid "Your default feed is \"Following\""
|
#~ msgid "Your default feed is \"Following\""
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1437,7 +1437,7 @@ msgstr "Sulje valintaikkuna"
|
|||||||
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:137
|
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:137
|
||||||
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:173
|
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:173
|
||||||
msgid "Close emoji picker"
|
msgid "Close emoji picker"
|
||||||
msgstr ""
|
msgstr "Sulje emojinvalitsin"
|
||||||
|
|
||||||
#: src/components/dialogs/GifSelect.tsx:169
|
#: src/components/dialogs/GifSelect.tsx:169
|
||||||
msgid "Close GIF dialog"
|
msgid "Close GIF dialog"
|
||||||
@@ -1823,7 +1823,7 @@ msgstr "Luotu {0}"
|
|||||||
|
|
||||||
#: src/screens/List/ListHiddenScreen.tsx:127
|
#: src/screens/List/ListHiddenScreen.tsx:127
|
||||||
msgid "Creator has been blocked"
|
msgid "Creator has been blocked"
|
||||||
msgstr ""
|
msgstr "Tekijä on estetty"
|
||||||
|
|
||||||
#: src/screens/Onboarding/index.tsx:26
|
#: src/screens/Onboarding/index.tsx:26
|
||||||
#: src/screens/Onboarding/state.ts:99
|
#: src/screens/Onboarding/state.ts:99
|
||||||
@@ -2321,7 +2321,7 @@ msgstr "Koulutus"
|
|||||||
|
|
||||||
#: src/screens/List/ListHiddenScreen.tsx:141
|
#: src/screens/List/ListHiddenScreen.tsx:141
|
||||||
msgid "Either the creator of this list has blocked you or you have blocked the creator."
|
msgid "Either the creator of this list has blocked you or you have blocked the creator."
|
||||||
msgstr ""
|
msgstr "Joko listan tekijä on estänyt sinut tai sitten sinä olet estänyt listan tekijän."
|
||||||
|
|
||||||
#: src/screens/Settings/AccountSettings.tsx:60
|
#: src/screens/Settings/AccountSettings.tsx:60
|
||||||
#: src/screens/Signup/StepInfo/index.tsx:170
|
#: src/screens/Signup/StepInfo/index.tsx:170
|
||||||
@@ -3410,7 +3410,7 @@ msgstr "Virheellinen tai ei-tuettu julkaisutietue"
|
|||||||
#: src/screens/Login/LoginForm.tsx:90
|
#: src/screens/Login/LoginForm.tsx:90
|
||||||
#: src/screens/Login/LoginForm.tsx:149
|
#: src/screens/Login/LoginForm.tsx:149
|
||||||
msgid "Incorrect username or password"
|
msgid "Incorrect username or password"
|
||||||
msgstr "Virheellinen käyttäjätunnus tai salasana"
|
msgstr "Väärä käyttäjätunnus tai salasana"
|
||||||
|
|
||||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:91
|
#: src/components/intents/VerifyEmailIntentDialog.tsx:91
|
||||||
msgid "Invalid Verification Code"
|
msgid "Invalid Verification Code"
|
||||||
@@ -4549,7 +4549,7 @@ msgstr "Hups!"
|
|||||||
|
|
||||||
#: src/screens/Onboarding/StepFinished.tsx:260
|
#: src/screens/Onboarding/StepFinished.tsx:260
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
msgstr "Avaa"
|
msgstr "Avoin"
|
||||||
|
|
||||||
#: src/view/com/posts/AviFollowButton.tsx:86
|
#: src/view/com/posts/AviFollowButton.tsx:86
|
||||||
msgid "Open {name} profile shortcut menu"
|
msgid "Open {name} profile shortcut menu"
|
||||||
@@ -4576,7 +4576,7 @@ msgstr "Avaa alavalikko"
|
|||||||
#: src/view/com/composer/Composer.tsx:1228
|
#: src/view/com/composer/Composer.tsx:1228
|
||||||
#: src/view/com/composer/Composer.tsx:1229
|
#: src/view/com/composer/Composer.tsx:1229
|
||||||
msgid "Open emoji picker"
|
msgid "Open emoji picker"
|
||||||
msgstr "Avaa emoji-valitsin"
|
msgstr "Avaa emojinvalitsin"
|
||||||
|
|
||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:190
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:190
|
||||||
msgid "Open feed info screen"
|
msgid "Open feed info screen"
|
||||||
@@ -6283,7 +6283,7 @@ msgstr "Kirjaudutaanko ulos?"
|
|||||||
#: src/view/shell/NavSignupCard.tsx:47
|
#: src/view/shell/NavSignupCard.tsx:47
|
||||||
#: src/view/shell/NavSignupCard.tsx:52
|
#: src/view/shell/NavSignupCard.tsx:52
|
||||||
msgid "Create account"
|
msgid "Create account"
|
||||||
msgstr "Rekisteröidy"
|
msgstr "Luo tili"
|
||||||
|
|
||||||
#: src/components/moderation/ScreenHider.tsx:91
|
#: src/components/moderation/ScreenHider.tsx:91
|
||||||
#: src/lib/moderation/useGlobalLabelStrings.ts:28
|
#: src/lib/moderation/useGlobalLabelStrings.ts:28
|
||||||
@@ -6543,7 +6543,7 @@ msgstr "Napauta vaihtaaksesi sovelluskuvakkeen"
|
|||||||
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:138
|
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:138
|
||||||
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:174
|
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:174
|
||||||
msgid "Tap to close the emoji picker"
|
msgid "Tap to close the emoji picker"
|
||||||
msgstr ""
|
msgstr "Napauta sulkeaksesi emojinvalitsimen"
|
||||||
|
|
||||||
#: src/components/ProgressGuide/Toast.tsx:156
|
#: src/components/ProgressGuide/Toast.tsx:156
|
||||||
msgid "Tap to dismiss"
|
msgid "Tap to dismiss"
|
||||||
|
|||||||
+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
|
#: src/screens/Messages/ChatList.tsx:190
|
||||||
msgid "You have no conversations yet. Start one!"
|
msgid "You have no conversations yet. Start one!"
|
||||||
msgstr "У вас ещё нет бесед. Начните одну!"
|
msgstr "У вас ещё нет бесед. Создайте первую!"
|
||||||
|
|
||||||
#: src/view/com/feeds/ProfileFeedgens.tsx:143
|
#: src/view/com/feeds/ProfileFeedgens.tsx:143
|
||||||
msgid "You have no feeds."
|
msgid "You have no feeds."
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+1073
-2194
File diff suppressed because it is too large
Load Diff
@@ -8,14 +8,14 @@ msgstr ""
|
|||||||
"Language: vi\n"
|
"Language: vi\n"
|
||||||
"Project-Id-Version: \n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2024-11-20T08:45:59.857Z\n"
|
"PO-Revision-Date: 2025-01-22T08:45:59.857Z\n"
|
||||||
"Last-Translator: vnphanquang\n"
|
"Last-Translator: vinhphm\n"
|
||||||
"Language-Team: Translators in PR #2524, ManhNguyen98, vinhphm, vnphanquang\n"
|
"Language-Team: Translators in PR #2524, ManhNguyen98, vinhphm, vnphanquang\n"
|
||||||
"Plural-Forms: \n"
|
"Plural-Forms: \n"
|
||||||
|
|
||||||
#: src/components/ProgressGuide/FollowDialog.tsx:567
|
#: src/components/ProgressGuide/FollowDialog.tsx:567
|
||||||
msgid "(active)"
|
msgid "(active)"
|
||||||
msgstr ""
|
msgstr "(hoạt động)"
|
||||||
|
|
||||||
#: src/screens/Messages/components/ChatListItem.tsx:130
|
#: src/screens/Messages/components/ChatListItem.tsx:130
|
||||||
msgid "(contains embedded content)"
|
msgid "(contains embedded content)"
|
||||||
@@ -32,11 +32,11 @@ msgstr "{0, plural, one {# ngày} other {# ngày}}"
|
|||||||
|
|
||||||
#: src/screens/Profile/ProfileFollowers.tsx:40
|
#: src/screens/Profile/ProfileFollowers.tsx:40
|
||||||
msgid "{0, plural, one {# follower} other {# followers}}"
|
msgid "{0, plural, one {# follower} other {# followers}}"
|
||||||
msgstr ""
|
msgstr "{0, plural, one {# người theo dõi} other {# người theo dõi}}"
|
||||||
|
|
||||||
#: src/screens/Profile/ProfileFollows.tsx:40
|
#: src/screens/Profile/ProfileFollows.tsx:40
|
||||||
msgid "{0, plural, one {# following} other {# following}}"
|
msgid "{0, plural, one {# following} other {# following}}"
|
||||||
msgstr ""
|
msgstr "{0, plural, one {# đang theo dõi} other {# đang theo dõi}}"
|
||||||
|
|
||||||
#: src/lib/hooks/useTimeAgo.ts:146
|
#: src/lib/hooks/useTimeAgo.ts:146
|
||||||
msgid "{0, plural, one {# hour} other {# hours}}"
|
msgid "{0, plural, one {# hour} other {# hours}}"
|
||||||
@@ -52,15 +52,15 @@ msgstr "{0, plural, one {# giờ} other {# giờ}}"
|
|||||||
|
|
||||||
#: src/components/moderation/LabelsOnMe.tsx:53
|
#: src/components/moderation/LabelsOnMe.tsx:53
|
||||||
msgid "{0, plural, one {# label has} other {# labels have}} been placed on this account"
|
msgid "{0, plural, one {# label has} other {# labels have}} been placed on this account"
|
||||||
msgstr ""
|
msgstr "{0, plural, one {# nhãn} other {# nhãn}} được gắn lên tài khoản này"
|
||||||
|
|
||||||
#: src/components/moderation/LabelsOnMe.tsx:62
|
#: src/components/moderation/LabelsOnMe.tsx:62
|
||||||
msgid "{0, plural, one {# label has} other {# labels have}} been placed on this content"
|
msgid "{0, plural, one {# label has} other {# labels have}} been placed on this content"
|
||||||
msgstr ""
|
msgstr "{0, plural, one {# nhãn} other {# nhãn}} được gắn lên nội dung này"
|
||||||
|
|
||||||
#: src/screens/Post/PostLikedBy.tsx:41
|
#: src/screens/Post/PostLikedBy.tsx:41
|
||||||
msgid "{0, plural, one {# like} other {# likes}}"
|
msgid "{0, plural, one {# like} other {# likes}}"
|
||||||
msgstr ""
|
msgstr "{0, plural, one {# lượt thích} other {# lượt thích}}"
|
||||||
|
|
||||||
#: src/lib/hooks/useTimeAgo.ts:136
|
#: src/lib/hooks/useTimeAgo.ts:136
|
||||||
msgid "{0, plural, one {# minute} other {# minutes}}"
|
msgid "{0, plural, one {# minute} other {# minutes}}"
|
||||||
@@ -72,7 +72,7 @@ msgstr "{0, plural, one {# tháng} other {# tháng}}"
|
|||||||
|
|
||||||
#: src/screens/Post/PostQuotes.tsx:41
|
#: src/screens/Post/PostQuotes.tsx:41
|
||||||
msgid "{0, plural, one {# quote} other {# quotes}}"
|
msgid "{0, plural, one {# quote} other {# quotes}}"
|
||||||
msgstr ""
|
msgstr "{0, plural, one {# trích dẫn} other {# trích dẫn}}"
|
||||||
|
|
||||||
#: src/screens/Post/PostRepostedBy.tsx:41
|
#: src/screens/Post/PostRepostedBy.tsx:41
|
||||||
msgid "{0, plural, one {# repost} other {# reposts}}"
|
msgid "{0, plural, one {# repost} other {# reposts}}"
|
||||||
@@ -322,7 +322,7 @@ msgstr "không thể nhắn tin cho {handle}"
|
|||||||
|
|
||||||
#: src/view/shell/bottom-bar/BottomBarWeb.tsx:260
|
#: src/view/shell/bottom-bar/BottomBarWeb.tsx:260
|
||||||
msgid "{notificationCount} unread items"
|
msgid "{notificationCount} unread items"
|
||||||
msgstr ""
|
msgstr "{notificationCount} mục chưa đọc"
|
||||||
|
|
||||||
#: src/view/shell/Drawer.tsx:448
|
#: src/view/shell/Drawer.tsx:448
|
||||||
msgid "{numUnreadNotifications} unread"
|
msgid "{numUnreadNotifications} unread"
|
||||||
@@ -555,7 +555,7 @@ msgstr "Thêm bài mới"
|
|||||||
#: src/view/screens/ProfileList.tsx:903
|
#: src/view/screens/ProfileList.tsx:903
|
||||||
#: src/view/screens/ProfileList.tsx:921
|
#: src/view/screens/ProfileList.tsx:921
|
||||||
msgid "Add people"
|
msgid "Add people"
|
||||||
msgstr ""
|
msgstr "Thêm người"
|
||||||
|
|
||||||
#: src/screens/Home/NoFeedsPinned.tsx:99
|
#: src/screens/Home/NoFeedsPinned.tsx:99
|
||||||
msgid "Add recommended feeds"
|
msgid "Add recommended feeds"
|
||||||
@@ -629,7 +629,7 @@ msgstr "Nâng cao"
|
|||||||
|
|
||||||
#: src/view/screens/Notifications.tsx:86
|
#: src/view/screens/Notifications.tsx:86
|
||||||
msgid "All"
|
msgid "All"
|
||||||
msgstr ""
|
msgstr "Tất cả"
|
||||||
|
|
||||||
#: src/screens/StarterPack/StarterPackScreen.tsx:381
|
#: src/screens/StarterPack/StarterPackScreen.tsx:381
|
||||||
msgid "All accounts have been followed!"
|
msgid "All accounts have been followed!"
|
||||||
@@ -651,7 +651,7 @@ msgstr "Cho phép tin nhắn mới từ"
|
|||||||
|
|
||||||
#: src/components/dialogs/PostInteractionSettingsDialog.tsx:314
|
#: src/components/dialogs/PostInteractionSettingsDialog.tsx:314
|
||||||
msgid "Allow quote posts"
|
msgid "Allow quote posts"
|
||||||
msgstr ""
|
msgstr "Cho phép trích dẫn bài đăng"
|
||||||
|
|
||||||
#: src/components/dialogs/PostInteractionSettingsDialog.tsx:359
|
#: src/components/dialogs/PostInteractionSettingsDialog.tsx:359
|
||||||
msgid "Allow replies from:"
|
msgid "Allow replies from:"
|
||||||
@@ -998,7 +998,7 @@ msgstr "Bạn cần xác minh email trước khi có thể nhắn tin với ngư
|
|||||||
|
|
||||||
#: src/screens/Search/components/ExploreTrendingTopics.tsx:74
|
#: src/screens/Search/components/ExploreTrendingTopics.tsx:74
|
||||||
msgid "BETA"
|
msgid "BETA"
|
||||||
msgstr ""
|
msgstr "Thử nghiệm"
|
||||||
|
|
||||||
#: src/components/dialogs/BirthDateSettings.tsx:106
|
#: src/components/dialogs/BirthDateSettings.tsx:106
|
||||||
#: src/screens/Settings/AccountSettings.tsx:109
|
#: src/screens/Settings/AccountSettings.tsx:109
|
||||||
@@ -1114,7 +1114,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: src/screens/Settings/AppIconSettings/index.tsx:102
|
#: src/screens/Settings/AppIconSettings/index.tsx:102
|
||||||
msgid "Bluesky+ icons"
|
msgid "Bluesky+ icons"
|
||||||
msgstr ""
|
msgstr "Biểu tượng Bluesky+"
|
||||||
|
|
||||||
#: src/lib/moderation/useLabelBehaviorDescription.ts:53
|
#: src/lib/moderation/useLabelBehaviorDescription.ts:53
|
||||||
msgid "Blur images"
|
msgid "Blur images"
|
||||||
@@ -1156,15 +1156,15 @@ msgstr "Xem các bảng tin khác"
|
|||||||
|
|
||||||
#: src/components/TrendingTopics.tsx:176
|
#: src/components/TrendingTopics.tsx:176
|
||||||
msgid "Browse posts about {displayName}"
|
msgid "Browse posts about {displayName}"
|
||||||
msgstr ""
|
msgstr "Tìm bài đăng về {displayName}"
|
||||||
|
|
||||||
#: src/components/TrendingTopics.tsx:184
|
#: src/components/TrendingTopics.tsx:184
|
||||||
msgid "Browse posts tagged with {displayName}"
|
msgid "Browse posts tagged with {displayName}"
|
||||||
msgstr ""
|
msgstr "Tìm bài đăng được gắn thẻ với {displayName}"
|
||||||
|
|
||||||
#: src/components/TrendingTopics.tsx:222
|
#: src/components/TrendingTopics.tsx:222
|
||||||
msgid "Browse topic {displayName}"
|
msgid "Browse topic {displayName}"
|
||||||
msgstr ""
|
msgstr "Tìm chủ đề {displayName}"
|
||||||
|
|
||||||
#: src/view/com/auth/SplashScreen.web.tsx:168
|
#: src/view/com/auth/SplashScreen.web.tsx:168
|
||||||
msgid "Business"
|
msgid "Business"
|
||||||
@@ -1184,7 +1184,7 @@ msgstr "Bởi {0}"
|
|||||||
|
|
||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:447
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:447
|
||||||
msgid "By <0>{0}</0>"
|
msgid "By <0>{0}</0>"
|
||||||
msgstr ""
|
msgstr "Bởi <0>{0}</0>"
|
||||||
|
|
||||||
#: src/screens/Signup/StepInfo/Policies.tsx:81
|
#: src/screens/Signup/StepInfo/Policies.tsx:81
|
||||||
msgid "By creating an account you agree to the <0>Privacy Policy</0>."
|
msgid "By creating an account you agree to the <0>Privacy Policy</0>."
|
||||||
@@ -1296,7 +1296,7 @@ msgstr "Thay đổi"
|
|||||||
|
|
||||||
#: src/screens/Settings/AppIconSettings/index.tsx:39
|
#: src/screens/Settings/AppIconSettings/index.tsx:39
|
||||||
msgid "Change app icon"
|
msgid "Change app icon"
|
||||||
msgstr ""
|
msgstr "Thay đổi biểu tượng ứng dụng"
|
||||||
|
|
||||||
#: src/screens/Settings/AppIconSettings/index.tsx:38
|
#: src/screens/Settings/AppIconSettings/index.tsx:38
|
||||||
#: src/screens/Settings/AppIconSettings/index.tsx:221
|
#: src/screens/Settings/AppIconSettings/index.tsx:221
|
||||||
@@ -1412,7 +1412,7 @@ msgstr "Chọn màu này làm hình đại diện của bạn"
|
|||||||
#: src/view/screens/Feeds.tsx:728
|
#: src/view/screens/Feeds.tsx:728
|
||||||
#: src/view/screens/Search/Explore.tsx:411
|
#: src/view/screens/Search/Explore.tsx:411
|
||||||
msgid "Choose your own timeline! Feeds built by the community help you find content you love."
|
msgid "Choose your own timeline! Feeds built by the community help you find content you love."
|
||||||
msgstr ""
|
msgstr "Chọn bảng tin của riêng bạn! Các bảng tin được xây dựng bởi cộng đồng giúp bạn tìm nội dung bạn yêu thích."
|
||||||
|
|
||||||
#: src/screens/Signup/StepInfo/index.tsx:201
|
#: src/screens/Signup/StepInfo/index.tsx:201
|
||||||
msgid "Choose your password"
|
msgid "Choose your password"
|
||||||
@@ -1468,7 +1468,7 @@ msgstr "Khí hậu"
|
|||||||
|
|
||||||
#: src/components/dms/ChatEmptyPill.tsx:39
|
#: src/components/dms/ChatEmptyPill.tsx:39
|
||||||
msgid "Clip 🐴 clop 🐴"
|
msgid "Clip 🐴 clop 🐴"
|
||||||
msgstr "Cộp 🐴 cộp 🐴"
|
msgstr "Lộp 🐴 cộp 🐴"
|
||||||
|
|
||||||
#: src/components/dialogs/GifSelect.tsx:281
|
#: src/components/dialogs/GifSelect.tsx:281
|
||||||
#: src/components/dialogs/VerifyEmailDialog.tsx:289
|
#: src/components/dialogs/VerifyEmailDialog.tsx:289
|
||||||
@@ -1504,7 +1504,7 @@ msgstr "Đóng hộp thoại"
|
|||||||
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:137
|
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:137
|
||||||
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:173
|
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:173
|
||||||
msgid "Close emoji picker"
|
msgid "Close emoji picker"
|
||||||
msgstr ""
|
msgstr "Đóng bảng chọn emoji"
|
||||||
|
|
||||||
#: src/components/dialogs/GifSelect.tsx:169
|
#: src/components/dialogs/GifSelect.tsx:169
|
||||||
msgid "Close GIF dialog"
|
msgid "Close GIF dialog"
|
||||||
@@ -1657,7 +1657,7 @@ msgstr "Liên hệ hỗ trợ"
|
|||||||
|
|
||||||
#: src/screens/Settings/ContentAndMediaSettings.tsx:49
|
#: src/screens/Settings/ContentAndMediaSettings.tsx:49
|
||||||
msgid "Content & Media"
|
msgid "Content & Media"
|
||||||
msgstr ""
|
msgstr "Nội dung & phương tiện"
|
||||||
|
|
||||||
#: src/screens/Settings/AccessibilitySettings.tsx:109
|
#: src/screens/Settings/AccessibilitySettings.tsx:109
|
||||||
#: src/screens/Settings/Settings.tsx:175
|
#: src/screens/Settings/Settings.tsx:175
|
||||||
@@ -1890,7 +1890,7 @@ msgstr "Đã tạo {0}"
|
|||||||
|
|
||||||
#: src/screens/List/ListHiddenScreen.tsx:127
|
#: src/screens/List/ListHiddenScreen.tsx:127
|
||||||
msgid "Creator has been blocked"
|
msgid "Creator has been blocked"
|
||||||
msgstr ""
|
msgstr "Người tạo đã bị chặn"
|
||||||
|
|
||||||
#: src/screens/Onboarding/index.tsx:26
|
#: src/screens/Onboarding/index.tsx:26
|
||||||
#: src/screens/Onboarding/state.ts:99
|
#: src/screens/Onboarding/state.ts:99
|
||||||
@@ -1948,7 +1948,7 @@ msgstr "Mặc định"
|
|||||||
|
|
||||||
#: src/screens/Settings/AppIconSettings/index.tsx:75
|
#: src/screens/Settings/AppIconSettings/index.tsx:75
|
||||||
msgid "Default icons"
|
msgid "Default icons"
|
||||||
msgstr ""
|
msgstr "Biểu tượng mặc định"
|
||||||
|
|
||||||
#: src/components/dms/MessageMenu.tsx:151
|
#: src/components/dms/MessageMenu.tsx:151
|
||||||
#: src/screens/Settings/AppPasswords.tsx:212
|
#: src/screens/Settings/AppPasswords.tsx:212
|
||||||
@@ -2246,7 +2246,7 @@ msgstr "Thả vào để thêm hình ảnh"
|
|||||||
|
|
||||||
#: src/components/dialogs/MutedWords.tsx:153
|
#: src/components/dialogs/MutedWords.tsx:153
|
||||||
msgid "Duration:"
|
msgid "Duration:"
|
||||||
msgstr "Thời gian:"
|
msgstr "Thời hạn:"
|
||||||
|
|
||||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:213
|
#: src/screens/Settings/components/ChangeHandleDialog.tsx:213
|
||||||
msgid "e.g. alice"
|
msgid "e.g. alice"
|
||||||
@@ -2393,7 +2393,7 @@ msgstr "Giáo dục"
|
|||||||
|
|
||||||
#: src/screens/List/ListHiddenScreen.tsx:141
|
#: src/screens/List/ListHiddenScreen.tsx:141
|
||||||
msgid "Either the creator of this list has blocked you or you have blocked the creator."
|
msgid "Either the creator of this list has blocked you or you have blocked the creator."
|
||||||
msgstr ""
|
msgstr "Người tạo danh sách này đã chặn bạn hoặc bạn đã chặn người đó."
|
||||||
|
|
||||||
#: src/screens/Settings/AccountSettings.tsx:60
|
#: src/screens/Settings/AccountSettings.tsx:60
|
||||||
#: src/screens/Signup/StepInfo/index.tsx:170
|
#: src/screens/Signup/StepInfo/index.tsx:170
|
||||||
@@ -2494,7 +2494,7 @@ msgstr "Chỉ bật nguồn này"
|
|||||||
#: src/screens/Settings/ContentAndMediaSettings.tsx:122
|
#: src/screens/Settings/ContentAndMediaSettings.tsx:122
|
||||||
#: src/screens/Settings/ContentAndMediaSettings.tsx:136
|
#: src/screens/Settings/ContentAndMediaSettings.tsx:136
|
||||||
msgid "Enable trending topics"
|
msgid "Enable trending topics"
|
||||||
msgstr ""
|
msgstr "Mở chủ đề nổi trội"
|
||||||
|
|
||||||
#: src/screens/Messages/Settings.tsx:130
|
#: src/screens/Messages/Settings.tsx:130
|
||||||
#: src/screens/Messages/Settings.tsx:133
|
#: src/screens/Messages/Settings.tsx:133
|
||||||
@@ -2808,7 +2808,7 @@ msgstr "Bảng tin bởi {0}"
|
|||||||
|
|
||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:353
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:353
|
||||||
msgid "Feed menu"
|
msgid "Feed menu"
|
||||||
msgstr ""
|
msgstr "Menu bảng tin"
|
||||||
|
|
||||||
#: src/components/StarterPack/Wizard/WizardListCard.tsx:54
|
#: src/components/StarterPack/Wizard/WizardListCard.tsx:54
|
||||||
msgid "Feed toggle"
|
msgid "Feed toggle"
|
||||||
@@ -2847,7 +2847,7 @@ msgstr "Đã cập nhật bảng tin"
|
|||||||
|
|
||||||
#: src/screens/Search/components/ExploreRecommendations.tsx:54
|
#: src/screens/Search/components/ExploreRecommendations.tsx:54
|
||||||
msgid "Feeds we think you might like."
|
msgid "Feeds we think you might like."
|
||||||
msgstr ""
|
msgstr "Bảng tin mà bạn có thể thích."
|
||||||
|
|
||||||
#: src/screens/Settings/components/ExportCarDialog.tsx:43
|
#: src/screens/Settings/components/ExportCarDialog.tsx:43
|
||||||
msgid "File saved successfully!"
|
msgid "File saved successfully!"
|
||||||
@@ -2871,7 +2871,7 @@ msgstr "Tìm tài khoản đề theo dõi"
|
|||||||
#: src/components/ProgressGuide/FollowDialog.tsx:88
|
#: src/components/ProgressGuide/FollowDialog.tsx:88
|
||||||
#: src/components/ProgressGuide/FollowDialog.tsx:417
|
#: src/components/ProgressGuide/FollowDialog.tsx:417
|
||||||
msgid "Find people to follow"
|
msgid "Find people to follow"
|
||||||
msgstr ""
|
msgstr "Tìm người để theo dõi"
|
||||||
|
|
||||||
#: src/view/screens/Search/Search.tsx:595
|
#: src/view/screens/Search/Search.tsx:595
|
||||||
msgid "Find posts and users on Bluesky"
|
msgid "Find posts and users on Bluesky"
|
||||||
@@ -2915,7 +2915,7 @@ msgstr "Theo dõi {name}"
|
|||||||
#: src/components/ProgressGuide/List.tsx:52
|
#: src/components/ProgressGuide/List.tsx:52
|
||||||
#: src/state/shell/progress-guide.tsx:229
|
#: src/state/shell/progress-guide.tsx:229
|
||||||
msgid "Follow 10 accounts"
|
msgid "Follow 10 accounts"
|
||||||
msgstr ""
|
msgstr "Theo dõi 10 tài khoản"
|
||||||
|
|
||||||
#: src/components/ProgressGuide/List.tsx:69
|
#: src/components/ProgressGuide/List.tsx:69
|
||||||
msgid "Follow 7 accounts"
|
msgid "Follow 7 accounts"
|
||||||
@@ -3289,13 +3289,13 @@ msgstr "Ẩn trả lời này?"
|
|||||||
#: src/screens/Search/components/ExploreTrendingTopics.tsx:83
|
#: src/screens/Search/components/ExploreTrendingTopics.tsx:83
|
||||||
#: src/view/shell/desktop/SidebarTrendingTopics.tsx:62
|
#: src/view/shell/desktop/SidebarTrendingTopics.tsx:62
|
||||||
msgid "Hide trending topics"
|
msgid "Hide trending topics"
|
||||||
msgstr ""
|
msgstr "Ẩn chủ đề nổi trội"
|
||||||
|
|
||||||
#: src/components/interstitials/Trending.tsx:99
|
#: src/components/interstitials/Trending.tsx:99
|
||||||
#: src/screens/Search/components/ExploreTrendingTopics.tsx:135
|
#: src/screens/Search/components/ExploreTrendingTopics.tsx:135
|
||||||
#: src/view/shell/desktop/SidebarTrendingTopics.tsx:108
|
#: src/view/shell/desktop/SidebarTrendingTopics.tsx:108
|
||||||
msgid "Hide trending topics?"
|
msgid "Hide trending topics?"
|
||||||
msgstr ""
|
msgstr "Ẩn chủ đề nổi trội?"
|
||||||
|
|
||||||
#: src/view/com/notifications/NotificationFeedItem.tsx:592
|
#: src/view/com/notifications/NotificationFeedItem.tsx:592
|
||||||
msgid "Hide user list"
|
msgid "Hide user list"
|
||||||
@@ -3356,7 +3356,7 @@ msgstr "Nhà cung cấp lưu trữ"
|
|||||||
#: src/view/com/post-thread/PostThread.tsx:652
|
#: src/view/com/post-thread/PostThread.tsx:652
|
||||||
#: src/view/com/post-thread/PostThread.tsx:657
|
#: src/view/com/post-thread/PostThread.tsx:657
|
||||||
msgid "Hot replies first"
|
msgid "Hot replies first"
|
||||||
msgstr "Trở lời nổi bật trước"
|
msgstr "Trả lời nổi bật trước"
|
||||||
|
|
||||||
#: src/view/com/modals/InAppBrowserConsent.tsx:41
|
#: src/view/com/modals/InAppBrowserConsent.tsx:41
|
||||||
msgid "How should we open this link?"
|
msgid "How should we open this link?"
|
||||||
@@ -3405,7 +3405,7 @@ msgstr "Danh sách không thể được khôi phục sau khi đã bị xóa."
|
|||||||
|
|
||||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:250
|
#: src/screens/Settings/components/ChangeHandleDialog.tsx:250
|
||||||
msgid "If you have your own domain, you can use that as your handle. This lets you self-verify your identity. <0>Learn more here.</0>"
|
msgid "If you have your own domain, you can use that as your handle. This lets you self-verify your identity. <0>Learn more here.</0>"
|
||||||
msgstr ""
|
msgstr "Nếu bạn có tên miền riêng, bạn có thể sử dụng nó làm tên người dùng. Điều này giúp bạn tự xác minh danh tính của mình. <0>Tìm hiểu thêm tại đây.</0>"
|
||||||
|
|
||||||
#: src/view/com/util/forms/PostDropdownBtnMenuItems.tsx:658
|
#: src/view/com/util/forms/PostDropdownBtnMenuItems.tsx:658
|
||||||
msgid "If you remove this post, you won't be able to recover it."
|
msgid "If you remove this post, you won't be able to recover it."
|
||||||
@@ -3703,11 +3703,11 @@ msgstr "Sáng"
|
|||||||
|
|
||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:514
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:514
|
||||||
msgid "Like"
|
msgid "Like"
|
||||||
msgstr ""
|
msgstr "Thích"
|
||||||
|
|
||||||
#: src/view/com/util/post-ctrls/PostCtrls.tsx:311
|
#: src/view/com/util/post-ctrls/PostCtrls.tsx:311
|
||||||
msgid "Like ({0, plural, one {# like} other {# likes}})"
|
msgid "Like ({0, plural, one {# like} other {# likes}})"
|
||||||
msgstr ""
|
msgstr "Thích ({0, plural, one {# lượt thích} other {# lượt thích}})"
|
||||||
|
|
||||||
#: src/components/ProgressGuide/List.tsx:63
|
#: src/components/ProgressGuide/List.tsx:63
|
||||||
msgid "Like 10 posts"
|
msgid "Like 10 posts"
|
||||||
@@ -3720,7 +3720,7 @@ msgstr "Thích 10 bài để dạy cho bảng tin Khám phá"
|
|||||||
|
|
||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:501
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:501
|
||||||
msgid "Like feed"
|
msgid "Like feed"
|
||||||
msgstr ""
|
msgstr "Thích bảng tin"
|
||||||
|
|
||||||
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:275
|
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:275
|
||||||
msgid "Like this feed"
|
msgid "Like this feed"
|
||||||
@@ -3741,14 +3741,14 @@ msgstr "Thích bởi"
|
|||||||
#: src/components/FeedCard.tsx:213
|
#: src/components/FeedCard.tsx:213
|
||||||
#: src/view/com/feeds/FeedSourceCard.tsx:303
|
#: src/view/com/feeds/FeedSourceCard.tsx:303
|
||||||
msgid "Liked by {0, plural, one {# user} other {# users}}"
|
msgid "Liked by {0, plural, one {# user} other {# users}}"
|
||||||
msgstr ""
|
msgstr "Thích bởi {0, plural, one {# người dùng} other {# người dùng}}"
|
||||||
|
|
||||||
#: src/components/LabelingServiceCard/index.tsx:96
|
#: src/components/LabelingServiceCard/index.tsx:96
|
||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:489
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:489
|
||||||
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:295
|
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:295
|
||||||
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:309
|
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:309
|
||||||
msgid "Liked by {likeCount, plural, one {# user} other {# users}}"
|
msgid "Liked by {likeCount, plural, one {# user} other {# users}}"
|
||||||
msgstr ""
|
msgstr "Thích bởi {likeCount, plural, one {# người dùng} other {# người dùng}}"
|
||||||
|
|
||||||
#: src/view/screens/Profile.tsx:234
|
#: src/view/screens/Profile.tsx:234
|
||||||
msgid "Likes"
|
msgid "Likes"
|
||||||
@@ -3761,7 +3761,7 @@ msgstr "Lượt thích cho bài đăng này"
|
|||||||
#: src/view/com/post-thread/PostThread.tsx:626
|
#: src/view/com/post-thread/PostThread.tsx:626
|
||||||
#: src/view/com/post-thread/PostThread.tsx:631
|
#: src/view/com/post-thread/PostThread.tsx:631
|
||||||
msgid "Linear"
|
msgid "Linear"
|
||||||
msgstr ""
|
msgstr "Ngang hàng"
|
||||||
|
|
||||||
#: src/Navigation.tsx:198
|
#: src/Navigation.tsx:198
|
||||||
msgid "List"
|
msgid "List"
|
||||||
@@ -3782,11 +3782,11 @@ msgstr "Danh sách bởi {0}"
|
|||||||
|
|
||||||
#: src/view/com/profile/ProfileSubpageHeader.tsx:156
|
#: src/view/com/profile/ProfileSubpageHeader.tsx:156
|
||||||
msgid "List by <0/>"
|
msgid "List by <0/>"
|
||||||
msgstr ""
|
msgstr "Danh sách tạo bởi <0/>"
|
||||||
|
|
||||||
#: src/view/com/profile/ProfileSubpageHeader.tsx:154
|
#: src/view/com/profile/ProfileSubpageHeader.tsx:154
|
||||||
msgid "List by you"
|
msgid "List by you"
|
||||||
msgstr ""
|
msgstr "Danh sách của bạn"
|
||||||
|
|
||||||
#: src/view/screens/ProfileList.tsx:455
|
#: src/view/screens/ProfileList.tsx:455
|
||||||
msgid "List deleted"
|
msgid "List deleted"
|
||||||
@@ -3882,7 +3882,7 @@ msgstr "Đăng nhập vào tải khoản chưa được liệt kê"
|
|||||||
|
|
||||||
#: src/view/shell/desktop/RightNav.tsx:121
|
#: src/view/shell/desktop/RightNav.tsx:121
|
||||||
msgid "Logo by @sawaratsuki.bsky.social"
|
msgid "Logo by @sawaratsuki.bsky.social"
|
||||||
msgstr ""
|
msgstr "Logo bởi @sawaratsuki.bsky.social"
|
||||||
|
|
||||||
#: src/view/shell/desktop/RightNav.tsx:103
|
#: src/view/shell/desktop/RightNav.tsx:103
|
||||||
#~ msgid "Logo by <0/>"
|
#~ msgid "Logo by <0/>"
|
||||||
@@ -3953,7 +3953,7 @@ msgstr "Người dùng được đề cập"
|
|||||||
|
|
||||||
#: src/view/screens/Notifications.tsx:99
|
#: src/view/screens/Notifications.tsx:99
|
||||||
msgid "Mentions"
|
msgid "Mentions"
|
||||||
msgstr ""
|
msgstr "Nhắc đến"
|
||||||
|
|
||||||
#: src/components/Menu/index.tsx:96
|
#: src/components/Menu/index.tsx:96
|
||||||
#: src/view/screens/Search/Search.tsx:863
|
#: src/view/screens/Search/Search.tsx:863
|
||||||
@@ -4282,7 +4282,7 @@ msgstr "Tên người dùng mới"
|
|||||||
#: src/view/screens/Lists.tsx:66
|
#: src/view/screens/Lists.tsx:66
|
||||||
#: src/view/screens/ModerationModlists.tsx:66
|
#: src/view/screens/ModerationModlists.tsx:66
|
||||||
msgid "New list"
|
msgid "New list"
|
||||||
msgstr ""
|
msgstr "Danh sách mới"
|
||||||
|
|
||||||
#: src/components/dms/NewMessagesPill.tsx:92
|
#: src/components/dms/NewMessagesPill.tsx:92
|
||||||
msgid "New messages"
|
msgid "New messages"
|
||||||
@@ -4668,7 +4668,7 @@ msgstr "Mở tùy chọn hội thoại"
|
|||||||
|
|
||||||
#: src/components/Layout/Header/index.tsx:145
|
#: src/components/Layout/Header/index.tsx:145
|
||||||
msgid "Open drawer menu"
|
msgid "Open drawer menu"
|
||||||
msgstr ""
|
msgstr "Mở trình đơn"
|
||||||
|
|
||||||
#: src/screens/Messages/components/MessageInput.web.tsx:181
|
#: src/screens/Messages/components/MessageInput.web.tsx:181
|
||||||
#: src/view/com/composer/Composer.tsx:1228
|
#: src/view/com/composer/Composer.tsx:1228
|
||||||
@@ -4678,7 +4678,7 @@ msgstr "Mở trình chọn biểu tượng cảm xúc"
|
|||||||
|
|
||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:190
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:190
|
||||||
msgid "Open feed info screen"
|
msgid "Open feed info screen"
|
||||||
msgstr ""
|
msgstr "Mở màn hình thông tin bảng tin"
|
||||||
|
|
||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:290
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:290
|
||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:295
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:295
|
||||||
@@ -4912,7 +4912,7 @@ msgstr "Hình ảnh cho người lớn"
|
|||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:519
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:519
|
||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:526
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:526
|
||||||
msgid "Pin feed"
|
msgid "Pin feed"
|
||||||
msgstr ""
|
msgstr "Ghim bảng tin"
|
||||||
|
|
||||||
#: src/view/screens/ProfileList.tsx:685
|
#: src/view/screens/ProfileList.tsx:685
|
||||||
msgid "Pin to home"
|
msgid "Pin to home"
|
||||||
@@ -4934,7 +4934,7 @@ msgstr "Đã ghim"
|
|||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:162
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:162
|
||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:174
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:174
|
||||||
msgid "Pinned {0} to Home"
|
msgid "Pinned {0} to Home"
|
||||||
msgstr ""
|
msgstr "Đã ghim {0} vào trang chủ"
|
||||||
|
|
||||||
#: src/view/screens/SavedFeeds.tsx:123
|
#: src/view/screens/SavedFeeds.tsx:123
|
||||||
msgid "Pinned Feeds"
|
msgid "Pinned Feeds"
|
||||||
@@ -5226,11 +5226,11 @@ msgstr "Công khai"
|
|||||||
|
|
||||||
#: src/view/com/lists/MyLists.tsx:77
|
#: src/view/com/lists/MyLists.tsx:77
|
||||||
msgid "Public, sharable lists of users to mute or block in bulk."
|
msgid "Public, sharable lists of users to mute or block in bulk."
|
||||||
msgstr ""
|
msgstr "Danh sách công khai, chia sẻ được của người dùng để cho việc ẩn hoặc chặn hàng loạt."
|
||||||
|
|
||||||
#: src/view/com/lists/MyLists.tsx:72
|
#: src/view/com/lists/MyLists.tsx:72
|
||||||
msgid "Public, sharable lists which can be used to drive feeds."
|
msgid "Public, sharable lists which can be used to drive feeds."
|
||||||
msgstr ""
|
msgstr "Danh sách công khai và chia sẻ được có thể thúc đẩy bảng tin."
|
||||||
|
|
||||||
#: src/view/screens/ModerationModlists.tsx:75
|
#: src/view/screens/ModerationModlists.tsx:75
|
||||||
#~ msgid "Public, shareable lists of users to mute or block in bulk."
|
#~ msgid "Public, shareable lists of users to mute or block in bulk."
|
||||||
@@ -5334,7 +5334,7 @@ msgstr "Tìm kiếm gần đây"
|
|||||||
|
|
||||||
#: src/screens/Search/components/ExploreRecommendations.tsx:50
|
#: src/screens/Search/components/ExploreRecommendations.tsx:50
|
||||||
msgid "Recommended"
|
msgid "Recommended"
|
||||||
msgstr ""
|
msgstr "Đề xuất"
|
||||||
|
|
||||||
#: src/screens/Messages/components/MessageListError.tsx:20
|
#: src/screens/Messages/components/MessageListError.tsx:20
|
||||||
msgid "Reconnect"
|
msgid "Reconnect"
|
||||||
@@ -5506,7 +5506,7 @@ msgstr "Trả lời"
|
|||||||
|
|
||||||
#: src/view/com/util/post-ctrls/PostCtrls.tsx:263
|
#: src/view/com/util/post-ctrls/PostCtrls.tsx:263
|
||||||
msgid "Reply ({0, plural, one {# reply} other {# replies}})"
|
msgid "Reply ({0, plural, one {# reply} other {# replies}})"
|
||||||
msgstr ""
|
msgstr "Trả lời ({0, plural, one {# trả lời} other {# trả lời}})"
|
||||||
|
|
||||||
#: src/components/moderation/ModerationDetailsDialog.tsx:114
|
#: src/components/moderation/ModerationDetailsDialog.tsx:114
|
||||||
#: src/lib/moderation/useModerationCauseDescription.ts:123
|
#: src/lib/moderation/useModerationCauseDescription.ts:123
|
||||||
@@ -5528,7 +5528,7 @@ msgstr "Cài đặt trả lời được chọn bởi tác giả của thảo lu
|
|||||||
|
|
||||||
#: src/view/com/post-thread/PostThread.tsx:648
|
#: src/view/com/post-thread/PostThread.tsx:648
|
||||||
msgid "Reply sorting"
|
msgid "Reply sorting"
|
||||||
msgstr ""
|
msgstr "Sắp xếp trả lời"
|
||||||
|
|
||||||
#: src/view/com/post/Post.tsx:204
|
#: src/view/com/post/Post.tsx:204
|
||||||
#: src/view/com/posts/PostFeedItem.tsx:553
|
#: src/view/com/posts/PostFeedItem.tsx:553
|
||||||
@@ -5647,7 +5647,7 @@ msgstr "Đăng lại"
|
|||||||
|
|
||||||
#: src/view/com/util/post-ctrls/RepostButton.tsx:74
|
#: src/view/com/util/post-ctrls/RepostButton.tsx:74
|
||||||
msgid "Repost ({0, plural, one {# repost} other {# reposts}})"
|
msgid "Repost ({0, plural, one {# repost} other {# reposts}})"
|
||||||
msgstr ""
|
msgstr "Đăng lại ({0, plural, one {# đăng lại} other {# đăng lại}})"
|
||||||
|
|
||||||
#: src/screens/StarterPack/StarterPackScreen.tsx:548
|
#: src/screens/StarterPack/StarterPackScreen.tsx:548
|
||||||
#: src/view/com/util/post-ctrls/RepostButton.tsx:148
|
#: src/view/com/util/post-ctrls/RepostButton.tsx:148
|
||||||
@@ -5890,15 +5890,15 @@ msgstr "Tìm kiếm"
|
|||||||
|
|
||||||
#: src/components/ProgressGuide/FollowDialog.tsx:745
|
#: src/components/ProgressGuide/FollowDialog.tsx:745
|
||||||
msgid "Search by name or interest"
|
msgid "Search by name or interest"
|
||||||
msgstr ""
|
msgstr "Tìm theo tên hoặc sở thích"
|
||||||
|
|
||||||
#: src/view/screens/Feeds.tsx:441
|
#: src/view/screens/Feeds.tsx:441
|
||||||
msgid "Search feeds"
|
msgid "Search feeds"
|
||||||
msgstr ""
|
msgstr "Tìm bảng tin"
|
||||||
|
|
||||||
#: src/components/ProgressGuide/FollowDialog.tsx:575
|
#: src/components/ProgressGuide/FollowDialog.tsx:575
|
||||||
msgid "Search for \"{interestsDisplayName}\"{activeText}"
|
msgid "Search for \"{interestsDisplayName}\"{activeText}"
|
||||||
msgstr ""
|
msgstr "Tìm kiếm \"{interestsDisplayName}\"{activeText}"
|
||||||
|
|
||||||
#: src/view/shell/desktop/Search.tsx:201
|
#: src/view/shell/desktop/Search.tsx:201
|
||||||
msgid "Search for \"{query}\""
|
msgid "Search for \"{query}\""
|
||||||
@@ -5961,7 +5961,7 @@ msgstr "Xem hướng dẫn này"
|
|||||||
|
|
||||||
#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/Scrubber.tsx:194
|
#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/Scrubber.tsx:194
|
||||||
msgid "Seek slider. Use the arrow keys to seek forwards and backwards, and space to play/pause"
|
msgid "Seek slider. Use the arrow keys to seek forwards and backwards, and space to play/pause"
|
||||||
msgstr ""
|
msgstr "Thanh trượt. Sử dụng các phím mũi tên để tìm kiếm về phía trước và phía sau, và phím cách để phát / tạm dừng"
|
||||||
|
|
||||||
#: src/screens/Onboarding/StepProfile/AvatarCreatorItems.tsx:67
|
#: src/screens/Onboarding/StepProfile/AvatarCreatorItems.tsx:67
|
||||||
msgid "Select a color"
|
msgid "Select a color"
|
||||||
@@ -6125,7 +6125,7 @@ msgstr "Địa chỉ máy chủ"
|
|||||||
|
|
||||||
#: src/screens/Settings/AppIconSettings/index.tsx:178
|
#: src/screens/Settings/AppIconSettings/index.tsx:178
|
||||||
msgid "Set app icon to {0}"
|
msgid "Set app icon to {0}"
|
||||||
msgstr ""
|
msgstr "Đặt biểu tượng ứng dụng thành {0}"
|
||||||
|
|
||||||
#: src/screens/Moderation/index.tsx:290
|
#: src/screens/Moderation/index.tsx:290
|
||||||
msgid "Set birthdate"
|
msgid "Set birthdate"
|
||||||
@@ -6218,7 +6218,7 @@ msgstr "Chia sẻ mã QR"
|
|||||||
|
|
||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:470
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:470
|
||||||
msgid "Share this feed"
|
msgid "Share this feed"
|
||||||
msgstr ""
|
msgstr "Chia sẻ bảng tin này"
|
||||||
|
|
||||||
#: src/screens/StarterPack/StarterPackScreen.tsx:416
|
#: src/screens/StarterPack/StarterPackScreen.tsx:416
|
||||||
msgid "Share this starter pack"
|
msgid "Share this starter pack"
|
||||||
@@ -6313,11 +6313,11 @@ msgstr "Hiển thị trả lời"
|
|||||||
|
|
||||||
#: src/view/com/post-thread/PostThread.tsx:622
|
#: src/view/com/post-thread/PostThread.tsx:622
|
||||||
msgid "Show replies as"
|
msgid "Show replies as"
|
||||||
msgstr ""
|
msgstr "Hiển thị trả lời dưới dạng"
|
||||||
|
|
||||||
#: src/screens/Settings/ThreadPreferences.tsx:151
|
#: src/screens/Settings/ThreadPreferences.tsx:151
|
||||||
msgid "Show replies as threaded"
|
msgid "Show replies as threaded"
|
||||||
msgstr ""
|
msgstr "Hiển thị trả lời dưới dạng phân luồng"
|
||||||
|
|
||||||
#: src/screens/Settings/ThreadPreferences.tsx:126
|
#: src/screens/Settings/ThreadPreferences.tsx:126
|
||||||
msgid "Show replies by people you follow before all other replies"
|
msgid "Show replies by people you follow before all other replies"
|
||||||
@@ -6479,7 +6479,7 @@ msgstr "Có gì đó không đúng!"
|
|||||||
|
|
||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:538
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:538
|
||||||
msgid "Something wrong? Let us know."
|
msgid "Something wrong? Let us know."
|
||||||
msgstr ""
|
msgstr "Có gì không đúng? Hãy cho chúng tôi biết."
|
||||||
|
|
||||||
#: src/App.native.tsx:122
|
#: src/App.native.tsx:122
|
||||||
#: src/App.web.tsx:97
|
#: src/App.web.tsx:97
|
||||||
@@ -6523,12 +6523,12 @@ msgstr "Bắt đầu cuộc trò chuyện mới"
|
|||||||
#: src/view/screens/ProfileList.tsx:817
|
#: src/view/screens/ProfileList.tsx:817
|
||||||
#: src/view/screens/ProfileList.tsx:938
|
#: src/view/screens/ProfileList.tsx:938
|
||||||
msgid "Start adding people"
|
msgid "Start adding people"
|
||||||
msgstr ""
|
msgstr "Bắt đầu thêm người"
|
||||||
|
|
||||||
#: src/view/screens/ProfileList.tsx:824
|
#: src/view/screens/ProfileList.tsx:824
|
||||||
#: src/view/screens/ProfileList.tsx:945
|
#: src/view/screens/ProfileList.tsx:945
|
||||||
msgid "Start adding people!"
|
msgid "Start adding people!"
|
||||||
msgstr ""
|
msgstr "Bắt đầu thêm người!"
|
||||||
|
|
||||||
#: src/components/dms/dialogs/SearchablePeopleList.tsx:353
|
#: src/components/dms/dialogs/SearchablePeopleList.tsx:353
|
||||||
msgid "Start chat with {displayName}"
|
msgid "Start chat with {displayName}"
|
||||||
@@ -6546,7 +6546,7 @@ msgstr "Gói khởi đầu bởi {0}"
|
|||||||
|
|
||||||
#: src/view/com/profile/ProfileSubpageHeader.tsx:182
|
#: src/view/com/profile/ProfileSubpageHeader.tsx:182
|
||||||
msgid "Starter pack by <0/>"
|
msgid "Starter pack by <0/>"
|
||||||
msgstr ""
|
msgstr "Gói khởi đầu bởi <0/>"
|
||||||
|
|
||||||
#: src/components/StarterPack/StarterPackCard.tsx:80
|
#: src/components/StarterPack/StarterPackCard.tsx:80
|
||||||
#: src/view/com/profile/ProfileSubpageHeader.tsx:180
|
#: src/view/com/profile/ProfileSubpageHeader.tsx:180
|
||||||
@@ -6670,7 +6670,7 @@ msgstr "Chạm để thay đổi biểu tượng ứng dụng"
|
|||||||
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:138
|
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:138
|
||||||
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:174
|
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:174
|
||||||
msgid "Tap to close the emoji picker"
|
msgid "Tap to close the emoji picker"
|
||||||
msgstr ""
|
msgstr "Chạm để đóng bảng chọn biểu tượng cảm xúc"
|
||||||
|
|
||||||
#: src/components/ProgressGuide/Toast.tsx:156
|
#: src/components/ProgressGuide/Toast.tsx:156
|
||||||
msgid "Tap to dismiss"
|
msgid "Tap to dismiss"
|
||||||
@@ -6695,7 +6695,7 @@ msgstr "Nhấn để xem ảnh đầy đủ"
|
|||||||
|
|
||||||
#: src/state/shell/progress-guide.tsx:233
|
#: src/state/shell/progress-guide.tsx:233
|
||||||
msgid "Task complete - 10 follows!"
|
msgid "Task complete - 10 follows!"
|
||||||
msgstr ""
|
msgstr "Hoàn thành nhiệm vụ - 10 người theo dõi!"
|
||||||
|
|
||||||
#: src/state/shell/progress-guide.tsx:223
|
#: src/state/shell/progress-guide.tsx:223
|
||||||
msgid "Task complete - 10 likes!"
|
msgid "Task complete - 10 likes!"
|
||||||
@@ -7099,7 +7099,7 @@ msgstr "Danh sách này - tạo bởi <0>{0}</0> - có thể chứa các vi ph
|
|||||||
|
|
||||||
#: src/view/screens/ProfileList.tsx:933
|
#: src/view/screens/ProfileList.tsx:933
|
||||||
msgid "This list is empty."
|
msgid "This list is empty."
|
||||||
msgstr ""
|
msgstr "Danh sách này đang trống."
|
||||||
|
|
||||||
#: src/screens/Profile/ErrorState.tsx:40
|
#: src/screens/Profile/ErrorState.tsx:40
|
||||||
msgid "This moderation service is unavailable. See below for more details. If this issue persists, contact us."
|
msgid "This moderation service is unavailable. See below for more details. If this issue persists, contact us."
|
||||||
@@ -7190,7 +7190,7 @@ msgstr "Tác vụ này sẽ xóa bài đăng của bạn khỏi bài đăng trí
|
|||||||
#: src/view/com/post-thread/PostThread.tsx:606
|
#: src/view/com/post-thread/PostThread.tsx:606
|
||||||
#: src/view/com/post-thread/PostThread.tsx:609
|
#: src/view/com/post-thread/PostThread.tsx:609
|
||||||
msgid "Thread options"
|
msgid "Thread options"
|
||||||
msgstr ""
|
msgstr "Tùy chọn thảo luận"
|
||||||
|
|
||||||
#: src/screens/Settings/ContentAndMediaSettings.tsx:66
|
#: src/screens/Settings/ContentAndMediaSettings.tsx:66
|
||||||
#: src/screens/Settings/ContentAndMediaSettings.tsx:69
|
#: src/screens/Settings/ContentAndMediaSettings.tsx:69
|
||||||
@@ -7204,7 +7204,7 @@ msgstr "Cài đặt thảo luận"
|
|||||||
#: src/view/com/post-thread/PostThread.tsx:636
|
#: src/view/com/post-thread/PostThread.tsx:636
|
||||||
#: src/view/com/post-thread/PostThread.tsx:641
|
#: src/view/com/post-thread/PostThread.tsx:641
|
||||||
msgid "Threaded"
|
msgid "Threaded"
|
||||||
msgstr ""
|
msgstr "Phân luồng"
|
||||||
|
|
||||||
#: src/screens/Settings/ThreadPreferences.tsx:142
|
#: src/screens/Settings/ThreadPreferences.tsx:142
|
||||||
msgid "Threaded mode"
|
msgid "Threaded mode"
|
||||||
@@ -7254,7 +7254,7 @@ msgstr "Hàng đầu"
|
|||||||
|
|
||||||
#: src/Navigation.tsx:383
|
#: src/Navigation.tsx:383
|
||||||
msgid "Topic"
|
msgid "Topic"
|
||||||
msgstr ""
|
msgstr "Chủ đề"
|
||||||
|
|
||||||
#: src/components/dms/MessageMenu.tsx:103
|
#: src/components/dms/MessageMenu.tsx:103
|
||||||
#: src/components/dms/MessageMenu.tsx:105
|
#: src/components/dms/MessageMenu.tsx:105
|
||||||
@@ -7268,7 +7268,7 @@ msgstr "Dịch"
|
|||||||
#: src/screens/Search/components/ExploreTrendingTopics.tsx:69
|
#: src/screens/Search/components/ExploreTrendingTopics.tsx:69
|
||||||
#: src/view/shell/desktop/SidebarTrendingTopics.tsx:59
|
#: src/view/shell/desktop/SidebarTrendingTopics.tsx:59
|
||||||
msgid "Trending"
|
msgid "Trending"
|
||||||
msgstr ""
|
msgstr "Chủ đề nổi trội"
|
||||||
|
|
||||||
#: src/view/com/util/error/ErrorScreen.tsx:103
|
#: src/view/com/util/error/ErrorScreen.tsx:103
|
||||||
msgctxt "action"
|
msgctxt "action"
|
||||||
@@ -7293,7 +7293,7 @@ msgstr "Gõ tin nhắn của bạn ở đây"
|
|||||||
|
|
||||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:415
|
#: src/screens/Settings/components/ChangeHandleDialog.tsx:415
|
||||||
msgid "Type:"
|
msgid "Type:"
|
||||||
msgstr "Type:"
|
msgstr "Loại:"
|
||||||
|
|
||||||
#: src/view/screens/ProfileList.tsx:590
|
#: src/view/screens/ProfileList.tsx:590
|
||||||
msgid "Un-block list"
|
msgid "Un-block list"
|
||||||
@@ -7358,7 +7358,7 @@ msgstr "Bỏ chặn bài đăng lại"
|
|||||||
|
|
||||||
#: src/view/com/util/post-ctrls/RepostButton.tsx:68
|
#: src/view/com/util/post-ctrls/RepostButton.tsx:68
|
||||||
msgid "Undo repost ({0, plural, one {# repost} other {# reposts}})"
|
msgid "Undo repost ({0, plural, one {# repost} other {# reposts}})"
|
||||||
msgstr ""
|
msgstr "Huỷ đăng lại ({0, plural, one {# đăng lại} other {# đăng lại}})"
|
||||||
|
|
||||||
#: src/view/com/profile/FollowButton.tsx:60
|
#: src/view/com/profile/FollowButton.tsx:60
|
||||||
msgctxt "action"
|
msgctxt "action"
|
||||||
@@ -7376,11 +7376,11 @@ msgstr "Bỏ theo dõi tài khoản"
|
|||||||
|
|
||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:514
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:514
|
||||||
msgid "Unlike"
|
msgid "Unlike"
|
||||||
msgstr ""
|
msgstr "Bỏ thích"
|
||||||
|
|
||||||
#: src/view/com/util/post-ctrls/PostCtrls.tsx:305
|
#: src/view/com/util/post-ctrls/PostCtrls.tsx:305
|
||||||
msgid "Unlike ({0, plural, one {# like} other {# likes}})"
|
msgid "Unlike ({0, plural, one {# like} other {# likes}})"
|
||||||
msgstr ""
|
msgstr "Bỏ thích ({0, plural, one {# thích} other {# thích}})"
|
||||||
|
|
||||||
#: src/view/screens/ProfileFeed.tsx:576
|
#: src/view/screens/ProfileFeed.tsx:576
|
||||||
#~ msgid "Unlike this feed"
|
#~ msgid "Unlike this feed"
|
||||||
@@ -7430,7 +7430,7 @@ msgstr "Bỏ ghim"
|
|||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:519
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:519
|
||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:526
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:526
|
||||||
msgid "Unpin feed"
|
msgid "Unpin feed"
|
||||||
msgstr ""
|
msgstr "Bỏ ghim bảng tin"
|
||||||
|
|
||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:309
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:309
|
||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:311
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:311
|
||||||
@@ -7448,7 +7448,7 @@ msgstr "Bỏ ghim khỏi danh sách kiểm duyệt"
|
|||||||
|
|
||||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:164
|
#: src/screens/Profile/components/ProfileFeedHeader.tsx:164
|
||||||
msgid "Unpinned {0} from Home"
|
msgid "Unpinned {0} from Home"
|
||||||
msgstr ""
|
msgstr "Đã bỏ ghim {0} khỏi Trang chủ"
|
||||||
|
|
||||||
#: src/view/screens/ProfileList.tsx:352
|
#: src/view/screens/ProfileList.tsx:352
|
||||||
msgid "Unpinned from your feeds"
|
msgid "Unpinned from your feeds"
|
||||||
@@ -7854,7 +7854,7 @@ msgstr "Không tìm thấy nội dung nào cho hashtag."
|
|||||||
|
|
||||||
#: src/screens/Topic.tsx:178
|
#: src/screens/Topic.tsx:178
|
||||||
msgid "We couldn't find any results for that topic."
|
msgid "We couldn't find any results for that topic."
|
||||||
msgstr ""
|
msgstr "Chúng tôi không tìm thấy kết quả nào cho chủ đề đó."
|
||||||
|
|
||||||
#: src/screens/Messages/Conversation.tsx:103
|
#: src/screens/Messages/Conversation.tsx:103
|
||||||
msgid "We couldn't load this conversation"
|
msgid "We couldn't load this conversation"
|
||||||
@@ -7952,7 +7952,7 @@ msgstr "Bạn muốn gọi gói khởi đầu của mình là gì?"
|
|||||||
|
|
||||||
#: src/screens/Search/components/ExploreTrendingTopics.tsx:79
|
#: src/screens/Search/components/ExploreTrendingTopics.tsx:79
|
||||||
msgid "What people are posting about."
|
msgid "What people are posting about."
|
||||||
msgstr ""
|
msgstr "Chủ đề mà mọi người đang bàn luận."
|
||||||
|
|
||||||
#: src/view/com/auth/SplashScreen.tsx:38
|
#: src/view/com/auth/SplashScreen.tsx:38
|
||||||
#: src/view/com/auth/SplashScreen.web.tsx:99
|
#: src/view/com/auth/SplashScreen.web.tsx:99
|
||||||
@@ -8107,7 +8107,7 @@ msgstr "Bạn có thể kích hoạt lại tài khoản của mình để tiếp
|
|||||||
#: src/screens/Search/components/ExploreTrendingTopics.tsx:136
|
#: src/screens/Search/components/ExploreTrendingTopics.tsx:136
|
||||||
#: src/view/shell/desktop/SidebarTrendingTopics.tsx:109
|
#: src/view/shell/desktop/SidebarTrendingTopics.tsx:109
|
||||||
msgid "You can update this later from your settings."
|
msgid "You can update this later from your settings."
|
||||||
msgstr ""
|
msgstr "Bạn có thể cập nhật lại sau trong phần cài đặt."
|
||||||
|
|
||||||
#: src/view/com/profile/ProfileFollowers.tsx:94
|
#: src/view/com/profile/ProfileFollowers.tsx:94
|
||||||
msgid "You do not have any followers."
|
msgid "You do not have any followers."
|
||||||
@@ -8325,7 +8325,7 @@ msgstr "Bạn đã chọn ẩn một từ hoặc thẻ trong bài đăng này."
|
|||||||
|
|
||||||
#: src/state/shell/progress-guide.tsx:234
|
#: src/state/shell/progress-guide.tsx:234
|
||||||
msgid "You've found some people to follow"
|
msgid "You've found some people to follow"
|
||||||
msgstr ""
|
msgstr "Bạn đã tìm thấy một số người để theo dõi"
|
||||||
|
|
||||||
#: src/view/com/posts/FollowingEndOfFeed.tsx:44
|
#: src/view/com/posts/FollowingEndOfFeed.tsx:44
|
||||||
msgid "You've reached the end of your feed! Find some more accounts to follow."
|
msgid "You've reached the end of your feed! Find some more accounts to follow."
|
||||||
@@ -8373,7 +8373,7 @@ msgstr "Lựa chọn của bạn sẽ được lưu, nhưng có thể được t
|
|||||||
|
|
||||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:496
|
#: src/screens/Settings/components/ChangeHandleDialog.tsx:496
|
||||||
msgid "Your current handle <0>{0}</0> will automatically remain reserved for you. You can switch back to it at any time from this account."
|
msgid "Your current handle <0>{0}</0> will automatically remain reserved for you. You can switch back to it at any time from this account."
|
||||||
msgstr ""
|
msgstr "Tên người dùng hiện tại của bạn <0>{0}</0> sẽ tự động được giữ lại cho bạn. Bạn có thể chuyển lại vào bất kỳ lúc nào từ tài khoản này."
|
||||||
|
|
||||||
#: src/screens/Login/ForgotPasswordForm.tsx:51
|
#: src/screens/Login/ForgotPasswordForm.tsx:51
|
||||||
#: src/screens/Signup/state.ts:203
|
#: src/screens/Signup/state.ts:203
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1
-13
@@ -1,4 +1,4 @@
|
|||||||
import {GestureResponderEvent, Linking} from 'react-native'
|
import {Linking} from 'react-native'
|
||||||
|
|
||||||
import {isNative, isWeb} from './detection'
|
import {isNative, isWeb} from './detection'
|
||||||
|
|
||||||
@@ -24,15 +24,3 @@ export function clearHash() {
|
|||||||
window.location.hash = ''
|
window.location.hash = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shouldClickOpenNewTab(e: GestureResponderEvent) {
|
|
||||||
/**
|
|
||||||
* A `GestureResponderEvent`, but cast to `any` to avoid using a bunch
|
|
||||||
* of @ts-ignore below.
|
|
||||||
*/
|
|
||||||
const event = e as any
|
|
||||||
const isMiddleClick = isWeb && event.button === 1
|
|
||||||
const isMetaKey =
|
|
||||||
isWeb && (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey)
|
|
||||||
return isMetaKey || isMiddleClick
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -64,4 +64,5 @@ export const router = new Router({
|
|||||||
StarterPack: '/starter-pack/:name/:rkey',
|
StarterPack: '/starter-pack/:name/:rkey',
|
||||||
StarterPackShort: '/starter-pack-short/:code',
|
StarterPackShort: '/starter-pack-short/:code',
|
||||||
StarterPackWizard: '/starter-pack/create',
|
StarterPackWizard: '/starter-pack/create',
|
||||||
|
VideoFeed: '/video-feed',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {logger} from '#/logger'
|
|||||||
import {isNative} from '#/platform/detection'
|
import {isNative} from '#/platform/detection'
|
||||||
import {MESSAGE_SCREEN_POLL_INTERVAL} from '#/state/messages/convo/const'
|
import {MESSAGE_SCREEN_POLL_INTERVAL} from '#/state/messages/convo/const'
|
||||||
import {useMessagesEventBus} from '#/state/messages/events'
|
import {useMessagesEventBus} from '#/state/messages/events'
|
||||||
|
import {useLeftConvos} from '#/state/queries/messages/leave-conversation'
|
||||||
import {useListConvosQuery} from '#/state/queries/messages/list-conversations'
|
import {useListConvosQuery} from '#/state/queries/messages/list-conversations'
|
||||||
import {List} from '#/view/com/util/List'
|
import {List} from '#/view/com/util/List'
|
||||||
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
||||||
@@ -94,12 +95,19 @@ export function MessagesScreen({navigation, route}: Props) {
|
|||||||
|
|
||||||
useRefreshOnFocus(refetch)
|
useRefreshOnFocus(refetch)
|
||||||
|
|
||||||
|
const leftConvos = useLeftConvos()
|
||||||
|
|
||||||
const conversations = useMemo(() => {
|
const conversations = useMemo(() => {
|
||||||
if (data?.pages) {
|
if (data?.pages) {
|
||||||
return data.pages.flatMap(page => page.convos)
|
return (
|
||||||
|
data.pages
|
||||||
|
.flatMap(page => page.convos)
|
||||||
|
// filter out convos that are actively being left
|
||||||
|
.filter(convo => !leftConvos.includes(convo.id))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return []
|
return []
|
||||||
}, [data])
|
}, [data, leftConvos])
|
||||||
|
|
||||||
const onRefresh = useCallback(async () => {
|
const onRefresh = useCallback(async () => {
|
||||||
setIsPTRing(true)
|
setIsPTRing(true)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
BSKY_APP_ACCOUNT_DID,
|
BSKY_APP_ACCOUNT_DID,
|
||||||
DISCOVER_SAVED_FEED,
|
DISCOVER_SAVED_FEED,
|
||||||
TIMELINE_SAVED_FEED,
|
TIMELINE_SAVED_FEED,
|
||||||
|
VIDEO_SAVED_FEED,
|
||||||
} from '#/lib/constants'
|
} from '#/lib/constants'
|
||||||
import {useRequestNotificationsPermission} from '#/lib/notifications/notifications'
|
import {useRequestNotificationsPermission} from '#/lib/notifications/notifications'
|
||||||
import {logEvent, useGate} from '#/lib/statsig/statsig'
|
import {logEvent, useGate} from '#/lib/statsig/statsig'
|
||||||
@@ -111,6 +112,12 @@ export function StepFinished() {
|
|||||||
id: TID.nextStr(),
|
id: TID.nextStr(),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
if (gate('onboarding_add_video_feed')) {
|
||||||
|
feedsToSave.push({
|
||||||
|
...VIDEO_SAVED_FEED,
|
||||||
|
id: TID.nextStr(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Any starter pack feeds will be pinned _after_ the defaults
|
// Any starter pack feeds will be pinned _after_ the defaults
|
||||||
if (starterPack && starterPack.feeds?.length) {
|
if (starterPack && starterPack.feeds?.length) {
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import React, {useCallback, useMemo} from 'react'
|
import React, {useCallback, useMemo} from 'react'
|
||||||
import {StyleSheet, View} from 'react-native'
|
import {StyleSheet, View} from 'react-native'
|
||||||
import {useAnimatedRef} from 'react-native-reanimated'
|
import {useAnimatedRef} from 'react-native-reanimated'
|
||||||
|
import {AppBskyFeedDefs} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useIsFocused, useNavigation} from '@react-navigation/native'
|
import {useIsFocused, useNavigation} from '@react-navigation/native'
|
||||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {VIDEO_FEED_URIS} from '#/lib/constants'
|
||||||
import {usePalette} from '#/lib/hooks/usePalette'
|
import {usePalette} from '#/lib/hooks/usePalette'
|
||||||
import {useSetTitle} from '#/lib/hooks/useSetTitle'
|
import {useSetTitle} from '#/lib/hooks/useSetTitle'
|
||||||
import {ComposeIcon2} from '#/lib/icons'
|
import {ComposeIcon2} from '#/lib/icons'
|
||||||
@@ -18,7 +20,7 @@ import {isNative} from '#/platform/detection'
|
|||||||
import {listenSoftReset} from '#/state/events'
|
import {listenSoftReset} from '#/state/events'
|
||||||
import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
|
import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
|
||||||
import {FeedSourceFeedInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
|
import {FeedSourceFeedInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
|
||||||
import {FeedDescriptor} from '#/state/queries/post-feed'
|
import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed'
|
||||||
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
|
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
|
||||||
import {
|
import {
|
||||||
usePreferencesQuery,
|
usePreferencesQuery,
|
||||||
@@ -46,6 +48,11 @@ type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFeed'>
|
|||||||
export function ProfileFeedScreen(props: Props) {
|
export function ProfileFeedScreen(props: Props) {
|
||||||
const {rkey, name: handleOrDid} = props.route.params
|
const {rkey, name: handleOrDid} = props.route.params
|
||||||
|
|
||||||
|
const feedParams: FeedParams | undefined = props.route.params.feedCacheKey
|
||||||
|
? {
|
||||||
|
feedCacheKey: props.route.params.feedCacheKey,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
@@ -96,7 +103,10 @@ export function ProfileFeedScreen(props: Props) {
|
|||||||
|
|
||||||
return resolvedUri ? (
|
return resolvedUri ? (
|
||||||
<Layout.Screen>
|
<Layout.Screen>
|
||||||
<ProfileFeedScreenIntermediate feedUri={resolvedUri.uri} />
|
<ProfileFeedScreenIntermediate
|
||||||
|
feedUri={resolvedUri.uri}
|
||||||
|
feedParams={feedParams}
|
||||||
|
/>
|
||||||
</Layout.Screen>
|
</Layout.Screen>
|
||||||
) : (
|
) : (
|
||||||
<Layout.Screen>
|
<Layout.Screen>
|
||||||
@@ -108,7 +118,13 @@ export function ProfileFeedScreen(props: Props) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) {
|
function ProfileFeedScreenIntermediate({
|
||||||
|
feedUri,
|
||||||
|
feedParams,
|
||||||
|
}: {
|
||||||
|
feedUri: string
|
||||||
|
feedParams: FeedParams | undefined
|
||||||
|
}) {
|
||||||
const {data: preferences} = usePreferencesQuery()
|
const {data: preferences} = usePreferencesQuery()
|
||||||
const {data: info} = useFeedSourceInfoQuery({uri: feedUri})
|
const {data: info} = useFeedSourceInfoQuery({uri: feedUri})
|
||||||
|
|
||||||
@@ -125,15 +141,18 @@ function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) {
|
|||||||
<ProfileFeedScreenInner
|
<ProfileFeedScreenInner
|
||||||
preferences={preferences}
|
preferences={preferences}
|
||||||
feedInfo={info as FeedSourceFeedInfo}
|
feedInfo={info as FeedSourceFeedInfo}
|
||||||
|
feedParams={feedParams}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProfileFeedScreenInner({
|
export function ProfileFeedScreenInner({
|
||||||
feedInfo,
|
feedInfo,
|
||||||
|
feedParams,
|
||||||
}: {
|
}: {
|
||||||
preferences: UsePreferencesQueryResponse
|
preferences: UsePreferencesQueryResponse
|
||||||
feedInfo: FeedSourceFeedInfo
|
feedInfo: FeedSourceFeedInfo
|
||||||
|
feedParams: FeedParams | undefined
|
||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {hasSession} = useSession()
|
const {hasSession} = useSession()
|
||||||
@@ -170,6 +189,14 @@ export function ProfileFeedScreenInner({
|
|||||||
return <EmptyState icon="hashtag" message={_(msg`This feed is empty.`)} />
|
return <EmptyState icon="hashtag" message={_(msg`This feed is empty.`)} />
|
||||||
}, [_])
|
}, [_])
|
||||||
|
|
||||||
|
const isVideoFeed = React.useMemo(() => {
|
||||||
|
const isBskyVideoFeed = VIDEO_FEED_URIS.includes(feedInfo.uri)
|
||||||
|
const feedIsVideoMode =
|
||||||
|
feedInfo.contentMode === AppBskyFeedDefs.CONTENTMODEVIDEO
|
||||||
|
const _isVideoFeed = isBskyVideoFeed || feedIsVideoMode
|
||||||
|
return isNative && _isVideoFeed
|
||||||
|
}, [feedInfo])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ProfileFeedHeader info={feedInfo} />
|
<ProfileFeedHeader info={feedInfo} />
|
||||||
@@ -177,12 +204,14 @@ export function ProfileFeedScreenInner({
|
|||||||
<FeedFeedbackProvider value={feedFeedback}>
|
<FeedFeedbackProvider value={feedFeedback}>
|
||||||
<PostFeed
|
<PostFeed
|
||||||
feed={feed}
|
feed={feed}
|
||||||
|
feedParams={feedParams}
|
||||||
pollInterval={60e3}
|
pollInterval={60e3}
|
||||||
disablePoll={hasNew}
|
disablePoll={hasNew}
|
||||||
onHasNew={setHasNew}
|
onHasNew={setHasNew}
|
||||||
scrollElRef={scrollElRef}
|
scrollElRef={scrollElRef}
|
||||||
onScrolledDownChange={setIsScrolledDown}
|
onScrolledDownChange={setIsScrolledDown}
|
||||||
renderEmptyState={renderPostsEmpty}
|
renderEmptyState={renderPostsEmpty}
|
||||||
|
isVideoFeed={isVideoFeed}
|
||||||
/>
|
/>
|
||||||
</FeedFeedbackProvider>
|
</FeedFeedbackProvider>
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export const ProfileFeedSection = React.forwardRef<
|
|||||||
const [hasNew, setHasNew] = React.useState(false)
|
const [hasNew, setHasNew] = React.useState(false)
|
||||||
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
|
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
|
||||||
const shouldUseAdjustedNumToRender = feed.endsWith('posts_and_author_threads')
|
const shouldUseAdjustedNumToRender = feed.endsWith('posts_and_author_threads')
|
||||||
|
const isVideoFeed = isNative && feed.endsWith('posts_with_video')
|
||||||
const adjustedInitialNumToRender = useInitialNumToRender({
|
const adjustedInitialNumToRender = useInitialNumToRender({
|
||||||
screenHeightOffset: headerHeight,
|
screenHeightOffset: headerHeight,
|
||||||
})
|
})
|
||||||
@@ -84,11 +85,12 @@ export const ProfileFeedSection = React.forwardRef<
|
|||||||
renderEmptyState={renderPostsEmpty}
|
renderEmptyState={renderPostsEmpty}
|
||||||
headerOffset={headerHeight}
|
headerOffset={headerHeight}
|
||||||
progressViewOffset={ios(0)}
|
progressViewOffset={ios(0)}
|
||||||
renderEndOfFeed={ProfileEndOfFeed}
|
renderEndOfFeed={isVideoFeed ? undefined : ProfileEndOfFeed}
|
||||||
ignoreFilterFor={ignoreFilterFor}
|
ignoreFilterFor={ignoreFilterFor}
|
||||||
initialNumToRender={
|
initialNumToRender={
|
||||||
shouldUseAdjustedNumToRender ? adjustedInitialNumToRender : undefined
|
shouldUseAdjustedNumToRender ? adjustedInitialNumToRender : undefined
|
||||||
}
|
}
|
||||||
|
isVideoFeed={isVideoFeed}
|
||||||
/>
|
/>
|
||||||
{(isScrolledDown || hasNew) && (
|
{(isScrolledDown || hasNew) && (
|
||||||
<LoadLatestBtn
|
<LoadLatestBtn
|
||||||
|
|||||||
@@ -0,0 +1,271 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {ScrollView, View} from 'react-native'
|
||||||
|
import {AppBskyEmbedVideo, AtUri} from '@atproto/api'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {useFocusEffect} from '@react-navigation/native'
|
||||||
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {VIDEO_FEED_URI} from '#/lib/constants'
|
||||||
|
import {makeCustomFeedLink} from '#/lib/routes/links'
|
||||||
|
import {logEvent} from '#/lib/statsig/statsig'
|
||||||
|
import {isWeb} from '#/platform/detection'
|
||||||
|
import {useSavedFeeds} from '#/state/queries/feed'
|
||||||
|
import {RQKEY, usePostFeedQuery} from '#/state/queries/post-feed'
|
||||||
|
import {useAddSavedFeedsMutation} from '#/state/queries/preferences'
|
||||||
|
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
|
||||||
|
import {atoms as a, tokens, useGutters, useTheme} from '#/alf'
|
||||||
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
|
import {GradientFill} from '#/components/GradientFill'
|
||||||
|
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
||||||
|
import {Pin_Stroke2_Corner0_Rounded as Pin} from '#/components/icons/Pin'
|
||||||
|
import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending2'
|
||||||
|
import {Link} from '#/components/Link'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
import {
|
||||||
|
CompactVideoPostCard,
|
||||||
|
CompactVideoPostCardPlaceholder,
|
||||||
|
} from '#/components/VideoPostCard'
|
||||||
|
|
||||||
|
const CARD_WIDTH = 100
|
||||||
|
|
||||||
|
const FEED_DESC = `feedgen|${VIDEO_FEED_URI}`
|
||||||
|
const FEED_PARAMS: {
|
||||||
|
feedCacheKey: 'explore'
|
||||||
|
} = {
|
||||||
|
feedCacheKey: 'explore',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ExploreTrendingVideos() {
|
||||||
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
|
const gutters = useGutters([0, 'base'])
|
||||||
|
const {data, isLoading, error} = usePostFeedQuery(FEED_DESC, FEED_PARAMS)
|
||||||
|
|
||||||
|
// Refetch on tab change if nothing else is using this query.
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
useFocusEffect(() => {
|
||||||
|
return () => {
|
||||||
|
const query = queryClient
|
||||||
|
.getQueryCache()
|
||||||
|
.find({queryKey: RQKEY(FEED_DESC, FEED_PARAMS)})
|
||||||
|
if (query && query.getObserversCount() <= 1) {
|
||||||
|
query.fetch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const {data: saved} = useSavedFeeds()
|
||||||
|
const isSavedAlready = React.useMemo(() => {
|
||||||
|
return !!saved?.feeds?.some(info => info.config.value === VIDEO_FEED_URI)
|
||||||
|
}, [saved])
|
||||||
|
|
||||||
|
const {mutateAsync: addSavedFeeds, isPending: isPinPending} =
|
||||||
|
useAddSavedFeedsMutation()
|
||||||
|
const pinFeed = React.useCallback(
|
||||||
|
(e: any) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
addSavedFeeds([
|
||||||
|
{
|
||||||
|
type: 'feed',
|
||||||
|
value: VIDEO_FEED_URI,
|
||||||
|
pinned: true,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
// prevent navigation
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
[addSavedFeeds],
|
||||||
|
)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[a.pb_xl]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_row,
|
||||||
|
isWeb
|
||||||
|
? [a.px_lg, a.py_lg, a.pt_2xl, a.gap_md]
|
||||||
|
: [a.p_lg, a.pt_xl, a.gap_md],
|
||||||
|
a.border_b,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
]}>
|
||||||
|
<View style={[a.flex_1, a.gap_sm]}>
|
||||||
|
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
|
||||||
|
<Graph
|
||||||
|
size="lg"
|
||||||
|
fill={t.palette.primary_500}
|
||||||
|
style={{marginLeft: -2}}
|
||||||
|
/>
|
||||||
|
<Text style={[a.text_2xl, a.font_heavy, t.atoms.text]}>
|
||||||
|
<Trans>Trending Videos</Trans>
|
||||||
|
</Text>
|
||||||
|
<View style={[a.py_xs, a.px_sm, a.rounded_sm, a.overflow_hidden]}>
|
||||||
|
<GradientFill gradient={tokens.gradients.primary} />
|
||||||
|
<Text style={[a.text_sm, a.font_heavy, {color: 'white'}]}>
|
||||||
|
<Trans>BETA</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<Text style={[t.atoms.text_contrast_high, a.leading_snug]}>
|
||||||
|
<Trans>Popular videos in your network.</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<BlockDrawerGesture>
|
||||||
|
<ScrollView
|
||||||
|
horizontal
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
decelerationRate="fast"
|
||||||
|
snapToInterval={CARD_WIDTH + tokens.space.sm}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.pt_lg,
|
||||||
|
a.flex_row,
|
||||||
|
a.gap_sm,
|
||||||
|
{
|
||||||
|
paddingLeft: gutters.paddingLeft,
|
||||||
|
paddingRight: gutters.paddingRight,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
{isLoading ? (
|
||||||
|
Array(10)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, i) => (
|
||||||
|
<View key={i} style={[{width: CARD_WIDTH}]}>
|
||||||
|
<CompactVideoPostCardPlaceholder />
|
||||||
|
</View>
|
||||||
|
))
|
||||||
|
) : error || !data ? (
|
||||||
|
<Text>
|
||||||
|
<Trans>Whoops! Trending videos failed to load.</Trans>
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<VideoCards data={data} />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</BlockDrawerGesture>
|
||||||
|
|
||||||
|
{!isSavedAlready && (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
gutters,
|
||||||
|
a.pt_lg,
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.justify_between,
|
||||||
|
a.gap_xl,
|
||||||
|
]}>
|
||||||
|
<Text style={[a.flex_1, a.text_sm, a.leading_snug]}>
|
||||||
|
<Trans>
|
||||||
|
Pin the trending videos feed to your home screen for easy access
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
disabled={isPinPending}
|
||||||
|
label={_(msg`Pin`)}
|
||||||
|
size="small"
|
||||||
|
variant="outline"
|
||||||
|
color="secondary"
|
||||||
|
onPress={pinFeed}>
|
||||||
|
<ButtonText>{_(msg`Pin`)}</ButtonText>
|
||||||
|
<ButtonIcon icon={Pin} position="right" />
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function VideoCards({
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
data: Exclude<ReturnType<typeof usePostFeedQuery>['data'], undefined>
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
|
const items = React.useMemo(() => {
|
||||||
|
return data.pages
|
||||||
|
.flatMap(page => page.slices)
|
||||||
|
.map(slice => slice.items[0])
|
||||||
|
.filter(Boolean)
|
||||||
|
.filter(item => AppBskyEmbedVideo.isView(item.post.embed))
|
||||||
|
.slice(0, 8)
|
||||||
|
}, [data])
|
||||||
|
const href = React.useMemo(() => {
|
||||||
|
const urip = new AtUri(VIDEO_FEED_URI)
|
||||||
|
return makeCustomFeedLink(urip.host, urip.rkey, undefined, 'explore')
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{items.map(item => (
|
||||||
|
<View key={item.post.uri} style={[{width: CARD_WIDTH}]}>
|
||||||
|
<CompactVideoPostCard
|
||||||
|
post={item.post}
|
||||||
|
moderation={item.moderation}
|
||||||
|
sourceContext={{
|
||||||
|
type: 'feedgen',
|
||||||
|
uri: VIDEO_FEED_URI,
|
||||||
|
sourceInterstitial: 'explore',
|
||||||
|
}}
|
||||||
|
onInteract={() => {
|
||||||
|
logEvent('videoCard:click', {
|
||||||
|
context: 'interstitial:explore',
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<View style={[{width: CARD_WIDTH * 2}]}>
|
||||||
|
<Link
|
||||||
|
to={href}
|
||||||
|
label={_(msg`View more`)}
|
||||||
|
style={[
|
||||||
|
a.justify_center,
|
||||||
|
a.align_center,
|
||||||
|
a.flex_1,
|
||||||
|
a.rounded_md,
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
]}>
|
||||||
|
{({pressed}) => (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.gap_md,
|
||||||
|
{
|
||||||
|
opacity: pressed ? 0.6 : 1,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<Text style={[a.text_md]}>
|
||||||
|
<Trans>View more</Trans>
|
||||||
|
</Text>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.align_center,
|
||||||
|
a.justify_center,
|
||||||
|
a.rounded_full,
|
||||||
|
{
|
||||||
|
width: 34,
|
||||||
|
height: 34,
|
||||||
|
backgroundColor: t.palette.primary_500,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<ButtonIcon icon={ChevronRight} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -37,8 +37,9 @@ export function ContentAndMediaSettingsScreen({}: Props) {
|
|||||||
const inAppBrowserPref = useInAppBrowser()
|
const inAppBrowserPref = useInAppBrowser()
|
||||||
const setUseInAppBrowser = useSetInAppBrowser()
|
const setUseInAppBrowser = useSetInAppBrowser()
|
||||||
const {enabled: trendingEnabled} = useTrendingConfig()
|
const {enabled: trendingEnabled} = useTrendingConfig()
|
||||||
const {trendingDisabled} = useTrendingSettings()
|
const {trendingDisabled, trendingVideoDisabled} = useTrendingSettings()
|
||||||
const {setTrendingDisabled} = useTrendingSettingsApi()
|
const {setTrendingDisabled, setTrendingVideoDisabled} =
|
||||||
|
useTrendingSettingsApi()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout.Screen>
|
<Layout.Screen>
|
||||||
@@ -138,6 +139,27 @@ export function ContentAndMediaSettingsScreen({}: Props) {
|
|||||||
<Toggle.Platform />
|
<Toggle.Platform />
|
||||||
</SettingsList.Item>
|
</SettingsList.Item>
|
||||||
</Toggle.Item>
|
</Toggle.Item>
|
||||||
|
<Toggle.Item
|
||||||
|
name="show_trending_videos"
|
||||||
|
label={_(msg`Enable trending videos in your Discover feed.`)}
|
||||||
|
value={!trendingVideoDisabled}
|
||||||
|
onChange={value => {
|
||||||
|
const hide = Boolean(!value)
|
||||||
|
if (hide) {
|
||||||
|
logEvent('trendingVideos:hide', {context: 'settings'})
|
||||||
|
} else {
|
||||||
|
logEvent('trendingVideos:show', {context: 'settings'})
|
||||||
|
}
|
||||||
|
setTrendingVideoDisabled(hide)
|
||||||
|
}}>
|
||||||
|
<SettingsList.Item>
|
||||||
|
<SettingsList.ItemIcon icon={Graph} />
|
||||||
|
<SettingsList.ItemText>
|
||||||
|
<Trans>Enable trending videos in your Discover feed</Trans>
|
||||||
|
</SettingsList.ItemText>
|
||||||
|
<Toggle.Platform />
|
||||||
|
</SettingsList.Item>
|
||||||
|
</Toggle.Item>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</SettingsList.Container>
|
</SettingsList.Container>
|
||||||
|
|||||||
@@ -80,9 +80,8 @@ export function StepCaptcha() {
|
|||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.w_full,
|
a.w_full,
|
||||||
a.pb_xl,
|
|
||||||
a.overflow_hidden,
|
a.overflow_hidden,
|
||||||
{minHeight: 500},
|
{minHeight: 510},
|
||||||
completed && [a.align_center, a.justify_center],
|
completed && [a.align_center, a.justify_center],
|
||||||
]}>
|
]}>
|
||||||
{!completed ? (
|
{!completed ? (
|
||||||
|
|||||||
@@ -85,6 +85,21 @@ export function SignupQueued() {
|
|||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const logoutBtn = (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="large"
|
||||||
|
color="primary"
|
||||||
|
label={_(msg`Log out`)}
|
||||||
|
onPress={() => logoutCurrentAccount('SignupQueued')}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Log out</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
|
||||||
|
const webLayout = isWeb && gtMobile
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
visible
|
visible
|
||||||
@@ -108,7 +123,7 @@ export function SignupQueued() {
|
|||||||
<Logo width={120} />
|
<Logo width={120} />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Text style={[a.text_4xl, a.font_bold, a.pb_sm]}>
|
<Text style={[a.text_4xl, a.font_heavy, a.pb_sm]}>
|
||||||
<Trans>You're in line</Trans>
|
<Trans>You're in line</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<P style={[t.atoms.text_contrast_medium]}>
|
<P style={[t.atoms.text_contrast_medium]}>
|
||||||
@@ -153,7 +168,7 @@ export function SignupQueued() {
|
|||||||
</P>
|
</P>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{isWeb && gtMobile && (
|
{webLayout && (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.w_full,
|
a.w_full,
|
||||||
@@ -162,15 +177,7 @@ export function SignupQueued() {
|
|||||||
a.pt_5xl,
|
a.pt_5xl,
|
||||||
{paddingBottom: 200},
|
{paddingBottom: 200},
|
||||||
]}>
|
]}>
|
||||||
<Button
|
{logoutBtn}
|
||||||
variant="ghost"
|
|
||||||
size="large"
|
|
||||||
label={_(msg`Log out`)}
|
|
||||||
onPress={() => logoutCurrentAccount('SignupQueued')}>
|
|
||||||
<ButtonText style={[{color: t.palette.primary_500}]}>
|
|
||||||
<Trans>Log out</Trans>
|
|
||||||
</ButtonText>
|
|
||||||
</Button>
|
|
||||||
{checkBtn}
|
{checkBtn}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
@@ -178,27 +185,17 @@ export function SignupQueued() {
|
|||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
{(!isWeb || !gtMobile) && (
|
{!webLayout && (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.align_center,
|
a.align_center,
|
||||||
t.atoms.bg,
|
t.atoms.bg,
|
||||||
gtMobile ? a.px_5xl : a.px_xl,
|
gtMobile ? a.px_5xl : a.px_xl,
|
||||||
{
|
{paddingBottom: Math.max(insets.bottom, a.pb_5xl.paddingBottom)},
|
||||||
paddingBottom: Math.max(insets.bottom, a.pb_5xl.paddingBottom),
|
|
||||||
},
|
|
||||||
]}>
|
]}>
|
||||||
<View style={[a.w_full, a.gap_sm, {maxWidth: COL_WIDTH}]}>
|
<View style={[a.w_full, a.gap_sm, {maxWidth: COL_WIDTH}]}>
|
||||||
{checkBtn}
|
{checkBtn}
|
||||||
<Button
|
{logoutBtn}
|
||||||
variant="ghost"
|
|
||||||
size="large"
|
|
||||||
label={_(msg`Log out`)}
|
|
||||||
onPress={() => logoutCurrentAccount('SignupQueued')}>
|
|
||||||
<ButtonText style={[{color: t.palette.primary_500}]}>
|
|
||||||
<Trans>Log out</Trans>
|
|
||||||
</ButtonText>
|
|
||||||
</Button>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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 {AppBskyFeedDefs} from '@atproto/api'
|
||||||
import throttle from 'lodash.throttle'
|
import throttle from 'lodash.throttle'
|
||||||
|
|
||||||
import {PROD_DEFAULT_FEED} from '#/lib/constants'
|
import {FEEDBACK_FEEDS, STAGING_FEEDS} from '#/lib/constants'
|
||||||
import {logEvent} from '#/lib/statsig/statsig'
|
import {logEvent} from '#/lib/statsig/statsig'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {FeedDescriptor, FeedPostSliceItem} from '#/state/queries/post-feed'
|
import {FeedDescriptor, FeedPostSliceItem} from '#/state/queries/post-feed'
|
||||||
import {getFeedPostSlice} from '#/view/com/posts/PostFeed'
|
import {getItemsForFeedback} from '#/view/com/posts/PostFeed'
|
||||||
import {useAgent} from './session'
|
import {useAgent} from './session'
|
||||||
|
|
||||||
type StateContext = {
|
type StateContext = {
|
||||||
@@ -25,6 +25,7 @@ const stateContext = React.createContext<StateContext>({
|
|||||||
export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
|
export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
const enabled = isDiscoverFeed(feed) && hasSession
|
const enabled = isDiscoverFeed(feed) && hasSession
|
||||||
|
|
||||||
const queue = React.useRef<Set<string>>(new Set())
|
const queue = React.useRef<Set<string>>(new Set())
|
||||||
const history = React.useRef<
|
const history = React.useRef<
|
||||||
// Use a WeakSet so that we don't need to clear it.
|
// Use a WeakSet so that we don't need to clear it.
|
||||||
@@ -46,6 +47,11 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
|
|||||||
const interactions = Array.from(queue.current).map(toInteraction)
|
const interactions = Array.from(queue.current).map(toInteraction)
|
||||||
queue.current.clear()
|
queue.current.clear()
|
||||||
|
|
||||||
|
let proxyDid = 'did:web:discover.bsky.app'
|
||||||
|
if (STAGING_FEEDS.includes(feed)) {
|
||||||
|
proxyDid = 'did:web:algo.pop2.bsky.app'
|
||||||
|
}
|
||||||
|
|
||||||
// Send to the feed
|
// Send to the feed
|
||||||
agent.app.bsky.feed
|
agent.app.bsky.feed
|
||||||
.sendInteractions(
|
.sendInteractions(
|
||||||
@@ -54,7 +60,7 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
|
|||||||
encoding: 'application/json',
|
encoding: 'application/json',
|
||||||
headers: {
|
headers: {
|
||||||
// TODO when we start sending to other feeds, we need to grab their DID -prf
|
// TODO when we start sending to other feeds, we need to grab their DID -prf
|
||||||
'atproto-proxy': 'did:web:discover.bsky.app#bsky_fg',
|
'atproto-proxy': `${proxyDid}#bsky_fg`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -68,7 +74,7 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
|
|||||||
}
|
}
|
||||||
sendOrAggregateInteractionsForStats(aggregatedStats.current, interactions)
|
sendOrAggregateInteractionsForStats(aggregatedStats.current, interactions)
|
||||||
throttledFlushAggregatedStats()
|
throttledFlushAggregatedStats()
|
||||||
}, [agent, throttledFlushAggregatedStats])
|
}, [agent, throttledFlushAggregatedStats, feed])
|
||||||
|
|
||||||
const sendToFeed = React.useMemo(
|
const sendToFeed = React.useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -96,18 +102,15 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
|
|||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const slice = getFeedPostSlice(feedItem)
|
const items = getItemsForFeedback(feedItem)
|
||||||
if (slice === null) {
|
for (const {item: postItem, feedContext} of items) {
|
||||||
return
|
|
||||||
}
|
|
||||||
for (const postItem of slice.items) {
|
|
||||||
if (!history.current.has(postItem)) {
|
if (!history.current.has(postItem)) {
|
||||||
history.current.add(postItem)
|
history.current.add(postItem)
|
||||||
queue.current.add(
|
queue.current.add(
|
||||||
toString({
|
toString({
|
||||||
item: postItem.uri,
|
item: postItem.uri,
|
||||||
event: 'app.bsky.feed.defs#interactionSeen',
|
event: 'app.bsky.feed.defs#interactionSeen',
|
||||||
feedContext: slice.feedContext,
|
feedContext,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
sendToFeed()
|
sendToFeed()
|
||||||
@@ -155,7 +158,7 @@ export function useFeedFeedbackContext() {
|
|||||||
// place, we're hardcoding it to the discover feed.
|
// place, we're hardcoding it to the discover feed.
|
||||||
// -prf
|
// -prf
|
||||||
function isDiscoverFeed(feed: FeedDescriptor) {
|
function isDiscoverFeed(feed: FeedDescriptor) {
|
||||||
return feed === `feedgen|${PROD_DEFAULT_FEED('whats-hot')}`
|
return FEEDBACK_FEEDS.includes(feed)
|
||||||
}
|
}
|
||||||
|
|
||||||
function toString(interaction: AppBskyFeedDefs.Interaction): string {
|
function toString(interaction: AppBskyFeedDefs.Interaction): string {
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ const schema = z.object({
|
|||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
mutedThreads: z.array(z.string()),
|
mutedThreads: z.array(z.string()),
|
||||||
trendingDisabled: z.boolean().optional(),
|
trendingDisabled: z.boolean().optional(),
|
||||||
|
trendingVideoDisabled: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
export type Schema = z.infer<typeof schema>
|
export type Schema = z.infer<typeof schema>
|
||||||
|
|
||||||
@@ -172,6 +173,7 @@ export const defaults: Schema = {
|
|||||||
hasCheckedForStarterPack: false,
|
hasCheckedForStarterPack: false,
|
||||||
subtitlesEnabled: true,
|
subtitlesEnabled: true,
|
||||||
trendingDisabled: false,
|
trendingDisabled: false,
|
||||||
|
trendingVideoDisabled: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tryParse(rawData: string): Schema | undefined {
|
export function tryParse(rawData: string): Schema | undefined {
|
||||||
|
|||||||
@@ -4,18 +4,27 @@ import * as persisted from '#/state/persisted'
|
|||||||
|
|
||||||
type StateContext = {
|
type StateContext = {
|
||||||
trendingDisabled: Exclude<persisted.Schema['trendingDisabled'], undefined>
|
trendingDisabled: Exclude<persisted.Schema['trendingDisabled'], undefined>
|
||||||
|
trendingVideoDisabled: Exclude<
|
||||||
|
persisted.Schema['trendingVideoDisabled'],
|
||||||
|
undefined
|
||||||
|
>
|
||||||
}
|
}
|
||||||
type ApiContext = {
|
type ApiContext = {
|
||||||
setTrendingDisabled(
|
setTrendingDisabled(
|
||||||
hidden: Exclude<persisted.Schema['trendingDisabled'], undefined>,
|
hidden: Exclude<persisted.Schema['trendingDisabled'], undefined>,
|
||||||
): void
|
): void
|
||||||
|
setTrendingVideoDisabled(
|
||||||
|
hidden: Exclude<persisted.Schema['trendingVideoDisabled'], undefined>,
|
||||||
|
): void
|
||||||
}
|
}
|
||||||
|
|
||||||
const StateContext = React.createContext<StateContext>({
|
const StateContext = React.createContext<StateContext>({
|
||||||
trendingDisabled: Boolean(persisted.defaults.trendingDisabled),
|
trendingDisabled: Boolean(persisted.defaults.trendingDisabled),
|
||||||
|
trendingVideoDisabled: Boolean(persisted.defaults.trendingVideoDisabled),
|
||||||
})
|
})
|
||||||
const ApiContext = React.createContext<ApiContext>({
|
const ApiContext = React.createContext<ApiContext>({
|
||||||
setTrendingDisabled() {},
|
setTrendingDisabled() {},
|
||||||
|
setTrendingVideoDisabled() {},
|
||||||
})
|
})
|
||||||
|
|
||||||
function usePersistedBooleanValue<T extends keyof persisted.Schema>(key: T) {
|
function usePersistedBooleanValue<T extends keyof persisted.Schema>(key: T) {
|
||||||
@@ -43,14 +52,19 @@ function usePersistedBooleanValue<T extends keyof persisted.Schema>(key: T) {
|
|||||||
export function Provider({children}: React.PropsWithChildren<{}>) {
|
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
const [trendingDisabled, setTrendingDisabled] =
|
const [trendingDisabled, setTrendingDisabled] =
|
||||||
usePersistedBooleanValue('trendingDisabled')
|
usePersistedBooleanValue('trendingDisabled')
|
||||||
|
const [trendingVideoDisabled, setTrendingVideoDisabled] =
|
||||||
|
usePersistedBooleanValue('trendingVideoDisabled')
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Context
|
* Context
|
||||||
*/
|
*/
|
||||||
const state = React.useMemo(() => ({trendingDisabled}), [trendingDisabled])
|
const state = React.useMemo(
|
||||||
|
() => ({trendingDisabled, trendingVideoDisabled}),
|
||||||
|
[trendingDisabled, trendingVideoDisabled],
|
||||||
|
)
|
||||||
const api = React.useMemo(
|
const api = React.useMemo(
|
||||||
() => ({setTrendingDisabled}),
|
() => ({setTrendingDisabled, setTrendingVideoDisabled}),
|
||||||
[setTrendingDisabled],
|
[setTrendingDisabled, setTrendingVideoDisabled],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export type FeedSourceFeedInfo = {
|
|||||||
creatorHandle: string
|
creatorHandle: string
|
||||||
likeCount: number | undefined
|
likeCount: number | undefined
|
||||||
likeUri: string | undefined
|
likeUri: string | undefined
|
||||||
|
contentMode: AppBskyFeedDefs.GeneratorView['contentMode']
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FeedSourceListInfo = {
|
export type FeedSourceListInfo = {
|
||||||
@@ -65,6 +66,7 @@ export type FeedSourceListInfo = {
|
|||||||
description: RichText
|
description: RichText
|
||||||
creatorDid: string
|
creatorDid: string
|
||||||
creatorHandle: string
|
creatorHandle: string
|
||||||
|
contentMode: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FeedSourceInfo = FeedSourceFeedInfo | FeedSourceListInfo
|
export type FeedSourceInfo = FeedSourceFeedInfo | FeedSourceListInfo
|
||||||
@@ -111,6 +113,7 @@ export function hydrateFeedGenerator(
|
|||||||
creatorHandle: view.creator.handle,
|
creatorHandle: view.creator.handle,
|
||||||
likeCount: view.likeCount,
|
likeCount: view.likeCount,
|
||||||
likeUri: view.viewer?.like,
|
likeUri: view.viewer?.like,
|
||||||
|
contentMode: view.contentMode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,6 +144,7 @@ export function hydrateList(view: AppBskyGraphDefs.ListView): FeedSourceInfo {
|
|||||||
displayName: view.name
|
displayName: view.name
|
||||||
? sanitizeDisplayName(view.name)
|
? sanitizeDisplayName(view.name)
|
||||||
: `User List by ${sanitizeHandle(view.creator.handle, '@')}`,
|
: `User List by ${sanitizeHandle(view.creator.handle, '@')}`,
|
||||||
|
contentMode: undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,6 +403,7 @@ const PWI_DISCOVER_FEED_STUB: SavedFeedSourceInfo = {
|
|||||||
id: 'pwi-discover',
|
id: 'pwi-discover',
|
||||||
...DISCOVER_SAVED_FEED,
|
...DISCOVER_SAVED_FEED,
|
||||||
},
|
},
|
||||||
|
contentMode: undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
const pinnedFeedInfosQueryKeyRoot = 'pinnedFeedsInfos'
|
const pinnedFeedInfosQueryKeyRoot = 'pinnedFeedsInfos'
|
||||||
@@ -485,6 +490,7 @@ export function usePinnedFeedsInfos() {
|
|||||||
likeCount: 0,
|
likeCount: 0,
|
||||||
likeUri: '',
|
likeUri: '',
|
||||||
savedFeed: pinnedItem,
|
savedFeed: pinnedItem,
|
||||||
|
contentMode: undefined,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,28 @@
|
|||||||
import {ChatBskyConvoLeaveConvo, ChatBskyConvoListConvos} from '@atproto/api'
|
import {ChatBskyConvoLeaveConvo, ChatBskyConvoListConvos} from '@atproto/api'
|
||||||
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
import {
|
||||||
|
useMutation,
|
||||||
|
useMutationState,
|
||||||
|
useQueryClient,
|
||||||
|
} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
|
import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
|
||||||
import {useAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
import {RQKEY as CONVO_LIST_KEY} from './list-conversations'
|
import {RQKEY as CONVO_LIST_KEY} from './list-conversations'
|
||||||
|
|
||||||
|
const RQKEY_ROOT = 'leave-convo'
|
||||||
|
export function RQKEY(convoId: string | undefined) {
|
||||||
|
return [RQKEY_ROOT, convoId]
|
||||||
|
}
|
||||||
|
|
||||||
export function useLeaveConvo(
|
export function useLeaveConvo(
|
||||||
convoId: string | undefined,
|
convoId: string | undefined,
|
||||||
{
|
{
|
||||||
onSuccess,
|
onSuccess,
|
||||||
|
onMutate,
|
||||||
onError,
|
onError,
|
||||||
}: {
|
}: {
|
||||||
|
onMutate?: () => void
|
||||||
onSuccess?: (data: ChatBskyConvoLeaveConvo.OutputSchema) => void
|
onSuccess?: (data: ChatBskyConvoLeaveConvo.OutputSchema) => void
|
||||||
onError?: (error: Error) => void
|
onError?: (error: Error) => void
|
||||||
},
|
},
|
||||||
@@ -20,6 +31,7 @@ export function useLeaveConvo(
|
|||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
|
mutationKey: RQKEY(convoId),
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
if (!convoId) throw new Error('No convoId provided')
|
if (!convoId) throw new Error('No convoId provided')
|
||||||
|
|
||||||
@@ -51,6 +63,7 @@ export function useLeaveConvo(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
onMutate?.()
|
||||||
return {prevPages}
|
return {prevPages}
|
||||||
},
|
},
|
||||||
onSuccess: data => {
|
onSuccess: data => {
|
||||||
@@ -77,3 +90,20 @@ export function useLeaveConvo(
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets currently pending and successful leave convo mutations
|
||||||
|
*
|
||||||
|
* @returns Array of `convoId`
|
||||||
|
*/
|
||||||
|
export function useLeftConvos() {
|
||||||
|
const pending = useMutationState({
|
||||||
|
filters: {mutationKey: [RQKEY_ROOT], status: 'pending'},
|
||||||
|
select: mutation => mutation.options.mutationKey?.[1] as string | undefined,
|
||||||
|
})
|
||||||
|
const success = useMutationState({
|
||||||
|
filters: {mutationKey: [RQKEY_ROOT], status: 'success'},
|
||||||
|
select: mutation => mutation.options.mutationKey?.[1] as string | undefined,
|
||||||
|
})
|
||||||
|
return [...pending, ...success].filter(id => id !== undefined)
|
||||||
|
}
|
||||||
|
|||||||
@@ -44,11 +44,12 @@ import {
|
|||||||
} from './util'
|
} from './util'
|
||||||
|
|
||||||
type ActorDid = string
|
type ActorDid = string
|
||||||
type AuthorFilter =
|
export type AuthorFilter =
|
||||||
| 'posts_with_replies'
|
| 'posts_with_replies'
|
||||||
| 'posts_no_replies'
|
| 'posts_no_replies'
|
||||||
| 'posts_and_author_threads'
|
| 'posts_and_author_threads'
|
||||||
| 'posts_with_media'
|
| 'posts_with_media'
|
||||||
|
| 'posts_with_video'
|
||||||
type FeedUri = string
|
type FeedUri = string
|
||||||
type ListUri = string
|
type ListUri = string
|
||||||
|
|
||||||
@@ -61,6 +62,7 @@ export type FeedDescriptor =
|
|||||||
export interface FeedParams {
|
export interface FeedParams {
|
||||||
mergeFeedEnabled?: boolean
|
mergeFeedEnabled?: boolean
|
||||||
mergeFeedSources?: string[]
|
mergeFeedSources?: string[]
|
||||||
|
feedCacheKey?: 'discover' | 'explore' | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
type RQPageParam = {cursor: string | undefined; api: FeedAPI} | undefined
|
type RQPageParam = {cursor: string | undefined; api: FeedAPI} | undefined
|
||||||
@@ -88,6 +90,7 @@ export interface FeedPostSlice {
|
|||||||
isIncompleteThread: boolean
|
isIncompleteThread: boolean
|
||||||
isFallbackMarker: boolean
|
isFallbackMarker: boolean
|
||||||
feedContext: string | undefined
|
feedContext: string | undefined
|
||||||
|
feedPostUri: string
|
||||||
reason?:
|
reason?:
|
||||||
| AppBskyFeedDefs.ReasonRepost
|
| AppBskyFeedDefs.ReasonRepost
|
||||||
| AppBskyFeedDefs.ReasonPin
|
| AppBskyFeedDefs.ReasonPin
|
||||||
@@ -329,6 +332,7 @@ export function usePostFeedQuery(
|
|||||||
isFallbackMarker: slice.isFallbackMarker,
|
isFallbackMarker: slice.isFallbackMarker,
|
||||||
feedContext: slice.feedContext,
|
feedContext: slice.feedContext,
|
||||||
reason: slice.reason,
|
reason: slice.reason,
|
||||||
|
feedPostUri: slice.feedPostUri,
|
||||||
items: slice.items.map((item, i) => {
|
items: slice.items.map((item, i) => {
|
||||||
const feedPostSliceItem: FeedPostSliceItem = {
|
const feedPostSliceItem: FeedPostSliceItem = {
|
||||||
_reactKey: `${slice._reactKey}-${i}-${item.post.uri}`,
|
_reactKey: `${slice._reactKey}-${i}-${item.post.uri}`,
|
||||||
|
|||||||
@@ -53,8 +53,9 @@ export type ThreadPost = {
|
|||||||
uri: string
|
uri: string
|
||||||
post: AppBskyFeedDefs.PostView
|
post: AppBskyFeedDefs.PostView
|
||||||
record: AppBskyFeedPost.Record
|
record: AppBskyFeedPost.Record
|
||||||
parent?: ThreadNode
|
parent: ThreadNode | undefined
|
||||||
replies?: ThreadNode[]
|
replies: ThreadNode[] | undefined
|
||||||
|
hasOPLike: boolean | undefined
|
||||||
ctx: ThreadCtx
|
ctx: ThreadCtx
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,8 +256,8 @@ export function sortThread(
|
|||||||
if (aFetchedAt !== bFetchedAt) {
|
if (aFetchedAt !== bFetchedAt) {
|
||||||
return aFetchedAt - bFetchedAt // older fetches first
|
return aFetchedAt - bFetchedAt // older fetches first
|
||||||
} else if (opts.sort === 'hotness') {
|
} else if (opts.sort === 'hotness') {
|
||||||
const aHotness = getHotness(a.post, aFetchedAt)
|
const aHotness = getHotness(a, aFetchedAt)
|
||||||
const bHotness = getHotness(b.post, bFetchedAt /* same as aFetchedAt */)
|
const bHotness = getHotness(b, bFetchedAt /* same as aFetchedAt */)
|
||||||
return bHotness - aHotness
|
return bHotness - aHotness
|
||||||
} else if (opts.sort === 'oldest') {
|
} else if (opts.sort === 'oldest') {
|
||||||
return a.post.indexedAt.localeCompare(b.post.indexedAt)
|
return a.post.indexedAt.localeCompare(b.post.indexedAt)
|
||||||
@@ -309,16 +310,18 @@ export function sortThread(
|
|||||||
// We want to give recent comments a real chance (and not bury them deep below the fold)
|
// We want to give recent comments a real chance (and not bury them deep below the fold)
|
||||||
// while also surfacing well-liked comments from the past. In the future, we can explore
|
// while also surfacing well-liked comments from the past. In the future, we can explore
|
||||||
// something more sophisticated, but we don't have much data on the client right now.
|
// something more sophisticated, but we don't have much data on the client right now.
|
||||||
function getHotness(post: AppBskyFeedDefs.PostView, fetchedAt: number) {
|
function getHotness(threadPost: ThreadPost, fetchedAt: number) {
|
||||||
|
const {post, hasOPLike} = threadPost
|
||||||
const hoursAgo = Math.max(
|
const hoursAgo = Math.max(
|
||||||
0,
|
0,
|
||||||
(new Date(fetchedAt).getTime() - new Date(post.indexedAt).getTime()) /
|
(new Date(fetchedAt).getTime() - new Date(post.indexedAt).getTime()) /
|
||||||
(1000 * 60 * 60),
|
(1000 * 60 * 60),
|
||||||
)
|
)
|
||||||
const likeCount = post.likeCount ?? 0
|
const likeCount = post.likeCount ?? 0
|
||||||
const likeOrder = Math.log(3 + likeCount)
|
const likeOrder = Math.log(3 + likeCount) * (hasOPLike ? 1.45 : 1.0)
|
||||||
const timePenaltyExponent = 1.5 + 1.5 / (1 + Math.log(1 + likeCount))
|
const timePenaltyExponent = 1.5 + 1.5 / (1 + Math.log(1 + likeCount))
|
||||||
const timePenalty = Math.pow(hoursAgo + 2, timePenaltyExponent)
|
const opLikeBoost = hasOPLike ? 0.8 : 1.0
|
||||||
|
const timePenalty = Math.pow(hoursAgo + 2, timePenaltyExponent * opLikeBoost)
|
||||||
return likeOrder / timePenalty
|
return likeOrder / timePenalty
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,6 +359,7 @@ function responseToThreadNodes(
|
|||||||
// do not show blocked posts in replies
|
// do not show blocked posts in replies
|
||||||
.filter(node => node.type !== 'blocked')
|
.filter(node => node.type !== 'blocked')
|
||||||
: undefined,
|
: undefined,
|
||||||
|
hasOPLike: Boolean(node?.threadContext?.rootAuthorLike),
|
||||||
ctx: {
|
ctx: {
|
||||||
depth,
|
depth,
|
||||||
isHighlightedPost: depth === 0,
|
isHighlightedPost: depth === 0,
|
||||||
@@ -552,6 +556,7 @@ function threadNodeToPlaceholderThread(
|
|||||||
record: node.record,
|
record: node.record,
|
||||||
parent: undefined,
|
parent: undefined,
|
||||||
replies: undefined,
|
replies: undefined,
|
||||||
|
hasOPLike: undefined,
|
||||||
ctx: {
|
ctx: {
|
||||||
depth: 0,
|
depth: 0,
|
||||||
isHighlightedPost: true,
|
isHighlightedPost: true,
|
||||||
@@ -573,6 +578,7 @@ function postViewToPlaceholderThread(
|
|||||||
record: post.record as AppBskyFeedPost.Record, // validated in notifs
|
record: post.record as AppBskyFeedPost.Record, // validated in notifs
|
||||||
parent: undefined,
|
parent: undefined,
|
||||||
replies: undefined,
|
replies: undefined,
|
||||||
|
hasOPLike: undefined,
|
||||||
ctx: {
|
ctx: {
|
||||||
depth: 0,
|
depth: 0,
|
||||||
isHighlightedPost: true,
|
isHighlightedPost: true,
|
||||||
@@ -594,6 +600,7 @@ function embedViewRecordToPlaceholderThread(
|
|||||||
record: record.value as AppBskyFeedPost.Record, // validated in getEmbeddedPost
|
record: record.value as AppBskyFeedPost.Record, // validated in getEmbeddedPost
|
||||||
parent: undefined,
|
parent: undefined,
|
||||||
replies: undefined,
|
replies: undefined,
|
||||||
|
hasOPLike: undefined,
|
||||||
ctx: {
|
ctx: {
|
||||||
depth: 0,
|
depth: 0,
|
||||||
isHighlightedPost: true,
|
isHighlightedPost: true,
|
||||||
|
|||||||
@@ -83,7 +83,12 @@ export async function createAgentAndLogin(
|
|||||||
) => void,
|
) => void,
|
||||||
) {
|
) {
|
||||||
const agent = new BskyAppAgent({service})
|
const agent = new BskyAppAgent({service})
|
||||||
await agent.login({identifier, password, authFactorToken})
|
await agent.login({
|
||||||
|
identifier,
|
||||||
|
password,
|
||||||
|
authFactorToken,
|
||||||
|
allowTakendown: true,
|
||||||
|
})
|
||||||
|
|
||||||
const account = agentToSessionAccountOrThrow(agent)
|
const account = agentToSessionAccountOrThrow(agent)
|
||||||
const gates = tryFetchGates(account.did, 'prefer-fresh-gates')
|
const gates = tryFetchGates(account.did, 'prefer-fresh-gates')
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-expect-error window type is not declared, debug only
|
||||||
if (__DEV__ && isWeb) window.agent = state.currentAgentState.agent
|
if (__DEV__ && isWeb) window.agent = state.currentAgentState.agent
|
||||||
|
|
||||||
const agent = state.currentAgentState.agent as BskyAppAgent
|
const agent = state.currentAgentState.agent as BskyAppAgent
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {AppBskyActorDefs} from '@atproto/api'
|
import {AppBskyActorDefs, AppBskyFeedDefs} from '@atproto/api'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {NavigationProp, useNavigation} from '@react-navigation/native'
|
import {NavigationProp, useNavigation} from '@react-navigation/native'
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {VIDEO_FEED_URIS} from '#/lib/constants'
|
||||||
import {ComposeIcon2} from '#/lib/icons'
|
import {ComposeIcon2} from '#/lib/icons'
|
||||||
import {getRootNavigation, getTabState, TabState} from '#/lib/routes/helpers'
|
import {getRootNavigation, getTabState, TabState} from '#/lib/routes/helpers'
|
||||||
import {AllNavigatorParams} from '#/lib/routes/types'
|
import {AllNavigatorParams} from '#/lib/routes/types'
|
||||||
@@ -15,6 +16,7 @@ import {isNative} from '#/platform/detection'
|
|||||||
import {listenSoftReset} from '#/state/events'
|
import {listenSoftReset} from '#/state/events'
|
||||||
import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
|
import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
|
||||||
import {useSetHomeBadge} from '#/state/home-badge'
|
import {useSetHomeBadge} from '#/state/home-badge'
|
||||||
|
import {SavedFeedSourceInfo} from '#/state/queries/feed'
|
||||||
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
|
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
|
||||||
import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed'
|
import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed'
|
||||||
import {truncateAndInvalidate} from '#/state/queries/util'
|
import {truncateAndInvalidate} from '#/state/queries/util'
|
||||||
@@ -39,6 +41,7 @@ export function FeedPage({
|
|||||||
renderEmptyState,
|
renderEmptyState,
|
||||||
renderEndOfFeed,
|
renderEndOfFeed,
|
||||||
savedFeedConfig,
|
savedFeedConfig,
|
||||||
|
feedInfo,
|
||||||
}: {
|
}: {
|
||||||
testID?: string
|
testID?: string
|
||||||
feed: FeedDescriptor
|
feed: FeedDescriptor
|
||||||
@@ -48,6 +51,7 @@ export function FeedPage({
|
|||||||
renderEmptyState: () => JSX.Element
|
renderEmptyState: () => JSX.Element
|
||||||
renderEndOfFeed?: () => JSX.Element
|
renderEndOfFeed?: () => JSX.Element
|
||||||
savedFeedConfig?: AppBskyActorDefs.SavedFeed
|
savedFeedConfig?: AppBskyActorDefs.SavedFeed
|
||||||
|
feedInfo: SavedFeedSourceInfo
|
||||||
}) {
|
}) {
|
||||||
const {hasSession} = useSession()
|
const {hasSession} = useSession()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
@@ -61,6 +65,13 @@ export function FeedPage({
|
|||||||
const scrollElRef = React.useRef<ListMethods>(null)
|
const scrollElRef = React.useRef<ListMethods>(null)
|
||||||
const [hasNew, setHasNew] = React.useState(false)
|
const [hasNew, setHasNew] = React.useState(false)
|
||||||
const setHomeBadge = useSetHomeBadge()
|
const setHomeBadge = useSetHomeBadge()
|
||||||
|
const isVideoFeed = React.useMemo(() => {
|
||||||
|
const isBskyVideoFeed = VIDEO_FEED_URIS.includes(feedInfo.uri)
|
||||||
|
const feedIsVideoMode =
|
||||||
|
feedInfo.contentMode === AppBskyFeedDefs.CONTENTMODEVIDEO
|
||||||
|
const _isVideoFeed = isBskyVideoFeed || feedIsVideoMode
|
||||||
|
return isNative && _isVideoFeed
|
||||||
|
}, [feedInfo])
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (isPageFocused) {
|
if (isPageFocused) {
|
||||||
@@ -134,6 +145,7 @@ export function FeedPage({
|
|||||||
renderEndOfFeed={renderEndOfFeed}
|
renderEndOfFeed={renderEndOfFeed}
|
||||||
headerOffset={headerOffset}
|
headerOffset={headerOffset}
|
||||||
savedFeedConfig={savedFeedConfig}
|
savedFeedConfig={savedFeedConfig}
|
||||||
|
isVideoFeed={isVideoFeed}
|
||||||
/>
|
/>
|
||||||
</FeedFeedbackProvider>
|
</FeedFeedbackProvider>
|
||||||
</MainScrollProvider>
|
</MainScrollProvider>
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import {usePalette} from '#/lib/hooks/usePalette'
|
|||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
import {s} from '#/lib/styles'
|
import {s} from '#/lib/styles'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {shouldClickOpenNewTab} from '#/platform/urls'
|
|
||||||
import {FeedSourceInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
|
import {FeedSourceInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
|
||||||
import {
|
import {
|
||||||
useAddSavedFeedsMutation,
|
useAddSavedFeedsMutation,
|
||||||
@@ -29,6 +28,7 @@ import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
|||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {useTheme} from '#/alf'
|
import {useTheme} from '#/alf'
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
|
import {shouldClickOpenNewTab} from '#/components/Link'
|
||||||
import * as Prompt from '#/components/Prompt'
|
import * as Prompt from '#/components/Prompt'
|
||||||
import {RichText} from '#/components/RichText'
|
import {RichText} from '#/components/RichText'
|
||||||
import {Text} from '../util/text/Text'
|
import {Text} from '../util/text/Text'
|
||||||
|
|||||||
@@ -205,9 +205,7 @@ export const ProfileFeedgens = React.forwardRef<
|
|||||||
headerOffset={headerOffset}
|
headerOffset={headerOffset}
|
||||||
progressViewOffset={ios(0)}
|
progressViewOffset={ios(0)}
|
||||||
contentContainerStyle={isMobile && {paddingBottom: headerOffset + 100}}
|
contentContainerStyle={isMobile && {paddingBottom: headerOffset + 100}}
|
||||||
indicatorStyle={t.name === 'light' ? 'black' : 'white'}
|
|
||||||
removeClippedSubviews={true}
|
removeClippedSubviews={true}
|
||||||
// @ts-ignore our .web version only -prf
|
|
||||||
desktopFixedHeight
|
desktopFixedHeight
|
||||||
onEndReached={onEndReached}
|
onEndReached={onEndReached}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -203,9 +203,7 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
|
|||||||
contentContainerStyle={
|
contentContainerStyle={
|
||||||
isMobile && {paddingBottom: headerOffset + 100}
|
isMobile && {paddingBottom: headerOffset + 100}
|
||||||
}
|
}
|
||||||
indicatorStyle={t.name === 'light' ? 'black' : 'white'}
|
|
||||||
removeClippedSubviews={true}
|
removeClippedSubviews={true}
|
||||||
// @ts-ignore our .web version only -prf
|
|
||||||
desktopFixedHeight
|
desktopFixedHeight
|
||||||
onEndReached={onEndReached}
|
onEndReached={onEndReached}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, {Children, forwardRef, useCallback, useContext} from 'react'
|
import React, {forwardRef, useCallback, useContext} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {DrawerGestureContext} from 'react-native-drawer-layout'
|
import {DrawerGestureContext} from 'react-native-drawer-layout'
|
||||||
import {Gesture, GestureDetector} from 'react-native-gesture-handler'
|
import {Gesture, GestureDetector} from 'react-native-gesture-handler'
|
||||||
@@ -17,7 +17,6 @@ import Animated, {
|
|||||||
} from 'react-native-reanimated'
|
} from 'react-native-reanimated'
|
||||||
import {useFocusEffect} from '@react-navigation/native'
|
import {useFocusEffect} from '@react-navigation/native'
|
||||||
|
|
||||||
import {isAndroid} from '#/platform/detection'
|
|
||||||
import {useSetDrawerSwipeDisabled} from '#/state/shell'
|
import {useSetDrawerSwipeDisabled} from '#/state/shell'
|
||||||
import {atoms as a, native} from '#/alf'
|
import {atoms as a, native} from '#/alf'
|
||||||
|
|
||||||
@@ -149,11 +148,7 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
|
|||||||
style={[a.flex_1]}
|
style={[a.flex_1]}
|
||||||
initialPage={initialPage}
|
initialPage={initialPage}
|
||||||
onPageScroll={handlePageScroll}>
|
onPageScroll={handlePageScroll}>
|
||||||
{isAndroid
|
{children}
|
||||||
? Children.map(children, child => (
|
|
||||||
<CaptureSwipesAndroid>{child}</CaptureSwipesAndroid>
|
|
||||||
))
|
|
||||||
: children}
|
|
||||||
</AnimatedPagerView>
|
</AnimatedPagerView>
|
||||||
</GestureDetector>
|
</GestureDetector>
|
||||||
</View>
|
</View>
|
||||||
@@ -161,39 +156,6 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// HACK.
|
|
||||||
// This works around https://github.com/callstack/react-native-pager-view/issues/960.
|
|
||||||
// It appears that the Pressables inside the pager get confused if there's enough work
|
|
||||||
// happening on the JS thread, and mistakingly interpret a pager swipe as a tap.
|
|
||||||
// We can prevent this by stealing all horizontal movements from the tree inside.
|
|
||||||
function CaptureSwipesAndroid({children}: {children: React.ReactNode}) {
|
|
||||||
const lastTouchStart = React.useRef<{x: number; y: number} | null>(null)
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
onTouchStart={e => {
|
|
||||||
lastTouchStart.current = {
|
|
||||||
x: e.nativeEvent.pageX,
|
|
||||||
y: e.nativeEvent.pageY,
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onMoveShouldSetResponderCapture={e => {
|
|
||||||
const coords = lastTouchStart.current
|
|
||||||
if (!coords) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const dx = Math.abs(e.nativeEvent.pageX - coords.x)
|
|
||||||
if (dx > 0) {
|
|
||||||
// This is a horizontal movement and will result in a swipe.
|
|
||||||
// Prevent pager children from receiving this touch.
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}}>
|
|
||||||
{children}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function usePagerHandlers(
|
function usePagerHandlers(
|
||||||
handlers: {
|
handlers: {
|
||||||
onPageScroll: (e: PagerViewOnPageScrollEventData) => void
|
onPageScroll: (e: PagerViewOnPageScrollEventData) => void
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user