Merge remote-tracking branch 'origin/main' into next/base
* origin/main: (100 commits) Adding webp to regex list. Added function return type for consistency with the rest of the functions (#7649) bump version (#7650) use did for userID (#7647) Upgrade RN to 0.76.6 (#7557) Update expo-blur (#7596) Fix slow Hosting Provider dialog (#7594) make convo menu lazy (#7604) Fix convo header loading state (#7603) Spring cleaning (#7640) Date input improvements (#7639) Add example account store (#7641) rm refs in render (#7589) change search cancel behaviour (#7624) reduce padding on video overlay (#7617) Typo fix (#7637) no follow self (#7606) env var tweaks (#7457) Test fix Add Danish localization (#7220) Add Swedish localization (#7232) ...
This commit is contained in:
@@ -93,6 +93,7 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/ban-ts-comment': 'warn',
|
||||
},
|
||||
ignorePatterns: [
|
||||
'**/__mocks__/*.ts',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright 2023–2024 Bluesky PBC
|
||||
Copyright 2023–2025 Bluesky PBC
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
||||
@@ -10,8 +10,7 @@ appId: xyz.blueskyweb.app
|
||||
id: "e2eOpenLoggedOutView"
|
||||
- tapOn:
|
||||
id: "createAccountButton"
|
||||
- tapOn:
|
||||
id: "selectServiceButton"
|
||||
- tapOn: "Bluesky Social"
|
||||
- tapOn:
|
||||
id: "customSelectBtn"
|
||||
- tapOn:
|
||||
|
||||
@@ -21,7 +21,7 @@ appId: xyz.blueskyweb.app
|
||||
label: "Feeds button disappears after pinning a feed"
|
||||
id: "bottomBarProfileBtn"
|
||||
- tapOn:
|
||||
id: "profilePager-selector-4"
|
||||
id: "profilePager-selector-5"
|
||||
- tapOn: "alice-favs"
|
||||
- tapOn: "Pin to Home"
|
||||
- tapOn:
|
||||
|
||||
+21
-3
@@ -75,9 +75,11 @@ module.exports = function (config) {
|
||||
'an',
|
||||
'ast',
|
||||
'ca',
|
||||
'da',
|
||||
'de',
|
||||
'el',
|
||||
'es',
|
||||
'eu',
|
||||
'fi',
|
||||
'fr',
|
||||
'ga',
|
||||
@@ -95,11 +97,12 @@ module.exports = function (config) {
|
||||
'pt-BR',
|
||||
'ro',
|
||||
'ru',
|
||||
'sv',
|
||||
'th',
|
||||
'tr',
|
||||
'uk',
|
||||
'vi',
|
||||
'yue-Hant',
|
||||
'yue',
|
||||
'zh-Hans',
|
||||
'zh-Hant',
|
||||
],
|
||||
@@ -191,6 +194,7 @@ module.exports = function (config) {
|
||||
channel: UPDATES_CHANNEL,
|
||||
},
|
||||
plugins: [
|
||||
'expo-video',
|
||||
'expo-localization',
|
||||
USE_SENTRY && [
|
||||
'@sentry/react-native/expo',
|
||||
@@ -225,8 +229,12 @@ module.exports = function (config) {
|
||||
},
|
||||
],
|
||||
'react-native-compressor',
|
||||
// TODO: Reenable when the build issue is fixed.
|
||||
// '@bitdrift/react-native',
|
||||
[
|
||||
'@bitdrift/react-native',
|
||||
{
|
||||
networkInstrumentation: true,
|
||||
},
|
||||
],
|
||||
'./plugins/starterPackAppClipExtension/withStarterPackAppClip.js',
|
||||
'./plugins/withAndroidManifestPlugin.js',
|
||||
'./plugins/withAndroidManifestFCMIconPlugin.js',
|
||||
@@ -347,6 +355,16 @@ module.exports = function (config) {
|
||||
},
|
||||
],
|
||||
['expo-screen-orientation', {initialOrientation: 'PORTRAIT_UP'}],
|
||||
[
|
||||
'react-native-vision-camera',
|
||||
{
|
||||
enableLocation: false,
|
||||
cameraPermissionText: 'Bluesky needs access to your camera.',
|
||||
enableMicrophonePermission: true,
|
||||
microphonePermissionText:
|
||||
'Bluesky needs access to your microphone.',
|
||||
},
|
||||
],
|
||||
].filter(Boolean),
|
||||
extra: {
|
||||
eas: {
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" stroke="#000" stroke-linejoin="round" d="M4 5.5a.5.5 0 0 0-.5.5v2.535a.5.5 0 0 0 .25.433A3.5 3.5 0 0 1 5.5 12a3.5 3.5 0 0 1-1.75 3.032.5.5 0 0 0-.25.433V18a.5.5 0 0 0 .5.5h16a.5.5 0 0 0 .5-.5v-2.535a.5.5 0 0 0-.25-.433A3.5 3.5 0 0 1 18.5 12a3.5 3.5 0 0 1 1.75-3.032.5.5 0 0 0 .25-.433V6a.5.5 0 0 0-.5-.5H4ZM2.5 6A1.5 1.5 0 0 1 4 4.5h16A1.5 1.5 0 0 1 21.5 6v3.17a.5.5 0 0 1-.333.472 2.501 2.501 0 0 0 0 4.716.5.5 0 0 1 .333.471V18a1.5 1.5 0 0 1-1.5 1.5H4A1.5 1.5 0 0 1 2.5 18v-3.17a.5.5 0 0 1 .333-.472 2.501 2.501 0 0 0 0-4.716.5.5 0 0 1-.333-.471V6Zm12 2a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Zm0 4a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Zm0 4a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M3 4a1 1 0 0 0-1 1v4.17a1 1 0 0 0 .667.944 2.001 2.001 0 0 1 0 3.772A1 1 0 0 0 2 14.83V19a1 1 0 0 0 1 1h18a1 1 0 0 0 1-1v-4.17a1 1 0 0 0-.667-.944 2.001 2.001 0 0 1 0-3.772A1 1 0 0 0 22 9.17V5a1 1 0 0 0-1-1H3Zm1 4.535V6h16v2.535A4 4 0 0 0 18 12c0 1.482.805 2.773 2 3.465V18H4v-2.535A4 4 0 0 0 6 12a4 4 0 0 0-2-3.465ZM15 15a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm-1-3a1 1 0 1 1 2 0 1 1 0 0 1-2 0Zm1-5a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z" clip-rule="evenodd"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 756 B After Width: | Height: | Size: 561 B |
@@ -214,15 +214,21 @@ func serve(cctx *cli.Context) error {
|
||||
e.GET("/iframe/youtube.html", echo.WrapHandler(staticHandler))
|
||||
e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler)), func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
path := c.Request().URL.Path
|
||||
maxAge := 1 * (60 * 60) // default is 1 hour
|
||||
c.Response().Before(func() {
|
||||
if c.Response().Status >= 300 {
|
||||
return
|
||||
}
|
||||
|
||||
// all assets in /static/js, /static/css, /static/media are content-hashed and can be cached for a long time
|
||||
if strings.HasPrefix(path, "/static/js/") || strings.HasPrefix(path, "/static/css/") || strings.HasPrefix(path, "/static/media/") {
|
||||
maxAge = 365 * (60 * 60 * 24) // 1 year
|
||||
}
|
||||
path := c.Request().URL.Path
|
||||
maxAge := 1 * (60 * 60) // default is 1 hour
|
||||
|
||||
c.Response().Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", maxAge))
|
||||
// all assets in /static/js, /static/css, /static/media are content-hashed and can be cached for a long time
|
||||
if strings.HasPrefix(path, "/static/js/") || strings.HasPrefix(path, "/static/css/") || strings.HasPrefix(path, "/static/media/") {
|
||||
maxAge = 365 * (60 * 60 * 24) // 1 year
|
||||
}
|
||||
|
||||
c.Response().Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", maxAge))
|
||||
})
|
||||
return next(c)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
Preconnect to essential domains
|
||||
-->
|
||||
<link rel="preconnect" href="https://bsky.social">
|
||||
<link rel="preconnect" href="https://bsky.network">
|
||||
<title>{%- block head_title -%}Bluesky{%- endblock -%}</title>
|
||||
|
||||
<!-- Hello Humans! API docs at https://atproto.com -->
|
||||
|
||||
@@ -5,10 +5,12 @@ module.exports = {
|
||||
'an',
|
||||
'ast',
|
||||
'ca',
|
||||
'da',
|
||||
'de',
|
||||
'el',
|
||||
'en-GB',
|
||||
'es',
|
||||
'eu',
|
||||
'fi',
|
||||
'fr',
|
||||
'ga',
|
||||
@@ -26,6 +28,7 @@ module.exports = {
|
||||
'pt-BR',
|
||||
'ro',
|
||||
'ru',
|
||||
'sv',
|
||||
'th',
|
||||
'tr',
|
||||
'uk',
|
||||
|
||||
+9
-6
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "bsky.app",
|
||||
"version": "1.97.0",
|
||||
"version": "1.98.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"packageManager": "yarn@1.22.19",
|
||||
"packageManager": "yarn@1.22.22",
|
||||
"scripts": {
|
||||
"prepare": "is-ci || husky install",
|
||||
"postinstall": "patch-package && yarn intl:compile",
|
||||
@@ -55,6 +55,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "0.14.0-next.2",
|
||||
"@bitdrift/react-native": "^0.6.2",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
@@ -72,7 +73,7 @@
|
||||
"@haileyok/bluesky-video": "0.2.6",
|
||||
"@ipld/dag-cbor": "^9.2.0",
|
||||
"@lingui/react": "^4.14.1",
|
||||
"@mattermost/react-native-paste-input": "^0.8.1",
|
||||
"@mattermost/react-native-paste-input": "^0.7.1",
|
||||
"@miblanchard/react-native-slider": "^2.3.1",
|
||||
"@mozzius/expo-dynamic-app-icon": "^1.5.0",
|
||||
"@radix-ui/react-dismissable-layer": "^1.1.1",
|
||||
@@ -120,7 +121,7 @@
|
||||
"eventemitter3": "^5.0.1",
|
||||
"expo": "~52.0.19",
|
||||
"expo-application": "^6.0.1",
|
||||
"expo-blur": "^14.0.1",
|
||||
"expo-blur": "^14.0.3",
|
||||
"expo-build-properties": "^0.13.1",
|
||||
"expo-camera": "~16.0.7",
|
||||
"expo-clipboard": "^7.0.0",
|
||||
@@ -145,6 +146,7 @@
|
||||
"expo-system-ui": "^4.0.4",
|
||||
"expo-task-manager": "~12.0.3",
|
||||
"expo-updates": "~0.26.10",
|
||||
"expo-video": "^2.0.5",
|
||||
"expo-web-browser": "~14.0.1",
|
||||
"fast-text-encoding": "^1.0.6",
|
||||
"history": "^5.3.0",
|
||||
@@ -168,7 +170,7 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"react-image-crop": "^11.0.7",
|
||||
"react-keyed-flatten-children": "^3.0.0",
|
||||
"react-native": "0.76.3",
|
||||
"react-native": "0.76.6",
|
||||
"react-native-compressor": "1.10.3",
|
||||
"react-native-date-picker": "^5.0.7",
|
||||
"react-native-drawer-layout": "^4.1.1",
|
||||
@@ -178,7 +180,7 @@
|
||||
"react-native-ios-context-menu": "^1.15.3",
|
||||
"react-native-keyboard-controller": "^1.14.5",
|
||||
"react-native-mmkv": "^2.12.2",
|
||||
"react-native-pager-view": "6.6.1",
|
||||
"react-native-pager-view": "6.7.0",
|
||||
"react-native-picker-select": "^9.3.1",
|
||||
"react-native-progress": "bluesky-social/react-native-progress",
|
||||
"react-native-qrcode-styled": "^0.3.3",
|
||||
@@ -191,6 +193,7 @@
|
||||
"react-native-url-polyfill": "^1.3.0",
|
||||
"react-native-uuid": "^2.0.2",
|
||||
"react-native-view-shot": "^4.0.3",
|
||||
"react-native-vision-camera": "^4.6.3",
|
||||
"react-native-web": "~0.19.13",
|
||||
"react-native-web-webview": "^1.0.2",
|
||||
"react-native-webview": "13.12.3",
|
||||
|
||||
+6
-9
@@ -1,19 +1,16 @@
|
||||
diff --git a/node_modules/@mattermost/react-native-paste-input/ios/PasteInputView.m b/node_modules/@mattermost/react-native-paste-input/ios/PasteInputView.m
|
||||
index e916023..9968f3c 100644
|
||||
index e916023..5049c33 100644
|
||||
--- a/node_modules/@mattermost/react-native-paste-input/ios/PasteInputView.m
|
||||
+++ b/node_modules/@mattermost/react-native-paste-input/ios/PasteInputView.m
|
||||
@@ -3,58 +3,87 @@
|
||||
// PasteInput
|
||||
@@ -4,6 +4,7 @@
|
||||
//
|
||||
// Created by Elias Nahum on 04-11-20.
|
||||
-// Copyright © 2020 Facebook. All rights reserved.
|
||||
// Copyright © 2020 Facebook. All rights reserved.
|
||||
+// Updated to remove parent’s default text view
|
||||
//
|
||||
|
||||
#import "PasteInputView.h"
|
||||
#import "PasteInputTextView.h"
|
||||
-#import <React/RCTUtils.h>
|
||||
+#import <React/RCTUtils.h> // for RCTDirectEventBlock, etc.
|
||||
@@ -12,49 +13,78 @@
|
||||
|
||||
@implementation PasteInputView
|
||||
{
|
||||
@@ -117,7 +114,7 @@ index e916023..9968f3c 100644
|
||||
}
|
||||
|
||||
#pragma mark - UIScrollViewDelegate
|
||||
@@ -62,7 +91,6 @@
|
||||
@@ -62,7 +92,6 @@
|
||||
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
|
||||
{
|
||||
RCTDirectEventBlock onScroll = self.onScroll;
|
||||
@@ -125,7 +122,7 @@ index e916023..9968f3c 100644
|
||||
if (onScroll) {
|
||||
CGPoint contentOffset = scrollView.contentOffset;
|
||||
CGSize contentSize = scrollView.contentSize;
|
||||
@@ -71,22 +99,22 @@
|
||||
@@ -71,22 +100,22 @@
|
||||
|
||||
onScroll(@{
|
||||
@"contentOffset": @{
|
||||
@@ -0,0 +1,36 @@
|
||||
diff --git a/node_modules/react-native-gesture-handler/apple/RNGestureHandler.mm b/node_modules/react-native-gesture-handler/apple/RNGestureHandler.mm
|
||||
index 43d11b4..5bf0d32 100644
|
||||
--- a/node_modules/react-native-gesture-handler/apple/RNGestureHandler.mm
|
||||
+++ b/node_modules/react-native-gesture-handler/apple/RNGestureHandler.mm
|
||||
@@ -461,16 +461,23 @@ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
|
||||
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
|
||||
shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
|
||||
{
|
||||
- if ([_handlersToWaitFor count]) {
|
||||
- RNGestureHandler *handler = [RNGestureHandler findGestureHandlerByRecognizer:otherGestureRecognizer];
|
||||
- if (handler != nil) {
|
||||
- for (NSNumber *handlerTag in _handlersToWaitFor) {
|
||||
- if ([handler.tag isEqual:handlerTag]) {
|
||||
- return YES;
|
||||
- }
|
||||
- }
|
||||
+ RNGestureHandler *handler = [RNGestureHandler findGestureHandlerByRecognizer:otherGestureRecognizer];
|
||||
+ if (handler == nil) {
|
||||
+ return NO;
|
||||
+ }
|
||||
+
|
||||
+ for (NSNumber *handlerTag in _handlersToWaitFor) {
|
||||
+ if ([handler.tag isEqual:handlerTag]) {
|
||||
+ return YES;
|
||||
}
|
||||
}
|
||||
+
|
||||
+ for (NSNumber *handlerTag in handler->_handlersThatShouldWait) {
|
||||
+ if ([_tag isEqual:handlerTag]) {
|
||||
+ return YES;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
return NO;
|
||||
}
|
||||
|
||||
+39
-42
@@ -69,7 +69,6 @@ import {NuxDialogs} from '#/components/dialogs/nuxs'
|
||||
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
||||
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
|
||||
import {Provider as PortalProvider} from '#/components/Portal'
|
||||
import {AppProfiler} from '#/AppProfiler'
|
||||
import {Splash} from '#/Splash'
|
||||
import {BottomSheetProvider} from '../modules/bottom-sheet'
|
||||
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||
@@ -152,9 +151,11 @@ function InnerApp() {
|
||||
<TrendingConfigProvider>
|
||||
<GestureHandlerRootView
|
||||
style={s.h100pct}>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
<IntentDialogProvider>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
</IntentDialogProvider>
|
||||
</GestureHandlerRootView>
|
||||
</TrendingConfigProvider>
|
||||
</ProgressGuideProvider>
|
||||
@@ -198,44 +199,40 @@ function App() {
|
||||
* that is set up in the InnerApp component above.
|
||||
*/
|
||||
return (
|
||||
<AppProfiler>
|
||||
<GeolocationProvider>
|
||||
<A11yProvider>
|
||||
<KeyboardControllerProvider>
|
||||
<SessionProvider>
|
||||
<PrefsStateProvider>
|
||||
<I18nProvider>
|
||||
<ShellStateProvider>
|
||||
<InvitesStateProvider>
|
||||
<ModalStateProvider>
|
||||
<DialogStateProvider>
|
||||
<LightboxStateProvider>
|
||||
<PortalProvider>
|
||||
<BottomSheetProvider>
|
||||
<StarterPackProvider>
|
||||
<SafeAreaProvider
|
||||
initialMetrics={initialWindowMetrics}>
|
||||
<IntentDialogProvider>
|
||||
<LightStatusBarProvider>
|
||||
<InnerApp />
|
||||
</LightStatusBarProvider>
|
||||
</IntentDialogProvider>
|
||||
</SafeAreaProvider>
|
||||
</StarterPackProvider>
|
||||
</BottomSheetProvider>
|
||||
</PortalProvider>
|
||||
</LightboxStateProvider>
|
||||
</DialogStateProvider>
|
||||
</ModalStateProvider>
|
||||
</InvitesStateProvider>
|
||||
</ShellStateProvider>
|
||||
</I18nProvider>
|
||||
</PrefsStateProvider>
|
||||
</SessionProvider>
|
||||
</KeyboardControllerProvider>
|
||||
</A11yProvider>
|
||||
</GeolocationProvider>
|
||||
</AppProfiler>
|
||||
<GeolocationProvider>
|
||||
<A11yProvider>
|
||||
<KeyboardControllerProvider>
|
||||
<SessionProvider>
|
||||
<PrefsStateProvider>
|
||||
<I18nProvider>
|
||||
<ShellStateProvider>
|
||||
<InvitesStateProvider>
|
||||
<ModalStateProvider>
|
||||
<DialogStateProvider>
|
||||
<LightboxStateProvider>
|
||||
<PortalProvider>
|
||||
<BottomSheetProvider>
|
||||
<StarterPackProvider>
|
||||
<SafeAreaProvider
|
||||
initialMetrics={initialWindowMetrics}>
|
||||
<LightStatusBarProvider>
|
||||
<InnerApp />
|
||||
</LightStatusBarProvider>
|
||||
</SafeAreaProvider>
|
||||
</StarterPackProvider>
|
||||
</BottomSheetProvider>
|
||||
</PortalProvider>
|
||||
</LightboxStateProvider>
|
||||
</DialogStateProvider>
|
||||
</ModalStateProvider>
|
||||
</InvitesStateProvider>
|
||||
</ShellStateProvider>
|
||||
</I18nProvider>
|
||||
</PrefsStateProvider>
|
||||
</SessionProvider>
|
||||
</KeyboardControllerProvider>
|
||||
</A11yProvider>
|
||||
</GeolocationProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+7
-7
@@ -129,8 +129,10 @@ function InnerApp() {
|
||||
<SafeAreaProvider>
|
||||
<ProgressGuideProvider>
|
||||
<TrendingConfigProvider>
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
<IntentDialogProvider>
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
</IntentDialogProvider>
|
||||
</TrendingConfigProvider>
|
||||
</ProgressGuideProvider>
|
||||
</SafeAreaProvider>
|
||||
@@ -187,11 +189,9 @@ function App() {
|
||||
<LightboxStateProvider>
|
||||
<PortalProvider>
|
||||
<StarterPackProvider>
|
||||
<IntentDialogProvider>
|
||||
<LightStatusBarProvider>
|
||||
<InnerApp />
|
||||
</LightStatusBarProvider>
|
||||
</IntentDialogProvider>
|
||||
<LightStatusBarProvider>
|
||||
<InnerApp />
|
||||
</LightStatusBarProvider>
|
||||
</StarterPackProvider>
|
||||
</PortalProvider>
|
||||
</LightboxStateProvider>
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import React, {Profiler} from 'react'
|
||||
|
||||
// Don't let it get stripped out in profiling builds (which apply production Babel preset).
|
||||
const log = (global as any)['con' + 'sole'].log
|
||||
|
||||
function onRender(id: string, phase: string, actualDuration: number) {
|
||||
if (!__DEV__) {
|
||||
// This block of code will exist in the production build of the app.
|
||||
// However, only profiling builds of React call `onRender` so it's dead code in actual production.
|
||||
const message = `<Profiler> ${id}:${phase} ${
|
||||
actualDuration > 500
|
||||
? '(╯°□°)╯ '
|
||||
: actualDuration > 100
|
||||
? '[!!] '
|
||||
: actualDuration > 16
|
||||
? '[!] '
|
||||
: ''
|
||||
}${Math.round(actualDuration)}ms`
|
||||
log(message)
|
||||
}
|
||||
}
|
||||
|
||||
export function AppProfiler({children}: {children: React.ReactNode}) {
|
||||
return (
|
||||
<Profiler id="app" onRender={onRender}>
|
||||
{children}
|
||||
</Profiler>
|
||||
)
|
||||
}
|
||||
@@ -86,6 +86,7 @@ import {
|
||||
StarterPackScreenShort,
|
||||
} from '#/screens/StarterPack/StarterPackScreen'
|
||||
import {Wizard} from '#/screens/StarterPack/Wizard'
|
||||
import {VideoFeed} from '#/screens/VideoFeed'
|
||||
import {useTheme} from '#/alf'
|
||||
import {router} from '#/routes'
|
||||
import {Referrer} from '../modules/expo-bluesky-swiss-army'
|
||||
@@ -422,6 +423,14 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
||||
getComponent={() => Wizard}
|
||||
options={{title: title(msg`Edit your starter pack`), requireAuth: true}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="VideoFeed"
|
||||
getComponent={() => VideoFeed}
|
||||
options={{
|
||||
title: title(msg`Video Feed`),
|
||||
requireAuth: true,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -961,6 +961,9 @@ export const atoms = {
|
||||
transitionTimingFunction: 'cubic-bezier(0.17, 0.73, 0.14, 1)',
|
||||
transitionDuration: '100ms',
|
||||
}),
|
||||
transition_delay_50ms: web({
|
||||
transitionDelay: '50ms',
|
||||
}),
|
||||
|
||||
/**
|
||||
* {@link Layout.SCROLLBAR_OFFSET}
|
||||
|
||||
+1
-1
@@ -497,7 +497,7 @@ export function createThemes({
|
||||
color: dimPalette.contrast_400,
|
||||
},
|
||||
text_contrast_medium: {
|
||||
color: dimPalette.contrast_700,
|
||||
color: dimPalette.contrast_600,
|
||||
},
|
||||
text_contrast_high: {
|
||||
color: dimPalette.contrast_900,
|
||||
|
||||
@@ -131,7 +131,11 @@ function AccountItem({
|
||||
(hovered || pressed || isPendingAccount) && t.atoms.bg_contrast_25,
|
||||
]}>
|
||||
<View style={a.p_md}>
|
||||
<UserAvatar avatar={profile?.avatar} size={24} />
|
||||
<UserAvatar
|
||||
avatar={profile?.avatar}
|
||||
size={24}
|
||||
type={profile?.associated?.labeler ? 'labeler' : 'user'}
|
||||
/>
|
||||
</View>
|
||||
<Text style={[a.align_baseline, a.flex_1, a.flex_row, a.py_sm]}>
|
||||
<Text emoji style={[a.font_bold]}>
|
||||
|
||||
@@ -65,6 +65,7 @@ export function AvatarStack({
|
||||
<UserAvatar
|
||||
size={size - 2}
|
||||
avatar={item.profile.avatar}
|
||||
type={item.profile.associated?.labeler ? 'labeler' : 'user'}
|
||||
moderation={item.moderation.ui('avatar')}
|
||||
/>
|
||||
)}
|
||||
|
||||
+30
-16
@@ -3,10 +3,12 @@ import {
|
||||
AccessibilityProps,
|
||||
GestureResponderEvent,
|
||||
MouseEvent,
|
||||
NativeSyntheticEvent,
|
||||
Pressable,
|
||||
PressableProps,
|
||||
StyleProp,
|
||||
StyleSheet,
|
||||
TargetedEvent,
|
||||
TextProps,
|
||||
TextStyle,
|
||||
View,
|
||||
@@ -76,6 +78,8 @@ export type ButtonProps = Pick<
|
||||
| 'onHoverOut'
|
||||
| 'onPressIn'
|
||||
| 'onPressOut'
|
||||
| 'onFocus'
|
||||
| 'onBlur'
|
||||
> &
|
||||
AccessibilityProps &
|
||||
VariantProps & {
|
||||
@@ -116,6 +120,12 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
style,
|
||||
hoverStyle: hoverStyleProp,
|
||||
PressableComponent = Pressable,
|
||||
onPressIn: onPressInOuter,
|
||||
onPressOut: onPressOutOuter,
|
||||
onHoverIn: onHoverInOuter,
|
||||
onHoverOut: onHoverOutOuter,
|
||||
onFocus: onFocusOuter,
|
||||
onBlur: onBlurOuter,
|
||||
...rest
|
||||
},
|
||||
ref,
|
||||
@@ -127,7 +137,6 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
focused: false,
|
||||
})
|
||||
|
||||
const onPressInOuter = rest.onPressIn
|
||||
const onPressIn = React.useCallback(
|
||||
(e: GestureResponderEvent) => {
|
||||
setState(s => ({
|
||||
@@ -138,7 +147,6 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
},
|
||||
[setState, onPressInOuter],
|
||||
)
|
||||
const onPressOutOuter = rest.onPressOut
|
||||
const onPressOut = React.useCallback(
|
||||
(e: GestureResponderEvent) => {
|
||||
setState(s => ({
|
||||
@@ -149,7 +157,6 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
},
|
||||
[setState, onPressOutOuter],
|
||||
)
|
||||
const onHoverInOuter = rest.onHoverIn
|
||||
const onHoverIn = React.useCallback(
|
||||
(e: MouseEvent) => {
|
||||
setState(s => ({
|
||||
@@ -160,7 +167,6 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
},
|
||||
[setState, onHoverInOuter],
|
||||
)
|
||||
const onHoverOutOuter = rest.onHoverOut
|
||||
const onHoverOut = React.useCallback(
|
||||
(e: MouseEvent) => {
|
||||
setState(s => ({
|
||||
@@ -171,18 +177,26 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
},
|
||||
[setState, onHoverOutOuter],
|
||||
)
|
||||
const onFocus = React.useCallback(() => {
|
||||
setState(s => ({
|
||||
...s,
|
||||
focused: true,
|
||||
}))
|
||||
}, [setState])
|
||||
const onBlur = React.useCallback(() => {
|
||||
setState(s => ({
|
||||
...s,
|
||||
focused: false,
|
||||
}))
|
||||
}, [setState])
|
||||
const onFocus = React.useCallback(
|
||||
(e: NativeSyntheticEvent<TargetedEvent>) => {
|
||||
setState(s => ({
|
||||
...s,
|
||||
focused: true,
|
||||
}))
|
||||
onFocusOuter?.(e)
|
||||
},
|
||||
[setState, onFocusOuter],
|
||||
)
|
||||
const onBlur = React.useCallback(
|
||||
(e: NativeSyntheticEvent<TargetedEvent>) => {
|
||||
setState(s => ({
|
||||
...s,
|
||||
focused: false,
|
||||
}))
|
||||
onBlurOuter?.(e)
|
||||
},
|
||||
[setState, onBlurOuter],
|
||||
)
|
||||
|
||||
const {baseStyles, hoverStyles} = React.useMemo(() => {
|
||||
const baseStyles: ViewStyle[] = []
|
||||
|
||||
@@ -27,6 +27,7 @@ import {useA11y} from '#/state/a11y'
|
||||
import {useDialogStateControlContext} from '#/state/dialogs'
|
||||
import {List, ListMethods, ListProps} from '#/view/com/util/List'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {useThemeName} from '#/alf/util/useColorModeTheme'
|
||||
import {Context, useDialogContext} from '#/components/Dialog/context'
|
||||
import {
|
||||
DialogControlProps,
|
||||
@@ -55,7 +56,8 @@ export function Outer({
|
||||
nativeOptions,
|
||||
testID,
|
||||
}: React.PropsWithChildren<DialogOuterProps>) {
|
||||
const t = useTheme()
|
||||
const themeName = useThemeName()
|
||||
const t = useTheme(themeName)
|
||||
const ref = React.useRef<BottomSheetNativeComponent>(null)
|
||||
const closeCallbacks = React.useRef<(() => void)[]>([])
|
||||
const {setDialogIsOpen, setFullyExpandedCount} =
|
||||
|
||||
@@ -17,6 +17,7 @@ import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows
|
||||
import {useSession} from '#/state/session'
|
||||
import * as userActionHistory from '#/state/userActionHistory'
|
||||
import {SeenPost} from '#/state/userActionHistory'
|
||||
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
|
||||
import {atoms as a, useBreakpoints, useTheme, ViewStyleProp, web} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import * as FeedCard from '#/components/FeedCard'
|
||||
@@ -197,6 +198,7 @@ export function SuggestedFollowsProfile({did}: {did: string}) {
|
||||
<ProfileGrid
|
||||
isSuggestionsLoading={isSuggestionsLoading}
|
||||
profiles={data?.suggestions ?? []}
|
||||
recId={data?.recId}
|
||||
error={error}
|
||||
viewContext="profile"
|
||||
/>
|
||||
@@ -223,10 +225,12 @@ export function ProfileGrid({
|
||||
isSuggestionsLoading,
|
||||
error,
|
||||
profiles,
|
||||
recId,
|
||||
viewContext = 'feed',
|
||||
}: {
|
||||
isSuggestionsLoading: boolean
|
||||
profiles: atp.profile.AnyProfileView[]
|
||||
recId?: number
|
||||
error: Error | null
|
||||
viewContext: 'profile' | 'feed'
|
||||
}) {
|
||||
@@ -250,12 +254,19 @@ export function ProfileGrid({
|
||||
))
|
||||
) : error || !profiles.length ? null : (
|
||||
<>
|
||||
{profiles.slice(0, maxLength).map(profile => (
|
||||
{profiles.slice(0, maxLength).map((profile, index) => (
|
||||
<ProfileCard.Link
|
||||
key={profile.did}
|
||||
profile={profile}
|
||||
onPress={() => {
|
||||
logEvent('feed:interstitial:profileCard:press', {})
|
||||
logEvent('suggestedUser:press', {
|
||||
logContext:
|
||||
viewContext === 'feed'
|
||||
? 'InterstitialDiscover'
|
||||
: 'InterstitialProfile',
|
||||
recId,
|
||||
position: index,
|
||||
})
|
||||
}}
|
||||
style={[
|
||||
a.flex_1,
|
||||
@@ -283,6 +294,17 @@ export function ProfileGrid({
|
||||
logContext="FeedInterstitial"
|
||||
shape="round"
|
||||
colorInverted
|
||||
onFollow={() => {
|
||||
logEvent('suggestedUser:follow', {
|
||||
logContext:
|
||||
viewContext === 'feed'
|
||||
? 'InterstitialDiscover'
|
||||
: 'InterstitialProfile',
|
||||
location: 'Card',
|
||||
recId,
|
||||
position: index,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</ProfileCard.Header>
|
||||
<ProfileCard.Description profile={profile} numberOfLines={2} />
|
||||
@@ -337,33 +359,37 @@ export function ProfileGrid({
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap}
|
||||
decelerationRate="fast">
|
||||
<View style={[a.px_lg, a.pt_sm, a.pb_lg, a.flex_row, a.gap_md]}>
|
||||
{content}
|
||||
<BlockDrawerGesture>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap}
|
||||
decelerationRate="fast">
|
||||
<View style={[a.px_lg, a.pt_sm, a.pb_lg, a.flex_row, a.gap_md]}>
|
||||
{content}
|
||||
|
||||
<Button
|
||||
label={_(msg`Browse more accounts on the Explore page`)}
|
||||
onPress={() => {
|
||||
navigation.navigate('SearchTab')
|
||||
}}>
|
||||
<CardOuter style={[a.flex_1, {borderWidth: 0}]}>
|
||||
<View style={[a.flex_1, a.justify_center]}>
|
||||
<View style={[a.flex_row, a.px_lg]}>
|
||||
<Text style={[a.pr_xl, a.flex_1, a.leading_snug]}>
|
||||
<Trans>Browse more suggestions on the Explore page</Trans>
|
||||
</Text>
|
||||
<Button
|
||||
label={_(msg`Browse more accounts on the Explore page`)}
|
||||
onPress={() => {
|
||||
navigation.navigate('SearchTab')
|
||||
}}>
|
||||
<CardOuter style={[a.flex_1, {borderWidth: 0}]}>
|
||||
<View style={[a.flex_1, a.justify_center]}>
|
||||
<View style={[a.flex_row, a.px_lg]}>
|
||||
<Text style={[a.pr_xl, a.flex_1, a.leading_snug]}>
|
||||
<Trans>
|
||||
Browse more suggestions on the Explore page
|
||||
</Trans>
|
||||
</Text>
|
||||
|
||||
<Arrow size="xl" />
|
||||
<Arrow size="xl" />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</CardOuter>
|
||||
</Button>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</CardOuter>
|
||||
</Button>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</BlockDrawerGesture>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
@@ -470,34 +496,38 @@ export function SuggestedFeeds() {
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap}
|
||||
decelerationRate="fast">
|
||||
<View style={[a.px_lg, a.pt_md, a.pb_xl, a.flex_row, a.gap_md]}>
|
||||
{content}
|
||||
<BlockDrawerGesture>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap}
|
||||
decelerationRate="fast">
|
||||
<View style={[a.px_lg, a.pt_md, a.pb_xl, a.flex_row, a.gap_md]}>
|
||||
{content}
|
||||
|
||||
<Button
|
||||
label={_(msg`Browse more feeds on the Explore page`)}
|
||||
onPress={() => {
|
||||
navigation.navigate('SearchTab')
|
||||
}}
|
||||
style={[a.flex_col]}>
|
||||
<CardOuter style={[a.flex_1]}>
|
||||
<View style={[a.flex_1, a.justify_center]}>
|
||||
<View style={[a.flex_row, a.px_lg]}>
|
||||
<Text style={[a.pr_xl, a.flex_1, a.leading_snug]}>
|
||||
<Trans>Browse more suggestions on the Explore page</Trans>
|
||||
</Text>
|
||||
<Button
|
||||
label={_(msg`Browse more feeds on the Explore page`)}
|
||||
onPress={() => {
|
||||
navigation.navigate('SearchTab')
|
||||
}}
|
||||
style={[a.flex_col]}>
|
||||
<CardOuter style={[a.flex_1]}>
|
||||
<View style={[a.flex_1, a.justify_center]}>
|
||||
<View style={[a.flex_row, a.px_lg]}>
|
||||
<Text style={[a.pr_xl, a.flex_1, a.leading_snug]}>
|
||||
<Trans>
|
||||
Browse more suggestions on the Explore page
|
||||
</Trans>
|
||||
</Text>
|
||||
|
||||
<Arrow size="xl" />
|
||||
<Arrow size="xl" />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</CardOuter>
|
||||
</Button>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</CardOuter>
|
||||
</Button>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</BlockDrawerGesture>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import {createContext, useContext, useMemo} from 'react'
|
||||
import {View} from 'react-native'
|
||||
|
||||
import {atoms as a, ViewStyleProp} from '#/alf'
|
||||
|
||||
const Context = createContext({
|
||||
gap: 0,
|
||||
})
|
||||
|
||||
export function Row({
|
||||
children,
|
||||
gap = 0,
|
||||
style,
|
||||
}: ViewStyleProp & {
|
||||
children: React.ReactNode
|
||||
gap?: number
|
||||
}) {
|
||||
return (
|
||||
<Context.Provider value={useMemo(() => ({gap}), [gap])}>
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.flex_1,
|
||||
{
|
||||
marginLeft: -gap / 2,
|
||||
marginRight: -gap / 2,
|
||||
},
|
||||
style,
|
||||
]}>
|
||||
{children}
|
||||
</View>
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function Col({
|
||||
children,
|
||||
width = 1,
|
||||
style,
|
||||
}: ViewStyleProp & {
|
||||
children: React.ReactNode
|
||||
width?: number
|
||||
}) {
|
||||
const {gap} = useContext(Context)
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.flex_col,
|
||||
{
|
||||
paddingLeft: gap / 2,
|
||||
paddingRight: gap / 2,
|
||||
width: `${width * 100}%`,
|
||||
},
|
||||
style,
|
||||
]}>
|
||||
{children}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -163,6 +163,7 @@ function KnownFollowersInner({
|
||||
size={SIZE}
|
||||
avatar={prof.avatar}
|
||||
moderation={moderation.ui('avatar')}
|
||||
type={prof.associated?.labeler ? 'labeler' : 'user'}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
|
||||
@@ -122,7 +122,11 @@ export function BackButton({onPress, style, ...props}: Partial<ButtonProps>) {
|
||||
shape="square"
|
||||
onPress={onPressBack}
|
||||
hitSlop={HITSLOP_30}
|
||||
style={[{marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET}, style]}
|
||||
style={[
|
||||
{marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET},
|
||||
a.bg_transparent,
|
||||
style,
|
||||
]}
|
||||
{...props}>
|
||||
<ButtonIcon icon={ArrowLeft} size="lg" />
|
||||
</Button>
|
||||
|
||||
@@ -58,6 +58,7 @@ export const Content = React.memo(function Content({
|
||||
contentContainerStyle,
|
||||
...props
|
||||
}: ContentProps) {
|
||||
const t = useTheme()
|
||||
const {footerHeight} = useShellLayout()
|
||||
const animatedProps = useAnimatedProps(() => {
|
||||
return {
|
||||
@@ -73,6 +74,7 @@ export const Content = React.memo(function Content({
|
||||
<Animated.ScrollView
|
||||
id="content"
|
||||
automaticallyAdjustsScrollIndicatorInsets={false}
|
||||
indicatorStyle={t.scheme === 'dark' ? 'white' : 'black'}
|
||||
// sets the scroll inset to the height of the footer
|
||||
animatedProps={animatedProps}
|
||||
style={[scrollViewStyles.common, style]}
|
||||
|
||||
@@ -6,12 +6,18 @@ import {gradients} from '#/alf/tokens'
|
||||
|
||||
export function LinearGradientBackground({
|
||||
style,
|
||||
gradient = 'sky',
|
||||
children,
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
style: StyleProp<ViewStyle>
|
||||
children: React.ReactNode
|
||||
style?: StyleProp<ViewStyle>
|
||||
gradient?: keyof typeof gradients
|
||||
children?: React.ReactNode
|
||||
start?: [number, number]
|
||||
end?: [number, number]
|
||||
}) {
|
||||
const gradient = gradients.sky.values.map(([_, color]) => {
|
||||
const colors = gradients[gradient].values.map(([_, color]) => {
|
||||
return color
|
||||
}) as [string, string, ...string[]]
|
||||
|
||||
@@ -20,7 +26,7 @@ export function LinearGradientBackground({
|
||||
}
|
||||
|
||||
return (
|
||||
<LinearGradient colors={gradient} style={style}>
|
||||
<LinearGradient colors={colors} style={style} start={start} end={end}>
|
||||
{children}
|
||||
</LinearGradient>
|
||||
)
|
||||
|
||||
+120
-24
@@ -15,7 +15,6 @@ import {
|
||||
linkRequiresWarning,
|
||||
} from '#/lib/strings/url-helpers'
|
||||
import {isNative, isWeb} from '#/platform/detection'
|
||||
import {shouldClickOpenNewTab} from '#/platform/urls'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {atoms as a, flatten, TextStyleProp, useTheme, web} from '#/alf'
|
||||
import {Button, ButtonProps} from '#/components/Button'
|
||||
@@ -55,6 +54,12 @@ type BaseLinkProps = Pick<
|
||||
*/
|
||||
onPress?: (e: GestureResponderEvent) => void | false
|
||||
|
||||
/**
|
||||
* Callback for when the link is long pressed (on native). Prevent default
|
||||
* and return `false` to exit early and prevent default long press hander.
|
||||
*/
|
||||
onLongPress?: (e: GestureResponderEvent) => void | false
|
||||
|
||||
/**
|
||||
* Web-only attribute. Sets `download` attr on web.
|
||||
*/
|
||||
@@ -72,9 +77,12 @@ export function useLink({
|
||||
action = 'push',
|
||||
disableMismatchWarning,
|
||||
onPress: outerOnPress,
|
||||
onLongPress: outerOnLongPress,
|
||||
shareOnLongPress,
|
||||
overridePresentation,
|
||||
}: BaseLinkProps & {
|
||||
displayText: string
|
||||
overridePresentation?: boolean
|
||||
}) {
|
||||
const navigation = useNavigationDeduped()
|
||||
const {href} = useLinkProps<AllNavigatorParams>({
|
||||
@@ -110,7 +118,7 @@ export function useLink({
|
||||
})
|
||||
} else {
|
||||
if (isExternal) {
|
||||
openLink(href)
|
||||
openLink(href, overridePresentation)
|
||||
} else {
|
||||
const shouldOpenInNewTab = shouldClickOpenNewTab(e)
|
||||
|
||||
@@ -152,6 +160,7 @@ export function useLink({
|
||||
closeModal,
|
||||
action,
|
||||
navigation,
|
||||
overridePresentation,
|
||||
],
|
||||
)
|
||||
|
||||
@@ -175,8 +184,14 @@ export function useLink({
|
||||
}
|
||||
}, [disableMismatchWarning, displayText, href, isExternal, openModal])
|
||||
|
||||
const onLongPress =
|
||||
isNative && isExternal && shareOnLongPress ? handleLongPress : undefined
|
||||
const onLongPress = React.useCallback(
|
||||
(e: GestureResponderEvent) => {
|
||||
const exitEarlyIfFalse = outerOnLongPress?.(e)
|
||||
if (exitEarlyIfFalse === false) return
|
||||
return isNative && shareOnLongPress ? handleLongPress() : undefined
|
||||
},
|
||||
[outerOnLongPress, handleLongPress, shareOnLongPress],
|
||||
)
|
||||
|
||||
return {
|
||||
isExternal,
|
||||
@@ -202,14 +217,16 @@ export function Link({
|
||||
to,
|
||||
action = 'push',
|
||||
onPress: outerOnPress,
|
||||
onLongPress: outerOnLongPress,
|
||||
download,
|
||||
...rest
|
||||
}: LinkProps) {
|
||||
const {href, isExternal, onPress} = useLink({
|
||||
const {href, isExternal, onPress, onLongPress} = useLink({
|
||||
to,
|
||||
displayText: typeof children === 'string' ? children : '',
|
||||
action,
|
||||
onPress: outerOnPress,
|
||||
onLongPress: outerOnLongPress,
|
||||
})
|
||||
|
||||
return (
|
||||
@@ -220,6 +237,7 @@ export function Link({
|
||||
accessibilityRole="link"
|
||||
href={href}
|
||||
onPress={download ? undefined : onPress}
|
||||
onLongPress={onLongPress}
|
||||
{...web({
|
||||
hrefAttrs: {
|
||||
target: download ? undefined : isExternal ? 'blank' : undefined,
|
||||
@@ -239,12 +257,13 @@ export function Link({
|
||||
export type InlineLinkProps = React.PropsWithChildren<
|
||||
BaseLinkProps &
|
||||
TextStyleProp &
|
||||
Pick<TextProps, 'selectable' | 'numberOfLines'>
|
||||
> &
|
||||
Pick<ButtonProps, 'label'> & {
|
||||
disableUnderline?: boolean
|
||||
title?: TextProps['title']
|
||||
}
|
||||
Pick<TextProps, 'selectable' | 'numberOfLines'> &
|
||||
Pick<ButtonProps, 'label' | 'accessibilityHint'> & {
|
||||
disableUnderline?: boolean
|
||||
title?: TextProps['title']
|
||||
overridePresentation?: boolean
|
||||
}
|
||||
>
|
||||
|
||||
export function InlineLinkText({
|
||||
children,
|
||||
@@ -253,11 +272,13 @@ export function InlineLinkText({
|
||||
disableMismatchWarning,
|
||||
style,
|
||||
onPress: outerOnPress,
|
||||
onLongPress: outerOnLongPress,
|
||||
download,
|
||||
selectable,
|
||||
label,
|
||||
shareOnLongPress,
|
||||
disableUnderline,
|
||||
overridePresentation,
|
||||
...rest
|
||||
}: InlineLinkProps) {
|
||||
const t = useTheme()
|
||||
@@ -268,7 +289,9 @@ export function InlineLinkText({
|
||||
action,
|
||||
disableMismatchWarning,
|
||||
onPress: outerOnPress,
|
||||
onLongPress: outerOnLongPress,
|
||||
shareOnLongPress,
|
||||
overridePresentation,
|
||||
})
|
||||
const {
|
||||
state: hovered,
|
||||
@@ -319,6 +342,21 @@ export function InlineLinkText({
|
||||
)
|
||||
}
|
||||
|
||||
export function WebOnlyInlineLinkText({
|
||||
children,
|
||||
to,
|
||||
onPress,
|
||||
...props
|
||||
}: Omit<InlineLinkProps, 'onLongPress'>) {
|
||||
return isWeb ? (
|
||||
<InlineLinkText {...props} to={to} onPress={onPress}>
|
||||
{children}
|
||||
</InlineLinkText>
|
||||
) : (
|
||||
<Text {...props}>{children}</Text>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to create a static `onPress` handler for a `Link` that would otherwise link to a URI
|
||||
*
|
||||
@@ -327,7 +365,10 @@ export function InlineLinkText({
|
||||
*/
|
||||
export function createStaticClick(
|
||||
onPressHandler: Exclude<BaseLinkProps['onPress'], undefined>,
|
||||
): Pick<BaseLinkProps, 'to' | 'onPress'> {
|
||||
): {
|
||||
to: BaseLinkProps['to']
|
||||
onPress: Exclude<BaseLinkProps['onPress'], undefined>
|
||||
} {
|
||||
return {
|
||||
to: '#',
|
||||
onPress(e: GestureResponderEvent) {
|
||||
@@ -338,17 +379,72 @@ export function createStaticClick(
|
||||
}
|
||||
}
|
||||
|
||||
export function WebOnlyInlineLinkText({
|
||||
children,
|
||||
to,
|
||||
onPress,
|
||||
...props
|
||||
}: InlineLinkProps) {
|
||||
return isWeb ? (
|
||||
<InlineLinkText {...props} to={to} onPress={onPress}>
|
||||
{children}
|
||||
</InlineLinkText>
|
||||
) : (
|
||||
<Text {...props}>{children}</Text>
|
||||
/**
|
||||
* Utility to create a static `onPress` handler for a `Link`, but only if the
|
||||
* click was not modified in some way e.g. `Cmd` or a middle click.
|
||||
*
|
||||
* On native, this behaves the same as `createStaticClick` because there are no
|
||||
* options to "modify" the click in this sense.
|
||||
*
|
||||
* Example:
|
||||
* `<Link {...createStaticClick(e => {...})} />`
|
||||
*/
|
||||
export function createStaticClickIfUnmodified(
|
||||
onPressHandler: Exclude<BaseLinkProps['onPress'], undefined>,
|
||||
): {onPress: Exclude<BaseLinkProps['onPress'], undefined>} {
|
||||
return {
|
||||
onPress(e: GestureResponderEvent) {
|
||||
if (!isWeb || !isModifiedClickEvent(e)) {
|
||||
e.preventDefault()
|
||||
onPressHandler(e)
|
||||
return false
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the click event has a meta key pressed, indicating the user
|
||||
* intends to deviate from default behavior.
|
||||
*/
|
||||
export function isClickEventWithMetaKey(e: GestureResponderEvent) {
|
||||
if (!isWeb) return false
|
||||
const event = e as unknown as MouseEvent
|
||||
return event.metaKey || event.altKey || event.ctrlKey || event.shiftKey
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the web click target is anything other than `_self`
|
||||
*/
|
||||
export function isClickTargetExternal(e: GestureResponderEvent) {
|
||||
if (!isWeb) return false
|
||||
const event = e as unknown as MouseEvent
|
||||
const el = event.currentTarget as HTMLAnchorElement
|
||||
return el && el.target && el.target !== '_self'
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a click event has been modified in some way from its default
|
||||
* behavior, e.g. `Cmd` or a middle click.
|
||||
* {@link https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button}
|
||||
*/
|
||||
export function isModifiedClickEvent(e: GestureResponderEvent): boolean {
|
||||
if (!isWeb) return false
|
||||
const event = e as unknown as MouseEvent
|
||||
const isPrimaryButton = event.button === 0
|
||||
return (
|
||||
isClickEventWithMetaKey(e) || isClickTargetExternal(e) || !isPrimaryButton
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a click event has been modified in a way that should indiciate
|
||||
* that the user intends to open a new tab.
|
||||
* {@link https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button}
|
||||
*/
|
||||
export function shouldClickOpenNewTab(e: GestureResponderEvent) {
|
||||
if (!isWeb) return false
|
||||
const event = e as unknown as MouseEvent
|
||||
const isMiddleClick = isWeb && event.button === 1
|
||||
return isClickEventWithMetaKey(e) || isClickTargetExternal(e) || isMiddleClick
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ export function ListFooter({
|
||||
style,
|
||||
showEndMessage = false,
|
||||
endMessageText,
|
||||
renderEndMessage,
|
||||
}: {
|
||||
isFetchingNextPage?: boolean
|
||||
hasNextPage?: boolean
|
||||
@@ -29,6 +30,7 @@ export function ListFooter({
|
||||
style?: StyleProp<ViewStyle>
|
||||
showEndMessage?: boolean
|
||||
endMessageText?: string
|
||||
renderEndMessage?: () => React.ReactNode
|
||||
}) {
|
||||
const t = useTheme()
|
||||
|
||||
@@ -48,9 +50,13 @@ export function ListFooter({
|
||||
) : error ? (
|
||||
<ListFooterMaybeError error={error} onRetry={onRetry} />
|
||||
) : !hasNextPage && showEndMessage ? (
|
||||
<Text style={[a.text_sm, t.atoms.text_contrast_low]}>
|
||||
{endMessageText ?? <Trans>You have reached the end</Trans>}
|
||||
</Text>
|
||||
renderEndMessage ? (
|
||||
renderEndMessage()
|
||||
) : (
|
||||
<Text style={[a.text_sm, t.atoms.text_contrast_low]}>
|
||||
{endMessageText ?? <Trans>You have reached the end</Trans>}
|
||||
</Text>
|
||||
)
|
||||
) : null}
|
||||
</View>
|
||||
)
|
||||
|
||||
@@ -47,7 +47,12 @@ export function Root({
|
||||
return <Context.Provider value={context}>{children}</Context.Provider>
|
||||
}
|
||||
|
||||
export function Trigger({children, label, role = 'button'}: TriggerProps) {
|
||||
export function Trigger({
|
||||
children,
|
||||
label,
|
||||
role = 'button',
|
||||
hint,
|
||||
}: TriggerProps) {
|
||||
const context = useMenuContext()
|
||||
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
|
||||
const {
|
||||
@@ -65,11 +70,13 @@ export function Trigger({children, label, role = 'button'}: TriggerProps) {
|
||||
pressed,
|
||||
},
|
||||
props: {
|
||||
ref: null,
|
||||
onPress: context.control.open,
|
||||
onFocus,
|
||||
onBlur,
|
||||
onPressIn,
|
||||
onPressOut,
|
||||
accessibilityHint: hint,
|
||||
accessibilityLabel: label,
|
||||
accessibilityRole: role,
|
||||
},
|
||||
|
||||
@@ -110,7 +110,12 @@ const RadixTriggerPassThrough = React.forwardRef(
|
||||
)
|
||||
RadixTriggerPassThrough.displayName = 'RadixTriggerPassThrough'
|
||||
|
||||
export function Trigger({children, label, role = 'button'}: TriggerProps) {
|
||||
export function Trigger({
|
||||
children,
|
||||
label,
|
||||
role = 'button',
|
||||
hint,
|
||||
}: TriggerProps) {
|
||||
const {control} = useMenuContext()
|
||||
const {
|
||||
state: hovered,
|
||||
@@ -153,6 +158,7 @@ export function Trigger({children, label, role = 'button'}: TriggerProps) {
|
||||
onBlur: onBlur,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
accessibilityHint: hint,
|
||||
accessibilityLabel: label,
|
||||
accessibilityRole: role,
|
||||
},
|
||||
@@ -202,7 +208,7 @@ export function Outer({
|
||||
)
|
||||
}
|
||||
|
||||
export function Item({children, label, onPress, ...rest}: ItemProps) {
|
||||
export function Item({children, label, onPress, style, ...rest}: ItemProps) {
|
||||
const t = useTheme()
|
||||
const {control} = useMenuContext()
|
||||
const {
|
||||
@@ -248,6 +254,7 @@ export function Item({children, label, onPress, ...rest}: ItemProps) {
|
||||
? t.atoms.bg_contrast_25
|
||||
: t.atoms.bg_contrast_50,
|
||||
],
|
||||
style,
|
||||
])}
|
||||
{...web({
|
||||
onMouseEnter,
|
||||
|
||||
@@ -19,6 +19,7 @@ export type ItemContextType = {
|
||||
}
|
||||
|
||||
export type RadixPassThroughTriggerProps = {
|
||||
ref: React.RefObject<any>
|
||||
id: string
|
||||
type: 'button'
|
||||
disabled: boolean
|
||||
@@ -37,6 +38,7 @@ export type RadixPassThroughTriggerProps = {
|
||||
export type TriggerProps = {
|
||||
children(props: TriggerChildProps): React.ReactNode
|
||||
label: string
|
||||
hint?: string
|
||||
role?: AccessibilityRole
|
||||
}
|
||||
export type TriggerChildProps =
|
||||
@@ -59,11 +61,13 @@ export type TriggerChildProps =
|
||||
* object is empty.
|
||||
*/
|
||||
props: {
|
||||
ref: null
|
||||
onPress: () => void
|
||||
onFocus: () => void
|
||||
onBlur: () => void
|
||||
onPressIn: () => void
|
||||
onPressOut: () => void
|
||||
accessibilityHint?: string
|
||||
accessibilityLabel: string
|
||||
accessibilityRole: AccessibilityRole
|
||||
}
|
||||
@@ -85,6 +89,7 @@ export type TriggerChildProps =
|
||||
onBlur: () => void
|
||||
onMouseEnter: () => void
|
||||
onMouseLeave: () => void
|
||||
accessibilityHint?: string
|
||||
accessibilityLabel: string
|
||||
accessibilityRole: AccessibilityRole
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ export function Label({
|
||||
fill={t.atoms.text_contrast_medium.color}
|
||||
/>
|
||||
) : (
|
||||
<UserAvatar avatar={desc.sourceAvi} size={avi} />
|
||||
<UserAvatar avatar={desc.sourceAvi} type="user" size={avi} />
|
||||
)}
|
||||
|
||||
<Text
|
||||
|
||||
@@ -285,6 +285,7 @@ export type FollowButtonProps = {
|
||||
logContext: LogEvents['profile:follow']['logContext'] &
|
||||
LogEvents['profile:unfollow']['logContext']
|
||||
colorInverted?: boolean
|
||||
onFollow?: () => void
|
||||
} & Partial<ButtonProps>
|
||||
|
||||
export function FollowButton(props: FollowButtonProps) {
|
||||
@@ -298,6 +299,7 @@ export function FollowButtonInner({
|
||||
moderationOpts,
|
||||
logContext,
|
||||
onPress: onPressProp,
|
||||
onFollow,
|
||||
colorInverted,
|
||||
...rest
|
||||
}: FollowButtonProps) {
|
||||
@@ -324,6 +326,7 @@ export function FollowButtonInner({
|
||||
),
|
||||
)
|
||||
onPressProp?.(e)
|
||||
onFollow?.()
|
||||
} catch (err: any) {
|
||||
if (err?.name !== 'AbortError') {
|
||||
Toast.show(_(msg`An issue occurred, please try again.`), 'xmark')
|
||||
|
||||
@@ -25,9 +25,7 @@ import {SquareArrowTopRight_Stroke2_Corner0_Rounded as SquareArrowTopRight} from
|
||||
import {Text} from '#/components/Typography'
|
||||
import {ReportDialogProps} from './types'
|
||||
|
||||
export function SelectReportOptionView({
|
||||
...props
|
||||
}: {
|
||||
export function SelectReportOptionView(props: {
|
||||
params: ReportDialogProps['params']
|
||||
labelers: AppBskyLabelerDefs.LabelerViewDetailed[]
|
||||
onSelectReportOption: (reportOption: ReportOption) => void
|
||||
|
||||
@@ -16,6 +16,7 @@ import * as Dialog from '#/components/Dialog'
|
||||
import * as Toggle from '#/components/forms/Toggle'
|
||||
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
|
||||
import {PaperPlane_Stroke2_Corner0_Rounded as SendIcon} from '#/components/icons/PaperPlane'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {ReportDialogProps} from './types'
|
||||
@@ -223,7 +224,7 @@ export function SubmitView({
|
||||
<ButtonText>
|
||||
<Trans>Send report</Trans>
|
||||
</ButtonText>
|
||||
{submitting && <ButtonIcon icon={Loader} />}
|
||||
<ButtonIcon icon={submitting ? Loader : SendIcon} />
|
||||
</Button>
|
||||
</View>
|
||||
{/* Maybe fix this later -h */}
|
||||
|
||||
+13
-91
@@ -1,25 +1,19 @@
|
||||
import React from 'react'
|
||||
import {TextStyle} from 'react-native'
|
||||
import {AppBskyRichtextFacet, RichText as RichTextAPI} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
import {toShortUrl} from '#/lib/strings/url-helpers'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {atoms as a, flatten, native, TextStyleProp, useTheme, web} from '#/alf'
|
||||
import {atoms as a, flatten, TextStyleProp} from '#/alf'
|
||||
import {isOnlyEmoji} from '#/alf/typography'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {InlineLinkText, LinkProps} from '#/components/Link'
|
||||
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
|
||||
import {TagMenu, useTagMenuControl} from '#/components/TagMenu'
|
||||
import {RichTextTag} from '#/components/RichTextTag'
|
||||
import {Text, TextProps} from '#/components/Typography'
|
||||
|
||||
const WORD_WRAP = {wordWrap: 1}
|
||||
|
||||
export type RichTextProps = TextStyleProp &
|
||||
Pick<TextProps, 'selectable'> & {
|
||||
Pick<TextProps, 'selectable' | 'onLayout' | 'onTextLayout'> & {
|
||||
value: RichTextAPI | string
|
||||
testID?: string
|
||||
numberOfLines?: number
|
||||
@@ -43,6 +37,8 @@ export function RichText({
|
||||
onLinkPress,
|
||||
interactiveStyle,
|
||||
emojiMultiplier = 1.85,
|
||||
onLayout,
|
||||
onTextLayout,
|
||||
}: RichTextProps) {
|
||||
const richText = React.useMemo(
|
||||
() =>
|
||||
@@ -70,6 +66,8 @@ export function RichText({
|
||||
selectable={selectable}
|
||||
testID={testID}
|
||||
style={[plainStyles, {fontSize}]}
|
||||
onLayout={onLayout}
|
||||
onTextLayout={onTextLayout}
|
||||
// @ts-ignore web only -prf
|
||||
dataSet={WORD_WRAP}>
|
||||
{text}
|
||||
@@ -83,6 +81,8 @@ export function RichText({
|
||||
testID={testID}
|
||||
style={plainStyles}
|
||||
numberOfLines={numberOfLines}
|
||||
onLayout={onLayout}
|
||||
onTextLayout={onTextLayout}
|
||||
// @ts-ignore web only -prf
|
||||
dataSet={WORD_WRAP}>
|
||||
{text}
|
||||
@@ -143,10 +143,9 @@ export function RichText({
|
||||
els.push(
|
||||
<RichTextTag
|
||||
key={key}
|
||||
text={segment.text}
|
||||
display={segment.text}
|
||||
tag={tag.tag}
|
||||
style={interactiveStyles}
|
||||
selectable={selectable}
|
||||
textStyle={interactiveStyles}
|
||||
authorHandle={authorHandle}
|
||||
/>,
|
||||
)
|
||||
@@ -163,88 +162,11 @@ export function RichText({
|
||||
testID={testID}
|
||||
style={plainStyles}
|
||||
numberOfLines={numberOfLines}
|
||||
onLayout={onLayout}
|
||||
onTextLayout={onTextLayout}
|
||||
// @ts-ignore web only -prf
|
||||
dataSet={WORD_WRAP}>
|
||||
{els}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
function RichTextTag({
|
||||
text,
|
||||
tag,
|
||||
style,
|
||||
selectable,
|
||||
authorHandle,
|
||||
}: {
|
||||
text: string
|
||||
tag: string
|
||||
selectable?: boolean
|
||||
authorHandle?: string
|
||||
} & TextStyleProp) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const control = useTagMenuControl()
|
||||
const {
|
||||
state: hovered,
|
||||
onIn: onHoverIn,
|
||||
onOut: onHoverOut,
|
||||
} = useInteractionState()
|
||||
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
|
||||
const navigateToPage = React.useCallback(() => {
|
||||
navigation.push('Hashtag', {
|
||||
tag: encodeURIComponent(tag),
|
||||
})
|
||||
}, [navigation, tag])
|
||||
|
||||
const openDialog = React.useCallback(() => {
|
||||
control.open()
|
||||
}, [control])
|
||||
|
||||
/*
|
||||
* N.B. On web, this is wrapped in another pressable comopnent with a11y
|
||||
* labels, etc. That's why only some of these props are applied here.
|
||||
*/
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<TagMenu control={control} tag={tag} authorHandle={authorHandle}>
|
||||
<Text
|
||||
emoji
|
||||
selectable={selectable}
|
||||
{...native({
|
||||
accessibilityLabel: _(msg`Hashtag: #${tag}`),
|
||||
accessibilityHint: _(msg`Long press to open tag menu for #${tag}`),
|
||||
accessibilityRole: isNative ? 'button' : undefined,
|
||||
onPress: navigateToPage,
|
||||
onLongPress: openDialog,
|
||||
})}
|
||||
{...web({
|
||||
onMouseEnter: onHoverIn,
|
||||
onMouseLeave: onHoverOut,
|
||||
})}
|
||||
// @ts-ignore
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
style={[
|
||||
web({
|
||||
cursor: 'pointer',
|
||||
}),
|
||||
{color: t.palette.primary_500},
|
||||
(hovered || focused) && {
|
||||
...web({
|
||||
outline: 0,
|
||||
textDecorationLine: 'underline',
|
||||
textDecorationColor: t.palette.primary_500,
|
||||
}),
|
||||
},
|
||||
style,
|
||||
]}>
|
||||
{text}
|
||||
</Text>
|
||||
</TagMenu>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
import React from 'react'
|
||||
import {StyleProp, Text as RNText, TextStyle} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
import {isInvalidHandle} from '#/lib/strings/handles'
|
||||
import {isNative, isWeb} from '#/platform/detection'
|
||||
import {
|
||||
usePreferencesQuery,
|
||||
useRemoveMutedWordsMutation,
|
||||
useUpsertMutedWordsMutation,
|
||||
} from '#/state/queries/preferences'
|
||||
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
|
||||
import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
|
||||
import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person'
|
||||
import {
|
||||
createStaticClick,
|
||||
createStaticClickIfUnmodified,
|
||||
InlineLinkText,
|
||||
} from '#/components/Link'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import * as Menu from '#/components/Menu'
|
||||
|
||||
export function RichTextTag({
|
||||
tag,
|
||||
display,
|
||||
authorHandle,
|
||||
textStyle,
|
||||
}: {
|
||||
tag: string
|
||||
display: string
|
||||
authorHandle?: string
|
||||
textStyle: StyleProp<TextStyle>
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const {isLoading: isPreferencesLoading, data: preferences} =
|
||||
usePreferencesQuery()
|
||||
const {
|
||||
mutateAsync: upsertMutedWord,
|
||||
variables: optimisticUpsert,
|
||||
reset: resetUpsert,
|
||||
} = useUpsertMutedWordsMutation()
|
||||
const {
|
||||
mutateAsync: removeMutedWords,
|
||||
variables: optimisticRemove,
|
||||
reset: resetRemove,
|
||||
} = useRemoveMutedWordsMutation()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const label = _(msg`Hashtag ${tag}`)
|
||||
const hint = isNative
|
||||
? _(msg`Long press to open tag menu for #${tag}`)
|
||||
: _(msg`Click to open tag menu for ${tag}`)
|
||||
|
||||
const isMuted = Boolean(
|
||||
(preferences?.moderationPrefs.mutedWords?.find(
|
||||
m => m.value === tag && m.targets.includes('tag'),
|
||||
) ??
|
||||
optimisticUpsert?.find(
|
||||
m => m.value === tag && m.targets.includes('tag'),
|
||||
)) &&
|
||||
!optimisticRemove?.find(m => m?.value === tag),
|
||||
)
|
||||
|
||||
/*
|
||||
* Mute word records that exactly match the tag in question.
|
||||
*/
|
||||
const removeableMuteWords = React.useMemo(() => {
|
||||
return (
|
||||
preferences?.moderationPrefs.mutedWords?.filter(word => {
|
||||
return word.value === tag
|
||||
}) || []
|
||||
)
|
||||
}, [tag, preferences?.moderationPrefs?.mutedWords])
|
||||
|
||||
return (
|
||||
<Menu.Root>
|
||||
<Menu.Trigger label={label} hint={hint}>
|
||||
{({props: menuProps}) => (
|
||||
<InlineLinkText
|
||||
to={{
|
||||
screen: 'Hashtag',
|
||||
params: {tag: encodeURIComponent(tag)},
|
||||
}}
|
||||
{...menuProps}
|
||||
onPress={e => {
|
||||
if (isWeb) {
|
||||
return createStaticClickIfUnmodified(() => {
|
||||
if (!isNative) {
|
||||
menuProps.onPress()
|
||||
}
|
||||
}).onPress(e)
|
||||
}
|
||||
}}
|
||||
onLongPress={createStaticClick(menuProps.onPress).onPress}
|
||||
accessibilityHint={hint}
|
||||
label={label}
|
||||
style={textStyle}>
|
||||
{isNative ? (
|
||||
display
|
||||
) : (
|
||||
<RNText ref={menuProps.ref}>{display}</RNText>
|
||||
)}
|
||||
</InlineLinkText>
|
||||
)}
|
||||
</Menu.Trigger>
|
||||
<Menu.Outer>
|
||||
<Menu.Group>
|
||||
<Menu.Item
|
||||
label={_(msg`See ${tag} posts`)}
|
||||
onPress={() => {
|
||||
navigation.push('Hashtag', {
|
||||
tag: encodeURIComponent(tag),
|
||||
})
|
||||
}}>
|
||||
<Menu.ItemText>
|
||||
<Trans>See #{tag} posts</Trans>
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Search} />
|
||||
</Menu.Item>
|
||||
{authorHandle && !isInvalidHandle(authorHandle) && (
|
||||
<Menu.Item
|
||||
label={_(msg`See ${tag} posts by user`)}
|
||||
onPress={() => {
|
||||
navigation.push('Hashtag', {
|
||||
tag: encodeURIComponent(tag),
|
||||
author: authorHandle,
|
||||
})
|
||||
}}>
|
||||
<Menu.ItemText>
|
||||
<Trans>See #{tag} posts by user</Trans>
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Person} />
|
||||
</Menu.Item>
|
||||
)}
|
||||
</Menu.Group>
|
||||
<Menu.Divider />
|
||||
<Menu.Item
|
||||
label={isMuted ? _(msg`Unmute ${tag}`) : _(msg`Mute ${tag}`)}
|
||||
onPress={() => {
|
||||
if (isMuted) {
|
||||
resetUpsert()
|
||||
removeMutedWords(removeableMuteWords)
|
||||
} else {
|
||||
resetRemove()
|
||||
upsertMutedWord([
|
||||
{value: tag, targets: ['tag'], actorTarget: 'all'},
|
||||
])
|
||||
}
|
||||
}}>
|
||||
<Menu.ItemText>
|
||||
{isMuted ? _(msg`Unmute ${tag}`) : _(msg`Mute ${tag}`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={isPreferencesLoading ? Loader : Mute} />
|
||||
</Menu.Item>
|
||||
</Menu.Outer>
|
||||
</Menu.Root>
|
||||
)
|
||||
}
|
||||
@@ -136,7 +136,6 @@ export const ProfileStarterPacks = React.forwardRef<
|
||||
headerOffset={headerOffset}
|
||||
progressViewOffset={ios(0)}
|
||||
contentContainerStyle={{paddingBottom: headerOffset + bottomBarOffset}}
|
||||
indicatorStyle={t.name === 'light' ? 'black' : 'white'}
|
||||
removeClippedSubviews={true}
|
||||
desktopFixedHeight
|
||||
onEndReached={onEndReached}
|
||||
|
||||
@@ -1,290 +0,0 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
import {isInvalidHandle} from '#/lib/strings/handles'
|
||||
import {
|
||||
usePreferencesQuery,
|
||||
useRemoveMutedWordsMutation,
|
||||
useUpsertMutedWordsMutation,
|
||||
} from '#/state/queries/preferences'
|
||||
import {atoms as a, native, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {Divider} from '#/components/Divider'
|
||||
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
|
||||
import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
|
||||
import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person'
|
||||
import {createStaticClick, Link} from '#/components/Link'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function useTagMenuControl() {
|
||||
return Dialog.useDialogControl()
|
||||
}
|
||||
|
||||
export function TagMenu({
|
||||
children,
|
||||
control,
|
||||
tag,
|
||||
authorHandle,
|
||||
}: React.PropsWithChildren<{
|
||||
control: Dialog.DialogOuterProps['control']
|
||||
/**
|
||||
* This should be the sanitized tag value from the facet itself, not the
|
||||
* "display" value with a leading `#`.
|
||||
*/
|
||||
tag: string
|
||||
authorHandle?: string
|
||||
}>) {
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Handle />
|
||||
<TagMenuInner
|
||||
control={control}
|
||||
tag={tag}
|
||||
authorHandle={authorHandle}
|
||||
navigation={navigation}
|
||||
/>
|
||||
</Dialog.Outer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function TagMenuInner({
|
||||
control,
|
||||
tag,
|
||||
authorHandle,
|
||||
navigation,
|
||||
}: {
|
||||
control: Dialog.DialogOuterProps['control']
|
||||
tag: string
|
||||
authorHandle?: string
|
||||
// Passed down because on native, we don't use real portals (and context would be wrong).
|
||||
navigation: NavigationProp
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
const {isLoading: isPreferencesLoading, data: preferences} =
|
||||
usePreferencesQuery()
|
||||
const {
|
||||
mutateAsync: upsertMutedWord,
|
||||
variables: optimisticUpsert,
|
||||
reset: resetUpsert,
|
||||
} = useUpsertMutedWordsMutation()
|
||||
const {
|
||||
mutateAsync: removeMutedWords,
|
||||
variables: optimisticRemove,
|
||||
reset: resetRemove,
|
||||
} = useRemoveMutedWordsMutation()
|
||||
const displayTag = '#' + tag
|
||||
|
||||
const isMuted = Boolean(
|
||||
(preferences?.moderationPrefs.mutedWords?.find(
|
||||
m => m.value === tag && m.targets.includes('tag'),
|
||||
) ??
|
||||
optimisticUpsert?.find(
|
||||
m => m.value === tag && m.targets.includes('tag'),
|
||||
)) &&
|
||||
!optimisticRemove?.find(m => m?.value === tag),
|
||||
)
|
||||
|
||||
/*
|
||||
* Mute word records that exactly match the tag in question.
|
||||
*/
|
||||
const removeableMuteWords = React.useMemo(() => {
|
||||
return (
|
||||
preferences?.moderationPrefs.mutedWords?.filter(word => {
|
||||
return word.value === tag
|
||||
}) || []
|
||||
)
|
||||
}, [tag, preferences?.moderationPrefs?.mutedWords])
|
||||
|
||||
return (
|
||||
<Dialog.Inner label={_(msg`Tag menu: ${displayTag}`)}>
|
||||
{isPreferencesLoading ? (
|
||||
<View style={[a.w_full, a.align_center]}>
|
||||
<Loader size="lg" />
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<View
|
||||
style={[
|
||||
a.rounded_md,
|
||||
a.border,
|
||||
a.mb_md,
|
||||
t.atoms.border_contrast_low,
|
||||
t.atoms.bg_contrast_25,
|
||||
]}>
|
||||
<Link
|
||||
label={_(msg`View all posts with tag ${displayTag}`)}
|
||||
{...createStaticClick(() => {
|
||||
control.close(() => {
|
||||
navigation.push('Hashtag', {
|
||||
tag: encodeURIComponent(tag),
|
||||
})
|
||||
})
|
||||
})}>
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.justify_start,
|
||||
a.gap_md,
|
||||
a.px_lg,
|
||||
a.py_md,
|
||||
]}>
|
||||
<Search size="lg" style={[t.atoms.text_contrast_medium]} />
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="middle"
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.text_md,
|
||||
a.font_bold,
|
||||
native({top: 2}),
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
<Trans>
|
||||
See{' '}
|
||||
<Text style={[a.text_md, a.font_bold, t.atoms.text]}>
|
||||
{displayTag}
|
||||
</Text>{' '}
|
||||
posts
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</Link>
|
||||
|
||||
{authorHandle && !isInvalidHandle(authorHandle) && (
|
||||
<>
|
||||
<Divider />
|
||||
|
||||
<Link
|
||||
label={_(
|
||||
msg`View all posts by @${authorHandle} with tag ${displayTag}`,
|
||||
)}
|
||||
{...createStaticClick(() => {
|
||||
control.close(() => {
|
||||
navigation.push('Hashtag', {
|
||||
tag: encodeURIComponent(tag),
|
||||
author: authorHandle,
|
||||
})
|
||||
})
|
||||
})}>
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.justify_start,
|
||||
a.gap_md,
|
||||
a.px_lg,
|
||||
a.py_md,
|
||||
]}>
|
||||
<Person size="lg" style={[t.atoms.text_contrast_medium]} />
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="middle"
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.text_md,
|
||||
a.font_bold,
|
||||
native({top: 2}),
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
<Trans>
|
||||
See{' '}
|
||||
<Text style={[a.text_md, a.font_bold, t.atoms.text]}>
|
||||
{displayTag}
|
||||
</Text>{' '}
|
||||
posts by this user
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
|
||||
{preferences ? (
|
||||
<>
|
||||
<Divider />
|
||||
|
||||
<Button
|
||||
label={
|
||||
isMuted
|
||||
? _(msg`Unmute all ${displayTag} posts`)
|
||||
: _(msg`Mute all ${displayTag} posts`)
|
||||
}
|
||||
onPress={() => {
|
||||
control.close(() => {
|
||||
if (isMuted) {
|
||||
resetUpsert()
|
||||
removeMutedWords(removeableMuteWords)
|
||||
} else {
|
||||
resetRemove()
|
||||
upsertMutedWord([
|
||||
{
|
||||
value: tag,
|
||||
targets: ['tag'],
|
||||
actorTarget: 'all',
|
||||
},
|
||||
])
|
||||
}
|
||||
})
|
||||
}}>
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.justify_start,
|
||||
a.gap_md,
|
||||
a.px_lg,
|
||||
a.py_md,
|
||||
]}>
|
||||
<Mute size="lg" style={[t.atoms.text_contrast_medium]} />
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="middle"
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.text_md,
|
||||
a.font_bold,
|
||||
native({top: 2}),
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
{isMuted ? _(msg`Unmute`) : _(msg`Mute`)}{' '}
|
||||
<Text style={[a.text_md, a.font_bold, t.atoms.text]}>
|
||||
{displayTag}
|
||||
</Text>{' '}
|
||||
<Trans>posts</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<Button
|
||||
label={_(msg`Close this dialog`)}
|
||||
size="small"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
onPress={() => control.close()}>
|
||||
<ButtonText>
|
||||
<Trans>Cancel</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Dialog.Inner>
|
||||
)
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
import React from 'react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
import {isInvalidHandle} from '#/lib/strings/handles'
|
||||
import {enforceLen} from '#/lib/strings/helpers'
|
||||
import {
|
||||
usePreferencesQuery,
|
||||
useRemoveMutedWordsMutation,
|
||||
useUpsertMutedWordsMutation,
|
||||
} from '#/state/queries/preferences'
|
||||
import {EventStopper} from '#/view/com/util/EventStopper'
|
||||
import {NativeDropdown} from '#/view/com/util/forms/NativeDropdown'
|
||||
import {web} from '#/alf'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
|
||||
export function useTagMenuControl(): Dialog.DialogControlProps {
|
||||
return {
|
||||
id: '',
|
||||
// @ts-ignore
|
||||
ref: null,
|
||||
open: () => {
|
||||
throw new Error(`TagMenu controls are only available on native platforms`)
|
||||
},
|
||||
close: () => {
|
||||
throw new Error(`TagMenu controls are only available on native platforms`)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function TagMenu({
|
||||
children,
|
||||
tag,
|
||||
authorHandle,
|
||||
}: React.PropsWithChildren<{
|
||||
/**
|
||||
* This should be the sanitized tag value from the facet itself, not the
|
||||
* "display" value with a leading `#`.
|
||||
*/
|
||||
tag: string
|
||||
authorHandle?: string
|
||||
}>) {
|
||||
const {_} = useLingui()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {data: preferences} = usePreferencesQuery()
|
||||
const {mutateAsync: upsertMutedWord, variables: optimisticUpsert} =
|
||||
useUpsertMutedWordsMutation()
|
||||
const {mutateAsync: removeMutedWords, variables: optimisticRemove} =
|
||||
useRemoveMutedWordsMutation()
|
||||
const isMuted = Boolean(
|
||||
(preferences?.moderationPrefs.mutedWords?.find(
|
||||
m => m.value === tag && m.targets.includes('tag'),
|
||||
) ??
|
||||
optimisticUpsert?.find(
|
||||
m => m.value === tag && m.targets.includes('tag'),
|
||||
)) &&
|
||||
!optimisticRemove?.find(m => m?.value === tag),
|
||||
)
|
||||
const truncatedTag = '#' + enforceLen(tag, 15, true, 'middle')
|
||||
|
||||
/*
|
||||
* Mute word records that exactly match the tag in question.
|
||||
*/
|
||||
const removeableMuteWords = React.useMemo(() => {
|
||||
return (
|
||||
preferences?.moderationPrefs.mutedWords?.filter(word => {
|
||||
return word.value === tag
|
||||
}) || []
|
||||
)
|
||||
}, [tag, preferences?.moderationPrefs?.mutedWords])
|
||||
|
||||
const dropdownItems = React.useMemo(() => {
|
||||
return [
|
||||
{
|
||||
label: _(msg`See ${truncatedTag} posts`),
|
||||
onPress() {
|
||||
navigation.push('Hashtag', {
|
||||
tag: encodeURIComponent(tag),
|
||||
})
|
||||
},
|
||||
testID: 'tagMenuSearch',
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'magnifyingglass',
|
||||
},
|
||||
android: '',
|
||||
web: 'magnifying-glass',
|
||||
},
|
||||
},
|
||||
authorHandle &&
|
||||
!isInvalidHandle(authorHandle) && {
|
||||
label: _(msg`See ${truncatedTag} posts by user`),
|
||||
onPress() {
|
||||
navigation.push('Hashtag', {
|
||||
tag: encodeURIComponent(tag),
|
||||
author: authorHandle,
|
||||
})
|
||||
},
|
||||
testID: 'tagMenuSearchByUser',
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'magnifyingglass',
|
||||
},
|
||||
android: '',
|
||||
web: ['far', 'user'],
|
||||
},
|
||||
},
|
||||
preferences && {
|
||||
label: 'separator',
|
||||
},
|
||||
preferences && {
|
||||
label: isMuted
|
||||
? _(msg`Unmute ${truncatedTag}`)
|
||||
: _(msg`Mute ${truncatedTag}`),
|
||||
onPress() {
|
||||
if (isMuted) {
|
||||
removeMutedWords(removeableMuteWords)
|
||||
} else {
|
||||
upsertMutedWord([
|
||||
{value: tag, targets: ['tag'], actorTarget: 'all'},
|
||||
])
|
||||
}
|
||||
},
|
||||
testID: 'tagMenuMute',
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'speaker.slash',
|
||||
},
|
||||
android: 'ic_menu_sort_alphabetically',
|
||||
web: isMuted ? 'eye' : ['far', 'eye-slash'],
|
||||
},
|
||||
},
|
||||
].filter(Boolean)
|
||||
}, [
|
||||
_,
|
||||
authorHandle,
|
||||
isMuted,
|
||||
navigation,
|
||||
preferences,
|
||||
tag,
|
||||
truncatedTag,
|
||||
upsertMutedWord,
|
||||
removeMutedWords,
|
||||
removeableMuteWords,
|
||||
])
|
||||
|
||||
return (
|
||||
<EventStopper>
|
||||
<NativeDropdown
|
||||
accessibilityLabel={_(msg`Click here to open tag menu for ${tag}`)}
|
||||
accessibilityHint=""
|
||||
// @ts-ignore
|
||||
items={dropdownItems}
|
||||
triggerStyle={web({
|
||||
textAlign: 'left',
|
||||
})}>
|
||||
{children}
|
||||
</NativeDropdown>
|
||||
</EventStopper>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,540 @@
|
||||
import {useMemo} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {Image} from 'expo-image'
|
||||
import {LinearGradient} from 'expo-linear-gradient'
|
||||
import {
|
||||
AppBskyActorDefs,
|
||||
AppBskyEmbedVideo,
|
||||
AppBskyFeedDefs,
|
||||
AppBskyFeedPost,
|
||||
ModerationDecision,
|
||||
} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {formatCount} from '#/view/com/util/numeric/format'
|
||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {VideoFeedSourceContext} from '#/screens/VideoFeed/types'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {BLUE_HUE} from '#/alf/util/colorGeneration'
|
||||
import {select} from '#/alf/util/themeSelector'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {EyeSlash_Stroke2_Corner0_Rounded as Eye} from '#/components/icons/EyeSlash'
|
||||
import {Heart2_Stroke2_Corner0_Rounded as Heart} from '#/components/icons/Heart2'
|
||||
import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost'
|
||||
import {Link} from '#/components/Link'
|
||||
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||
import * as Hider from '#/components/moderation/Hider'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
function getBlackColor(t: ReturnType<typeof useTheme>) {
|
||||
return select(t.name, {
|
||||
light: t.palette.black,
|
||||
dark: t.atoms.bg_contrast_25.backgroundColor,
|
||||
dim: `hsl(${BLUE_HUE}, 28%, 6%)`,
|
||||
})
|
||||
}
|
||||
|
||||
export function VideoPostCard({
|
||||
post,
|
||||
sourceContext,
|
||||
moderation,
|
||||
onInteract,
|
||||
}: {
|
||||
post: AppBskyFeedDefs.PostView
|
||||
sourceContext: VideoFeedSourceContext
|
||||
moderation: ModerationDecision
|
||||
/**
|
||||
* Callback for metrics etc
|
||||
*/
|
||||
onInteract?: () => void
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_, i18n} = useLingui()
|
||||
const embed = post.embed
|
||||
const {
|
||||
state: pressed,
|
||||
onIn: onPressIn,
|
||||
onOut: onPressOut,
|
||||
} = useInteractionState()
|
||||
|
||||
const listModUi = moderation.ui('contentList')
|
||||
|
||||
const mergedModui = useMemo(() => {
|
||||
const modui = moderation.ui('contentList')
|
||||
const mediaModui = moderation.ui('contentMedia')
|
||||
modui.alerts = [...modui.alerts, ...mediaModui.alerts]
|
||||
modui.blurs = [...modui.blurs, ...mediaModui.blurs]
|
||||
modui.filters = [...modui.filters, ...mediaModui.filters]
|
||||
modui.informs = [...modui.informs, ...mediaModui.informs]
|
||||
return modui
|
||||
}, [moderation])
|
||||
|
||||
/**
|
||||
* Filtering should be done at a higher level, such as `PostFeed` or
|
||||
* `PostFeedVideoGridRow`, but we need to protect here as well.
|
||||
*/
|
||||
if (!AppBskyEmbedVideo.isView(embed)) return null
|
||||
|
||||
const author = post.author
|
||||
const text = AppBskyFeedPost.isRecord(post.record) ? post.record?.text : ''
|
||||
const likeCount = post?.likeCount ?? 0
|
||||
const repostCount = post?.repostCount ?? 0
|
||||
const {thumbnail} = embed
|
||||
const black = getBlackColor(t)
|
||||
|
||||
const textAndAuthor = (
|
||||
<View style={[a.pr_xs, {paddingTop: 6, gap: 4}]}>
|
||||
{text && (
|
||||
<Text style={[a.text_md, a.leading_snug]} numberOfLines={2} emoji>
|
||||
{text}
|
||||
</Text>
|
||||
)}
|
||||
<View style={[a.flex_row, a.gap_xs, a.align_center]}>
|
||||
<View style={[a.relative, a.rounded_full, {width: 20, height: 20}]}>
|
||||
<UserAvatar type="user" size={20} avatar={post.author.avatar} />
|
||||
<MediaInsetBorder />
|
||||
</View>
|
||||
<Text
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.text_sm,
|
||||
a.leading_tight,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}
|
||||
numberOfLines={1}>
|
||||
{sanitizeHandle(post.author.handle, '@')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
|
||||
return (
|
||||
<Link
|
||||
accessibilityHint={_(msg`Tap to view video in immersive mode.`)}
|
||||
label={_(msg`Video from ${author.handle}: ${text}`)}
|
||||
to={{
|
||||
screen: 'VideoFeed',
|
||||
params: {
|
||||
...sourceContext,
|
||||
initialPostUri: post.uri,
|
||||
},
|
||||
}}
|
||||
onPress={() => {
|
||||
onInteract?.()
|
||||
}}
|
||||
onPressIn={onPressIn}
|
||||
onPressOut={onPressOut}
|
||||
style={[
|
||||
a.flex_col,
|
||||
{
|
||||
alignItems: undefined,
|
||||
justifyContent: undefined,
|
||||
},
|
||||
]}>
|
||||
<Hider.Outer modui={mergedModui}>
|
||||
<Hider.Mask>
|
||||
<View
|
||||
style={[
|
||||
a.justify_center,
|
||||
a.rounded_md,
|
||||
a.overflow_hidden,
|
||||
{
|
||||
backgroundColor: black,
|
||||
aspectRatio: 9 / 16,
|
||||
},
|
||||
]}>
|
||||
<Image
|
||||
source={{uri: thumbnail}}
|
||||
style={[a.w_full, a.h_full, {opacity: pressed ? 0.8 : 1}]}
|
||||
accessibilityIgnoresInvertColors
|
||||
blurRadius={100}
|
||||
/>
|
||||
<MediaInsetBorder />
|
||||
<View
|
||||
style={[a.absolute, a.inset_0, a.justify_center, a.align_center]}>
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
{
|
||||
backgroundColor: 'black',
|
||||
opacity: 0.2,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<View style={[a.align_center, a.gap_xs]}>
|
||||
<Eye size="lg" fill="white" />
|
||||
<Text style={[a.text_sm, {color: 'white'}]}>
|
||||
{_(msg`Hidden`)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
{listModUi.blur ? (
|
||||
<VideoPostCardTextPlaceholder author={post.author} />
|
||||
) : (
|
||||
textAndAuthor
|
||||
)}
|
||||
</Hider.Mask>
|
||||
<Hider.Content>
|
||||
<View
|
||||
style={[
|
||||
a.justify_center,
|
||||
a.rounded_md,
|
||||
a.overflow_hidden,
|
||||
{
|
||||
backgroundColor: black,
|
||||
aspectRatio: 9 / 16,
|
||||
},
|
||||
]}>
|
||||
<Image
|
||||
source={{uri: thumbnail}}
|
||||
style={[a.w_full, a.h_full, {opacity: pressed ? 0.8 : 1}]}
|
||||
accessibilityIgnoresInvertColors
|
||||
/>
|
||||
<MediaInsetBorder />
|
||||
|
||||
<View style={[a.absolute, a.inset_0]}>
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
a.pt_2xl,
|
||||
{
|
||||
top: 'auto',
|
||||
},
|
||||
]}>
|
||||
<LinearGradient
|
||||
colors={[black, 'rgba(0, 0, 0, 0)']}
|
||||
locations={[0.02, 1]}
|
||||
start={{x: 0, y: 1}}
|
||||
end={{x: 0, y: 0}}
|
||||
style={[a.absolute, a.inset_0, {opacity: 0.9}]}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={[a.relative, a.z_10, a.p_md, a.flex_row, a.gap_md]}>
|
||||
{likeCount > 0 && (
|
||||
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
|
||||
<Heart size="sm" fill="white" />
|
||||
<Text style={[a.text_sm, a.font_bold, {color: 'white'}]}>
|
||||
{formatCount(i18n, likeCount)}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{repostCount > 0 && (
|
||||
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
|
||||
<Repost size="sm" fill="white" />
|
||||
<Text style={[a.text_sm, a.font_bold, {color: 'white'}]}>
|
||||
{formatCount(i18n, repostCount)}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
{textAndAuthor}
|
||||
</Hider.Content>
|
||||
</Hider.Outer>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export function VideoPostCardPlaceholder() {
|
||||
const t = useTheme()
|
||||
const black = getBlackColor(t)
|
||||
|
||||
return (
|
||||
<View style={[a.flex_1]}>
|
||||
<View
|
||||
style={[
|
||||
a.rounded_md,
|
||||
a.overflow_hidden,
|
||||
{
|
||||
backgroundColor: black,
|
||||
aspectRatio: 9 / 16,
|
||||
},
|
||||
]}>
|
||||
<MediaInsetBorder />
|
||||
</View>
|
||||
<VideoPostCardTextPlaceholder />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function VideoPostCardTextPlaceholder({
|
||||
author,
|
||||
}: {
|
||||
author?: AppBskyActorDefs.ProfileViewBasic
|
||||
}) {
|
||||
const t = useTheme()
|
||||
|
||||
return (
|
||||
<View style={[a.flex_1]}>
|
||||
<View style={[a.pr_xs, {paddingTop: 8, gap: 6}]}>
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.rounded_xs,
|
||||
t.atoms.bg_contrast_50,
|
||||
{
|
||||
height: 14,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.rounded_xs,
|
||||
t.atoms.bg_contrast_50,
|
||||
{
|
||||
height: 14,
|
||||
width: '70%',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{author ? (
|
||||
<View style={[a.flex_row, a.gap_xs, a.align_center]}>
|
||||
<View style={[a.relative, a.rounded_full, {width: 20, height: 20}]}>
|
||||
<UserAvatar type="user" size={20} avatar={author.avatar} />
|
||||
<MediaInsetBorder />
|
||||
</View>
|
||||
<Text
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.text_sm,
|
||||
a.leading_tight,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}
|
||||
numberOfLines={1}>
|
||||
{sanitizeHandle(author.handle, '@')}
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View style={[a.flex_row, a.gap_xs, a.align_center]}>
|
||||
<View
|
||||
style={[
|
||||
a.rounded_full,
|
||||
t.atoms.bg_contrast_50,
|
||||
{
|
||||
width: 20,
|
||||
height: 20,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.rounded_xs,
|
||||
t.atoms.bg_contrast_25,
|
||||
{
|
||||
height: 12,
|
||||
width: '75%',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function CompactVideoPostCard({
|
||||
post,
|
||||
sourceContext,
|
||||
moderation,
|
||||
onInteract,
|
||||
}: {
|
||||
post: AppBskyFeedDefs.PostView
|
||||
sourceContext: VideoFeedSourceContext
|
||||
moderation: ModerationDecision
|
||||
/**
|
||||
* Callback for metrics etc
|
||||
*/
|
||||
onInteract?: () => void
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_, i18n} = useLingui()
|
||||
const embed = post.embed
|
||||
const {
|
||||
state: pressed,
|
||||
onIn: onPressIn,
|
||||
onOut: onPressOut,
|
||||
} = useInteractionState()
|
||||
|
||||
const mergedModui = useMemo(() => {
|
||||
const modui = moderation.ui('contentList')
|
||||
const mediaModui = moderation.ui('contentMedia')
|
||||
modui.alerts = [...modui.alerts, ...mediaModui.alerts]
|
||||
modui.blurs = [...modui.blurs, ...mediaModui.blurs]
|
||||
modui.filters = [...modui.filters, ...mediaModui.filters]
|
||||
modui.informs = [...modui.informs, ...mediaModui.informs]
|
||||
return modui
|
||||
}, [moderation])
|
||||
|
||||
/**
|
||||
* Filtering should be done at a higher level, such as `PostFeed` or
|
||||
* `PostFeedVideoGridRow`, but we need to protect here as well.
|
||||
*/
|
||||
if (!AppBskyEmbedVideo.isView(embed)) return null
|
||||
|
||||
const likeCount = post?.likeCount ?? 0
|
||||
const {thumbnail} = embed
|
||||
const black = getBlackColor(t)
|
||||
|
||||
return (
|
||||
<Link
|
||||
label={_(msg`View video`)}
|
||||
to={{
|
||||
screen: 'VideoFeed',
|
||||
params: {
|
||||
...sourceContext,
|
||||
initialPostUri: post.uri,
|
||||
},
|
||||
}}
|
||||
onPress={() => {
|
||||
onInteract?.()
|
||||
}}
|
||||
onPressIn={onPressIn}
|
||||
onPressOut={onPressOut}
|
||||
style={[
|
||||
a.flex_col,
|
||||
{
|
||||
alignItems: undefined,
|
||||
justifyContent: undefined,
|
||||
},
|
||||
]}>
|
||||
<Hider.Outer modui={mergedModui}>
|
||||
<Hider.Mask>
|
||||
<View
|
||||
style={[
|
||||
a.justify_center,
|
||||
a.rounded_md,
|
||||
a.overflow_hidden,
|
||||
{
|
||||
backgroundColor: black,
|
||||
aspectRatio: 9 / 16,
|
||||
},
|
||||
]}>
|
||||
<Image
|
||||
source={{uri: thumbnail}}
|
||||
style={[a.w_full, a.h_full, {opacity: pressed ? 0.8 : 1}]}
|
||||
accessibilityIgnoresInvertColors
|
||||
blurRadius={100}
|
||||
/>
|
||||
<MediaInsetBorder />
|
||||
<View
|
||||
style={[a.absolute, a.inset_0, a.justify_center, a.align_center]}>
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
{
|
||||
backgroundColor: 'black',
|
||||
opacity: 0.2,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<View style={[a.align_center, a.gap_xs]}>
|
||||
<Eye size="lg" fill="white" />
|
||||
<Text style={[a.text_sm, {color: 'white'}]}>
|
||||
{_(msg`Hidden`)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Hider.Mask>
|
||||
<Hider.Content>
|
||||
<View
|
||||
style={[
|
||||
a.justify_center,
|
||||
a.rounded_md,
|
||||
a.overflow_hidden,
|
||||
{
|
||||
backgroundColor: black,
|
||||
aspectRatio: 9 / 16,
|
||||
},
|
||||
]}>
|
||||
<Image
|
||||
source={{uri: thumbnail}}
|
||||
style={[a.w_full, a.h_full, {opacity: pressed ? 0.8 : 1}]}
|
||||
accessibilityIgnoresInvertColors
|
||||
/>
|
||||
<MediaInsetBorder />
|
||||
|
||||
<View style={[a.absolute, a.inset_0]}>
|
||||
<View style={[a.absolute, a.inset_0, a.p_sm, {bottom: 'auto'}]}>
|
||||
<View
|
||||
style={[a.relative, a.rounded_full, {width: 20, height: 20}]}>
|
||||
<UserAvatar
|
||||
type="user"
|
||||
size={20}
|
||||
avatar={post.author.avatar}
|
||||
/>
|
||||
<MediaInsetBorder />
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
a.pt_2xl,
|
||||
{
|
||||
top: 'auto',
|
||||
},
|
||||
]}>
|
||||
<LinearGradient
|
||||
colors={[black, 'rgba(0, 0, 0, 0)']}
|
||||
locations={[0.02, 1]}
|
||||
start={{x: 0, y: 1}}
|
||||
end={{x: 0, y: 0}}
|
||||
style={[a.absolute, a.inset_0, {opacity: 0.9}]}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={[a.relative, a.z_10, a.p_sm, a.flex_row, a.gap_md]}>
|
||||
{likeCount > 0 && (
|
||||
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
|
||||
<Heart size="sm" fill="white" />
|
||||
<Text style={[a.text_sm, a.font_bold, {color: 'white'}]}>
|
||||
{formatCount(i18n, likeCount)}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Hider.Content>
|
||||
</Hider.Outer>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export function CompactVideoPostCardPlaceholder() {
|
||||
const t = useTheme()
|
||||
const black = getBlackColor(t)
|
||||
|
||||
return (
|
||||
<View style={[a.flex_1]}>
|
||||
<View
|
||||
style={[
|
||||
a.rounded_md,
|
||||
a.overflow_hidden,
|
||||
{
|
||||
backgroundColor: black,
|
||||
aspectRatio: 9 / 16,
|
||||
},
|
||||
]}>
|
||||
<MediaInsetBorder />
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -12,12 +12,12 @@ import {
|
||||
usePreferencesSetBirthDateMutation,
|
||||
} from '#/state/queries/preferences'
|
||||
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
|
||||
import {DateInput} from '#/view/com/util/forms/DateInput'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {DateField} from '#/components/forms/DateField'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {Button, ButtonIcon, ButtonText} from '../Button'
|
||||
import {Text} from '../Typography'
|
||||
|
||||
export function BirthDateSettingsDialog({
|
||||
control,
|
||||
@@ -95,17 +95,13 @@ function BirthdayInner({
|
||||
return (
|
||||
<View style={a.gap_lg} testID="birthDateSettingsDialog">
|
||||
<View style={isIOS && [a.w_full, a.align_center]}>
|
||||
<DateInput
|
||||
handleAsUTC
|
||||
<DateField
|
||||
testID="birthdayInput"
|
||||
value={date}
|
||||
onChange={setDate}
|
||||
buttonType="default-light"
|
||||
buttonStyle={[a.rounded_sm]}
|
||||
buttonLabelType="lg"
|
||||
accessibilityLabel={_(msg`Birthday`)}
|
||||
onChangeDate={newDate => setDate(new Date(newDate))}
|
||||
label={_(msg`Birthday`)}
|
||||
accessibilityHint={_(msg`Enter your birth date`)}
|
||||
accessibilityLabelledBy="birthDate"
|
||||
maximumDate={new Date()}
|
||||
/>
|
||||
</View>
|
||||
|
||||
|
||||
+171
-125
@@ -32,15 +32,17 @@ import * as Menu from '#/components/Menu'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import * as atp from '#/types/atproto'
|
||||
import {Bubble_Stroke2_Corner2_Rounded as Bubble} from '../icons/Bubble'
|
||||
import {ReportDialog} from './ReportDialog'
|
||||
|
||||
let ConvoMenu = ({
|
||||
convo: initialConvo,
|
||||
convo,
|
||||
profile,
|
||||
control,
|
||||
currentScreen,
|
||||
showMarkAsRead,
|
||||
hideTrigger,
|
||||
blockInfo,
|
||||
latestReportableMessage,
|
||||
style,
|
||||
}: {
|
||||
convo: ChatBskyConvoDefs.ConvoView
|
||||
@@ -53,27 +55,122 @@ let ConvoMenu = ({
|
||||
listBlocks: ModerationCause[]
|
||||
userBlock?: ModerationCause
|
||||
}
|
||||
latestReportableMessage?: ChatBskyConvoDefs.MessageView
|
||||
style?: ViewStyleProp['style']
|
||||
}): React.ReactNode => {
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
|
||||
const leaveConvoControl = Prompt.usePromptControl()
|
||||
const reportControl = Prompt.usePromptControl()
|
||||
const blockedByListControl = Prompt.usePromptControl()
|
||||
|
||||
const {listBlocks} = blockInfo
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu.Root control={control}>
|
||||
{!hideTrigger && (
|
||||
<View style={[style]}>
|
||||
<Menu.Trigger label={_(msg`Chat settings`)}>
|
||||
{({props, state}) => (
|
||||
<Pressable
|
||||
{...props}
|
||||
onPress={() => {
|
||||
Keyboard.dismiss()
|
||||
props.onPress()
|
||||
}}
|
||||
style={[
|
||||
a.p_sm,
|
||||
a.rounded_full,
|
||||
(state.hovered || state.pressed) && t.atoms.bg_contrast_25,
|
||||
// make sure pfp is in the middle
|
||||
{marginLeft: -10},
|
||||
]}>
|
||||
<DotsHorizontal size="md" style={t.atoms.text} />
|
||||
</Pressable>
|
||||
)}
|
||||
</Menu.Trigger>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<Menu.Outer>
|
||||
<MenuContent
|
||||
profile={profile}
|
||||
showMarkAsRead={showMarkAsRead}
|
||||
blockInfo={blockInfo}
|
||||
convo={convo}
|
||||
leaveConvoControl={leaveConvoControl}
|
||||
reportControl={reportControl}
|
||||
blockedByListControl={blockedByListControl}
|
||||
/>
|
||||
</Menu.Outer>
|
||||
</Menu.Root>
|
||||
|
||||
<LeaveConvoPrompt
|
||||
control={leaveConvoControl}
|
||||
convoId={convo.id}
|
||||
currentScreen={currentScreen}
|
||||
/>
|
||||
{latestReportableMessage ? (
|
||||
<ReportDialog
|
||||
currentScreen={currentScreen}
|
||||
params={{
|
||||
type: 'convoMessage',
|
||||
convoId: convo.id,
|
||||
message: latestReportableMessage,
|
||||
}}
|
||||
control={reportControl}
|
||||
/>
|
||||
) : (
|
||||
<ReportConversationPrompt control={reportControl} />
|
||||
)}
|
||||
|
||||
<BlockedByListDialog
|
||||
control={blockedByListControl}
|
||||
listBlocks={listBlocks}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
ConvoMenu = React.memo(ConvoMenu)
|
||||
|
||||
function MenuContent({
|
||||
convo: initialConvo,
|
||||
profile,
|
||||
showMarkAsRead,
|
||||
blockInfo,
|
||||
leaveConvoControl,
|
||||
reportControl,
|
||||
blockedByListControl,
|
||||
}: {
|
||||
convo: ChatBskyConvoDefs.ConvoView
|
||||
profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
|
||||
showMarkAsRead?: boolean
|
||||
blockInfo: {
|
||||
listBlocks: ModerationCause[]
|
||||
userBlock?: ModerationCause
|
||||
}
|
||||
leaveConvoControl: Prompt.PromptControlProps
|
||||
reportControl: Prompt.PromptControlProps
|
||||
blockedByListControl: Prompt.PromptControlProps
|
||||
}) {
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {_} = useLingui()
|
||||
const {mutate: markAsRead} = useMarkAsReadMutation()
|
||||
|
||||
const {listBlocks, userBlock} = blockInfo
|
||||
const isBlocking = userBlock || !!listBlocks.length
|
||||
const isDeletedAccount = profile.handle === 'missing.invalid'
|
||||
|
||||
const convoId = initialConvo.id
|
||||
const {data: convo} = useConvoQuery(initialConvo)
|
||||
|
||||
const onNavigateToProfile = useCallback(() => {
|
||||
navigation.navigate('Profile', {name: profile.did})
|
||||
}, [navigation, profile.did])
|
||||
|
||||
const {mutate: muteConvo} = useMuteConvo(convo?.id, {
|
||||
const {mutate: muteConvo} = useMuteConvo(convoId, {
|
||||
onSuccess: data => {
|
||||
if (data.convo.muted) {
|
||||
Toast.show(_(msg`Chat muted`))
|
||||
@@ -101,132 +198,81 @@ let ConvoMenu = ({
|
||||
}
|
||||
}, [userBlock, listBlocks, blockedByListControl, queueBlock, queueUnblock])
|
||||
|
||||
return (
|
||||
return isDeletedAccount ? (
|
||||
<Menu.Item
|
||||
label={_(msg`Leave conversation`)}
|
||||
onPress={() => leaveConvoControl.open()}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Leave conversation</Trans>
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={ArrowBoxLeft} />
|
||||
</Menu.Item>
|
||||
) : (
|
||||
<>
|
||||
<Menu.Root control={control}>
|
||||
{!hideTrigger && (
|
||||
<View style={[style]}>
|
||||
<Menu.Trigger label={_(msg`Chat settings`)}>
|
||||
{({props, state}) => (
|
||||
<Pressable
|
||||
{...props}
|
||||
onPress={() => {
|
||||
Keyboard.dismiss()
|
||||
|
||||
props.onPress()
|
||||
}}
|
||||
style={[
|
||||
a.p_sm,
|
||||
a.rounded_full,
|
||||
(state.hovered || state.pressed) && t.atoms.bg_contrast_25,
|
||||
// make sure pfp is in the middle
|
||||
{marginLeft: -10},
|
||||
]}>
|
||||
<DotsHorizontal size="md" style={t.atoms.text} />
|
||||
</Pressable>
|
||||
)}
|
||||
</Menu.Trigger>
|
||||
</View>
|
||||
<Menu.Group>
|
||||
{showMarkAsRead && (
|
||||
<Menu.Item
|
||||
label={_(msg`Mark as read`)}
|
||||
onPress={() => markAsRead({convoId})}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Mark as read</Trans>
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Bubble} />
|
||||
</Menu.Item>
|
||||
)}
|
||||
|
||||
{isDeletedAccount ? (
|
||||
<Menu.Outer>
|
||||
<Menu.Item
|
||||
label={_(msg`Leave conversation`)}
|
||||
onPress={() => leaveConvoControl.open()}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Leave conversation</Trans>
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={ArrowBoxLeft} />
|
||||
</Menu.Item>
|
||||
</Menu.Outer>
|
||||
) : (
|
||||
<Menu.Outer>
|
||||
<Menu.Group>
|
||||
{showMarkAsRead && (
|
||||
<Menu.Item
|
||||
label={_(msg`Mark as read`)}
|
||||
onPress={() =>
|
||||
markAsRead({
|
||||
convoId: convo?.id,
|
||||
})
|
||||
}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Mark as read</Trans>
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Bubble} />
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Item
|
||||
label={_(msg`Go to user's profile`)}
|
||||
onPress={onNavigateToProfile}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Go to profile</Trans>
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Person} />
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
label={_(msg`Mute conversation`)}
|
||||
onPress={() => muteConvo({mute: !convo?.muted})}>
|
||||
<Menu.ItemText>
|
||||
{convo?.muted ? (
|
||||
<Trans>Unmute conversation</Trans>
|
||||
) : (
|
||||
<Trans>Mute conversation</Trans>
|
||||
)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={convo?.muted ? Unmute : Mute} />
|
||||
</Menu.Item>
|
||||
</Menu.Group>
|
||||
<Menu.Divider />
|
||||
<Menu.Group>
|
||||
<Menu.Item
|
||||
label={
|
||||
isBlocking ? _(msg`Unblock account`) : _(msg`Block account`)
|
||||
}
|
||||
onPress={toggleBlock}>
|
||||
<Menu.ItemText>
|
||||
{isBlocking ? _(msg`Unblock account`) : _(msg`Block account`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={isBlocking ? PersonCheck : PersonX} />
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
label={_(msg`Report conversation`)}
|
||||
onPress={() => reportControl.open()}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Report conversation</Trans>
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Flag} />
|
||||
</Menu.Item>
|
||||
</Menu.Group>
|
||||
<Menu.Divider />
|
||||
<Menu.Group>
|
||||
<Menu.Item
|
||||
label={_(msg`Leave conversation`)}
|
||||
onPress={() => leaveConvoControl.open()}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Leave conversation</Trans>
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={ArrowBoxLeft} />
|
||||
</Menu.Item>
|
||||
</Menu.Group>
|
||||
</Menu.Outer>
|
||||
)}
|
||||
</Menu.Root>
|
||||
|
||||
<LeaveConvoPrompt
|
||||
control={leaveConvoControl}
|
||||
convoId={convo.id}
|
||||
currentScreen={currentScreen}
|
||||
/>
|
||||
<ReportConversationPrompt control={reportControl} />
|
||||
<BlockedByListDialog
|
||||
control={blockedByListControl}
|
||||
listBlocks={listBlocks}
|
||||
/>
|
||||
<Menu.Item
|
||||
label={_(msg`Go to user's profile`)}
|
||||
onPress={onNavigateToProfile}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Go to profile</Trans>
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Person} />
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
label={_(msg`Mute conversation`)}
|
||||
onPress={() => muteConvo({mute: !convo?.muted})}>
|
||||
<Menu.ItemText>
|
||||
{convo?.muted ? (
|
||||
<Trans>Unmute conversation</Trans>
|
||||
) : (
|
||||
<Trans>Mute conversation</Trans>
|
||||
)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={convo?.muted ? Unmute : Mute} />
|
||||
</Menu.Item>
|
||||
</Menu.Group>
|
||||
<Menu.Divider />
|
||||
<Menu.Group>
|
||||
<Menu.Item
|
||||
label={isBlocking ? _(msg`Unblock account`) : _(msg`Block account`)}
|
||||
onPress={toggleBlock}>
|
||||
<Menu.ItemText>
|
||||
{isBlocking ? _(msg`Unblock account`) : _(msg`Block account`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={isBlocking ? PersonCheck : PersonX} />
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
label={_(msg`Report conversation`)}
|
||||
onPress={() => reportControl.open()}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Report conversation</Trans>
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Flag} />
|
||||
</Menu.Item>
|
||||
</Menu.Group>
|
||||
<Menu.Divider />
|
||||
<Menu.Group>
|
||||
<Menu.Item
|
||||
label={_(msg`Leave conversation`)}
|
||||
onPress={() => leaveConvoControl.open()}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Leave conversation</Trans>
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={ArrowBoxLeft} />
|
||||
</Menu.Item>
|
||||
</Menu.Group>
|
||||
</>
|
||||
)
|
||||
}
|
||||
ConvoMenu = React.memo(ConvoMenu)
|
||||
|
||||
export {ConvoMenu}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
import {StackActions, useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
import {isNative} from '#/platform/detection'
|
||||
@@ -22,15 +22,10 @@ export function LeaveConvoPrompt({
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
|
||||
const {mutate: leaveConvo} = useLeaveConvo(convoId, {
|
||||
onSuccess: () => {
|
||||
onMutate: () => {
|
||||
if (currentScreen === 'conversation') {
|
||||
navigation.replace(
|
||||
'Messages',
|
||||
isNative
|
||||
? {
|
||||
animation: 'pop',
|
||||
}
|
||||
: {},
|
||||
navigation.dispatch(
|
||||
StackActions.replace('Messages', isNative ? {animation: 'pop'} : {}),
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -138,6 +138,7 @@ export let MessageMenu = ({
|
||||
</Menu.Root>
|
||||
|
||||
<ReportDialog
|
||||
currentScreen="conversation"
|
||||
params={{type: 'convoMessage', convoId: convo.convo.id, message}}
|
||||
control={reportControl}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {ModerationCause} from '@atproto/api'
|
||||
import {ModerationDecision} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
@@ -20,15 +20,12 @@ export function MessagesListBlockedFooter({
|
||||
recipient: initialRecipient,
|
||||
convoId,
|
||||
hasMessages,
|
||||
blockInfo,
|
||||
moderation,
|
||||
}: {
|
||||
recipient: atp.profile.AnyProfileView
|
||||
convoId: string
|
||||
hasMessages: boolean
|
||||
blockInfo: {
|
||||
listBlocks: ModerationCause[]
|
||||
userBlock: ModerationCause | undefined
|
||||
}
|
||||
moderation: ModerationDecision
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
@@ -40,7 +37,17 @@ export function MessagesListBlockedFooter({
|
||||
const reportControl = useDialogControl()
|
||||
const blockedByListControl = useDialogControl()
|
||||
|
||||
const {listBlocks, userBlock} = blockInfo
|
||||
const {listBlocks, userBlock} = React.useMemo(() => {
|
||||
const modui = moderation.ui('profileView')
|
||||
const blocks = modui.alerts.filter(alert => alert.type === 'blocking')
|
||||
const listBlocks = blocks.filter(alert => alert.source.type === 'list')
|
||||
const userBlock = blocks.find(alert => alert.source.type === 'user')
|
||||
return {
|
||||
listBlocks,
|
||||
userBlock,
|
||||
}
|
||||
}, [moderation])
|
||||
|
||||
const isBlocking = !!userBlock || !!listBlocks.length
|
||||
|
||||
const onUnblockPress = React.useCallback(() => {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import React, {useCallback} from 'react'
|
||||
import {TouchableOpacity, View} from 'react-native'
|
||||
import {ModerationCause, ModerationDecision} from '@atproto/api'
|
||||
import {
|
||||
AppBskyActorDefs,
|
||||
ModerationCause,
|
||||
ModerationDecision,
|
||||
} from '@atproto/api'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
@@ -11,7 +15,7 @@ import {makeProfileLink} from '#/lib/routes/links'
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {Shadow} from '#/state/cache/profile-shadow'
|
||||
import {isConvoActive, useConvo} from '#/state/messages/convo'
|
||||
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
||||
@@ -20,27 +24,33 @@ import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/
|
||||
import {Link} from '#/components/Link'
|
||||
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
||||
import {Text} from '#/components/Typography'
|
||||
import * as atp from '#/types/atproto'
|
||||
|
||||
const PFP_SIZE = isWeb ? 40 : 34
|
||||
|
||||
export let MessagesListHeader = ({
|
||||
profile,
|
||||
moderation,
|
||||
blockInfo,
|
||||
}: {
|
||||
profile?: atp.profile.AnyProfileView
|
||||
profile?: Shadow<AppBskyActorDefs.ProfileViewBasic>
|
||||
moderation?: ModerationDecision
|
||||
blockInfo?: {
|
||||
listBlocks: ModerationCause[]
|
||||
userBlock?: ModerationCause
|
||||
}
|
||||
}): React.ReactNode => {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {gtTablet} = useBreakpoints()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
|
||||
const blockInfo = React.useMemo(() => {
|
||||
if (!moderation) return
|
||||
const modui = moderation.ui('profileView')
|
||||
const blocks = modui.alerts.filter(alert => alert.type === 'blocking')
|
||||
const listBlocks = blocks.filter(alert => alert.source.type === 'list')
|
||||
const userBlock = blocks.find(alert => alert.source.type === 'user')
|
||||
return {
|
||||
listBlocks,
|
||||
userBlock,
|
||||
}
|
||||
}, [moderation])
|
||||
|
||||
const onPressBack = useCallback(() => {
|
||||
if (isWeb) {
|
||||
navigation.replace('Messages', {})
|
||||
@@ -124,11 +134,11 @@ export let MessagesListHeader = ({
|
||||
MessagesListHeader = React.memo(MessagesListHeader)
|
||||
|
||||
function HeaderReady({
|
||||
profile: profileUnshadowed,
|
||||
profile,
|
||||
moderation,
|
||||
blockInfo,
|
||||
}: {
|
||||
profile: atp.profile.AnyProfileView
|
||||
profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
|
||||
moderation: ModerationDecision
|
||||
blockInfo: {
|
||||
listBlocks: ModerationCause[]
|
||||
@@ -138,7 +148,6 @@ function HeaderReady({
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
const convoState = useConvo()
|
||||
const profile = useProfileShadow(profileUnshadowed)
|
||||
|
||||
const isDeletedAccount = profile?.handle === 'missing.invalid'
|
||||
const displayName = isDeletedAccount
|
||||
@@ -148,6 +157,15 @@ function HeaderReady({
|
||||
moderation.ui('displayName'),
|
||||
)
|
||||
|
||||
const latestMessageFromOther = convoState.items.findLast(
|
||||
item => item.type === 'message' && item.message.sender.did === profile.did,
|
||||
)
|
||||
|
||||
const latestReportableMessage =
|
||||
latestMessageFromOther?.type === 'message'
|
||||
? latestMessageFromOther.message
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<View style={[a.flex_1]}>
|
||||
<View style={[a.w_full, a.flex_row, a.align_center, a.justify_between]}>
|
||||
@@ -205,6 +223,7 @@ function HeaderReady({
|
||||
profile={profile}
|
||||
currentScreen="conversation"
|
||||
blockInfo={blockInfo}
|
||||
latestReportableMessage={latestReportableMessage}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -2,27 +2,39 @@ import React, {memo, useMemo, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {
|
||||
$Typed,
|
||||
AppBskyActorDefs,
|
||||
ChatBskyConvoDefs,
|
||||
ComAtprotoModerationCreateReport,
|
||||
RichText as RichTextAPI,
|
||||
} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {StackActions, useNavigation} from '@react-navigation/native'
|
||||
import {useMutation} from '@tanstack/react-query'
|
||||
|
||||
import {ReportOption} from '#/lib/moderation/useReportOptions'
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {useLeaveConvo} from '#/state/queries/messages/leave-conversation'
|
||||
import {
|
||||
useProfileBlockMutationQueue,
|
||||
useProfileQuery,
|
||||
} from '#/state/queries/profile'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {atoms as a, platform, useBreakpoints, useTheme, web} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {Button, ButtonIcon, ButtonText} from '../Button'
|
||||
import {Divider} from '../Divider'
|
||||
import {ChevronLeft_Stroke2_Corner0_Rounded as Chevron} from '../icons/Chevron'
|
||||
import {Loader} from '../Loader'
|
||||
import {SelectReportOptionView} from '../ReportDialog/SelectReportOptionView'
|
||||
import {RichText} from '../RichText'
|
||||
import {Text} from '../Typography'
|
||||
import {Divider} from '#/components/Divider'
|
||||
import * as Toggle from '#/components/forms/Toggle'
|
||||
import {ChevronLeft_Stroke2_Corner0_Rounded as Chevron} from '#/components/icons/Chevron'
|
||||
import {PaperPlane_Stroke2_Corner0_Rounded as SendIcon} from '#/components/icons/PaperPlane'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {SelectReportOptionView} from '#/components/ReportDialog/SelectReportOptionView'
|
||||
import {RichText} from '#/components/RichText'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {MessageItemMetadata} from './MessageItem'
|
||||
|
||||
type ReportDialogParams = {
|
||||
@@ -34,16 +46,18 @@ type ReportDialogParams = {
|
||||
let ReportDialog = ({
|
||||
control,
|
||||
params,
|
||||
currentScreen,
|
||||
}: {
|
||||
control: Dialog.DialogControlProps
|
||||
params: ReportDialogParams
|
||||
currentScreen: 'list' | 'conversation'
|
||||
}): React.ReactNode => {
|
||||
const {_} = useLingui()
|
||||
return (
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Handle />
|
||||
<Dialog.ScrollableInner label={_(msg`Report this message`)}>
|
||||
<DialogInner params={params} />
|
||||
<DialogInner params={params} currentScreen={currentScreen} />
|
||||
<Dialog.Close />
|
||||
</Dialog.ScrollableInner>
|
||||
</Dialog.Outer>
|
||||
@@ -52,14 +66,44 @@ let ReportDialog = ({
|
||||
ReportDialog = memo(ReportDialog)
|
||||
export {ReportDialog}
|
||||
|
||||
function DialogInner({params}: {params: ReportDialogParams}) {
|
||||
function DialogInner({
|
||||
params,
|
||||
currentScreen,
|
||||
}: {
|
||||
params: ReportDialogParams
|
||||
currentScreen: 'list' | 'conversation'
|
||||
}) {
|
||||
const {data: profile, isError} = useProfileQuery({
|
||||
did: params.message.sender.did,
|
||||
})
|
||||
const [reportOption, setReportOption] = useState<ReportOption | null>(null)
|
||||
const [done, setDone] = useState(false)
|
||||
const control = Dialog.useDialogContext()
|
||||
|
||||
return reportOption ? (
|
||||
return done ? (
|
||||
profile ? (
|
||||
<DoneStep
|
||||
convoId={params.convoId}
|
||||
currentScreen={currentScreen}
|
||||
profile={profile}
|
||||
/>
|
||||
) : (
|
||||
<View style={[a.w_full, a.py_5xl, a.align_center]}>
|
||||
<Loader />
|
||||
</View>
|
||||
)
|
||||
) : reportOption ? (
|
||||
<SubmitStep
|
||||
params={params}
|
||||
reportOption={reportOption}
|
||||
goBack={() => setReportOption(null)}
|
||||
onComplete={() => {
|
||||
if (isError) {
|
||||
control.close()
|
||||
} else {
|
||||
setDone(true)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ReasonStep params={params} setReportOption={setReportOption} />
|
||||
@@ -90,16 +134,17 @@ function SubmitStep({
|
||||
params,
|
||||
reportOption,
|
||||
goBack,
|
||||
onComplete,
|
||||
}: {
|
||||
params: ReportDialogParams
|
||||
reportOption: ReportOption
|
||||
goBack: () => void
|
||||
onComplete: () => void
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const t = useTheme()
|
||||
const [details, setDetails] = useState('')
|
||||
const control = Dialog.useDialogContext()
|
||||
const agent = useAgent()
|
||||
|
||||
const {
|
||||
@@ -126,11 +171,7 @@ function SubmitStep({
|
||||
await agent.createModerationReport(report)
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
control.close(() => {
|
||||
Toast.show(_(msg`Thank you. Your report has been sent.`))
|
||||
})
|
||||
},
|
||||
onSuccess: onComplete,
|
||||
})
|
||||
|
||||
const copy = useMemo(() => {
|
||||
@@ -183,13 +224,12 @@ function SubmitStep({
|
||||
<View style={[a.relative, a.w_full]}>
|
||||
<Dialog.Input
|
||||
multiline
|
||||
value={details}
|
||||
defaultValue={details}
|
||||
onChangeText={setDetails}
|
||||
label="Text field"
|
||||
label={_(msg`Text field`)}
|
||||
style={{paddingRight: 60}}
|
||||
numberOfLines={6}
|
||||
numberOfLines={5}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
@@ -233,7 +273,115 @@ function SubmitStep({
|
||||
<ButtonText>
|
||||
<Trans>Send report</Trans>
|
||||
</ButtonText>
|
||||
{submitting && <ButtonIcon icon={Loader} />}
|
||||
<ButtonIcon icon={submitting ? Loader : SendIcon} />
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function DoneStep({
|
||||
convoId,
|
||||
currentScreen,
|
||||
profile,
|
||||
}: {
|
||||
convoId: string
|
||||
currentScreen: 'list' | 'conversation'
|
||||
profile: AppBskyActorDefs.ProfileViewBasic
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const control = Dialog.useDialogContext()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const t = useTheme()
|
||||
const [actions, setActions] = useState<string[]>(['block', 'leave'])
|
||||
const shadow = useProfileShadow(profile)
|
||||
const [queueBlock] = useProfileBlockMutationQueue(shadow)
|
||||
|
||||
const {mutate: leaveConvo} = useLeaveConvo(convoId, {
|
||||
onMutate: () => {
|
||||
if (currentScreen === 'conversation') {
|
||||
navigation.dispatch(
|
||||
StackActions.replace('Messages', isNative ? {animation: 'pop'} : {}),
|
||||
)
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
Toast.show(_(msg`Could not leave chat`), 'xmark')
|
||||
},
|
||||
})
|
||||
|
||||
const onPressPrimaryAction = () => {
|
||||
control.close(() => {
|
||||
if (actions.includes('block')) {
|
||||
queueBlock()
|
||||
}
|
||||
if (actions.includes('leave')) {
|
||||
leaveConvo()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let btnText = _(msg`Done`)
|
||||
if (actions.includes('leave') && actions.includes('block')) {
|
||||
btnText = _(msg`Block and Delete`)
|
||||
} else if (actions.includes('leave')) {
|
||||
btnText = _(msg`Delete Conversation`)
|
||||
} else if (actions.includes('block')) {
|
||||
btnText = _(msg`Block User`)
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={a.gap_2xl}>
|
||||
<View style={[a.justify_center, gtMobile ? a.gap_sm : a.gap_xs]}>
|
||||
<Text style={[a.text_2xl, a.font_bold]}>
|
||||
<Trans>Report submitted</Trans>
|
||||
</Text>
|
||||
<Text style={[a.text_md, t.atoms.text_contrast_medium]}>
|
||||
<Trans>Our moderation team has received your report.</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
<Toggle.Group
|
||||
label={_(msg`Block and/or delete this conversation`)}
|
||||
values={actions}
|
||||
onChange={setActions}>
|
||||
<View style={[a.gap_md]}>
|
||||
<Toggle.Item name="block" label={_(msg`Block user`)}>
|
||||
<Toggle.Checkbox />
|
||||
<Toggle.LabelText style={[a.text_md]}>
|
||||
<Trans>Block user</Trans>
|
||||
</Toggle.LabelText>
|
||||
</Toggle.Item>
|
||||
<Toggle.Item name="leave" label={_(msg`Delete conversation`)}>
|
||||
<Toggle.Checkbox />
|
||||
<Toggle.LabelText style={[a.text_md]}>
|
||||
<Trans>Delete conversation</Trans>
|
||||
</Toggle.LabelText>
|
||||
</Toggle.Item>
|
||||
</View>
|
||||
</Toggle.Group>
|
||||
|
||||
<View style={[a.gap_md, web([a.flex_row_reverse])]}>
|
||||
<Button
|
||||
label={btnText}
|
||||
onPress={onPressPrimaryAction}
|
||||
size="large"
|
||||
variant="solid"
|
||||
color={actions.length > 0 ? 'negative' : 'primary'}>
|
||||
<ButtonText>{btnText}</ButtonText>
|
||||
</Button>
|
||||
<Button
|
||||
label={_(msg`Close`)}
|
||||
onPress={() => control.close()}
|
||||
size={platform({native: 'small', web: 'large'})}
|
||||
variant={platform({
|
||||
native: 'ghost',
|
||||
web: 'solid',
|
||||
})}
|
||||
color="secondary">
|
||||
<ButtonText>
|
||||
<Trans>Close</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import {View} from 'react-native'
|
||||
import {AppBskyEmbedVideo} from '@atproto/api'
|
||||
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {FeedPostSliceItem} from '#/state/queries/post-feed'
|
||||
import {VideoFeedSourceContext} from '#/screens/VideoFeed/types'
|
||||
import {atoms as a, useGutters} from '#/alf'
|
||||
import * as Grid from '#/components/Grid'
|
||||
import {
|
||||
VideoPostCard,
|
||||
VideoPostCardPlaceholder,
|
||||
} from '#/components/VideoPostCard'
|
||||
|
||||
export function PostFeedVideoGridRow({
|
||||
items: slices,
|
||||
sourceContext,
|
||||
}: {
|
||||
items: FeedPostSliceItem[]
|
||||
sourceContext: VideoFeedSourceContext
|
||||
}) {
|
||||
const gutters = useGutters(['base', 'base', 0, 'base'])
|
||||
const posts = slices
|
||||
.filter(slice => AppBskyEmbedVideo.isView(slice.post.embed))
|
||||
.map(slice => ({
|
||||
post: slice.post,
|
||||
moderation: slice.moderation,
|
||||
}))
|
||||
|
||||
/**
|
||||
* This should not happen because we should be filtering out posts without
|
||||
* videos within the `PostFeed` component.
|
||||
*/
|
||||
if (posts.length !== slices.length) return null
|
||||
|
||||
return (
|
||||
<View style={[gutters]}>
|
||||
<View style={[a.flex_row, a.gap_sm]}>
|
||||
<Grid.Row gap={a.gap_sm.gap}>
|
||||
{posts.map(post => (
|
||||
<Grid.Col key={post.post.uri} width={1 / 2}>
|
||||
<VideoPostCard
|
||||
post={post.post}
|
||||
sourceContext={sourceContext}
|
||||
moderation={post.moderation}
|
||||
onInteract={() => {
|
||||
logEvent('videoCard:click', {context: 'feed'})
|
||||
}}
|
||||
/>
|
||||
</Grid.Col>
|
||||
))}
|
||||
</Grid.Row>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function PostFeedVideoGridRowPlaceholder() {
|
||||
const gutters = useGutters(['base', 'base', 0, 'base'])
|
||||
return (
|
||||
<View style={[gutters]}>
|
||||
<View style={[a.flex_row, a.gap_sm]}>
|
||||
<VideoPostCardPlaceholder />
|
||||
<VideoPostCardPlaceholder />
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -17,6 +17,7 @@ export function DateField({
|
||||
isInvalid,
|
||||
testID,
|
||||
accessibilityHint,
|
||||
maximumDate,
|
||||
}: DateFieldProps) {
|
||||
const t = useTheme()
|
||||
const [open, setOpen] = React.useState(false)
|
||||
@@ -67,6 +68,9 @@ export function DateField({
|
||||
aria-label={label}
|
||||
accessibilityLabel={label}
|
||||
accessibilityHint={accessibilityHint}
|
||||
maximumDate={
|
||||
maximumDate ? new Date(toSimpleDateString(maximumDate)) : undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -19,7 +19,7 @@ export function DateFieldButton({
|
||||
accessibilityHint,
|
||||
}: {
|
||||
label: string
|
||||
value: string
|
||||
value: string | Date
|
||||
onPress: () => void
|
||||
isInvalid?: boolean
|
||||
accessibilityHint?: string
|
||||
|
||||
@@ -16,10 +16,11 @@ export * as utils from '#/components/forms/DateField/utils'
|
||||
export const LabelText = TextField.LabelText
|
||||
|
||||
/**
|
||||
* Date-only input. Accepts a date in the format YYYY-MM-DD, and reports date
|
||||
* changes in the same format.
|
||||
* Date-only input. Accepts a string in the format YYYY-MM-DD, or a Date object.
|
||||
* Date objects are converted to strings in the format YYYY-MM-DD.
|
||||
* Returns a string in the format YYYY-MM-DD.
|
||||
*
|
||||
* For dates of unknown format, convert with the
|
||||
* To generate a string in the format YYYY-MM-DD from a Date object, use the
|
||||
* `utils.toSimpleDateString(Date)` export of this file.
|
||||
*/
|
||||
export function DateField({
|
||||
@@ -29,6 +30,7 @@ export function DateField({
|
||||
label,
|
||||
isInvalid,
|
||||
accessibilityHint,
|
||||
maximumDate,
|
||||
}: DateFieldProps) {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
@@ -56,21 +58,29 @@ export function DateField({
|
||||
isInvalid={isInvalid}
|
||||
accessibilityHint={accessibilityHint}
|
||||
/>
|
||||
<Dialog.Outer control={control} testID={testID}>
|
||||
<Dialog.Outer
|
||||
control={control}
|
||||
testID={testID}
|
||||
nativeOptions={{preventExpansion: true}}>
|
||||
<Dialog.Handle />
|
||||
<Dialog.Inner label={label}>
|
||||
<Dialog.ScrollableInner label={label}>
|
||||
<View style={a.gap_lg}>
|
||||
<View style={[a.relative, a.w_full, a.align_center]}>
|
||||
<DatePicker
|
||||
timeZoneOffsetInMinutes={0}
|
||||
theme={t.name === 'light' ? 'light' : 'dark'}
|
||||
date={new Date(value)}
|
||||
date={new Date(toSimpleDateString(value))}
|
||||
onDateChange={onChangeInternal}
|
||||
mode="date"
|
||||
testID={`${testID}-datepicker`}
|
||||
aria-label={label}
|
||||
accessibilityLabel={label}
|
||||
accessibilityHint={accessibilityHint}
|
||||
maximumDate={
|
||||
maximumDate
|
||||
? new Date(toSimpleDateString(maximumDate))
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<Button
|
||||
@@ -84,7 +94,7 @@ export function DateField({
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
</Dialog.Inner>
|
||||
</Dialog.ScrollableInner>
|
||||
</Dialog.Outer>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import {StyleSheet, TextInput, TextInputProps} from 'react-native'
|
||||
// @ts-ignore
|
||||
// @ts-expect-error untyped
|
||||
import {unstable_createElement} from 'react-native-web'
|
||||
|
||||
import {DateFieldProps} from '#/components/forms/DateField/types'
|
||||
@@ -39,6 +39,7 @@ export function DateField({
|
||||
isInvalid,
|
||||
testID,
|
||||
accessibilityHint,
|
||||
maximumDate,
|
||||
}: DateFieldProps) {
|
||||
const handleOnChange = React.useCallback(
|
||||
(e: any) => {
|
||||
@@ -56,12 +57,14 @@ export function DateField({
|
||||
<TextField.Root isInvalid={isInvalid}>
|
||||
<TextField.Icon icon={CalendarDays} />
|
||||
<Input
|
||||
value={value}
|
||||
value={toSimpleDateString(value)}
|
||||
label={label}
|
||||
onChange={handleOnChange}
|
||||
onChangeText={() => {}}
|
||||
testID={testID}
|
||||
accessibilityHint={accessibilityHint}
|
||||
// @ts-expect-error not typed as <input type="date"> even though it is one
|
||||
max={maximumDate ? toSimpleDateString(maximumDate) : undefined}
|
||||
/>
|
||||
</TextField.Root>
|
||||
)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
export type DateFieldProps = {
|
||||
value: string
|
||||
value: string | Date
|
||||
onChangeDate: (date: string) => void
|
||||
label: string
|
||||
isInvalid?: boolean
|
||||
testID?: string
|
||||
accessibilityHint?: string
|
||||
maximumDate?: string | Date
|
||||
}
|
||||
|
||||
@@ -6,21 +6,23 @@ import {useLingui} from '@lingui/react'
|
||||
import {toNiceDomain} from '#/lib/strings/url-helpers'
|
||||
import {isAndroid} from '#/platform/detection'
|
||||
import {ServerInputDialog} from '#/view/com/auth/server-input'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
|
||||
import {PencilLine_Stroke2_Corner0_Rounded as Pencil} from '#/components/icons/Pencil'
|
||||
import {Button} from '../Button'
|
||||
import {useDialogControl} from '../Dialog'
|
||||
import {Text} from '../Typography'
|
||||
import {atoms as a, tokens, useTheme} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
import {Globe_Stroke2_Corner0_Rounded as GlobeIcon} from '#/components/icons/Globe'
|
||||
import {PencilLine_Stroke2_Corner0_Rounded as PencilIcon} from '#/components/icons/Pencil'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function HostingProvider({
|
||||
serviceUrl,
|
||||
onSelectServiceUrl,
|
||||
onOpenDialog,
|
||||
minimal,
|
||||
}: {
|
||||
serviceUrl: string
|
||||
onSelectServiceUrl: (provider: string) => void
|
||||
onOpenDialog?: () => void
|
||||
minimal?: boolean
|
||||
}) {
|
||||
const serverInputControl = useDialogControl()
|
||||
const t = useTheme()
|
||||
@@ -29,9 +31,7 @@ export function HostingProvider({
|
||||
const onPressSelectService = React.useCallback(() => {
|
||||
Keyboard.dismiss()
|
||||
serverInputControl.open()
|
||||
if (onOpenDialog) {
|
||||
onOpenDialog()
|
||||
}
|
||||
onOpenDialog?.()
|
||||
}, [onOpenDialog, serverInputControl])
|
||||
|
||||
return (
|
||||
@@ -40,57 +40,80 @@ export function HostingProvider({
|
||||
control={serverInputControl}
|
||||
onSelect={onSelectServiceUrl}
|
||||
/>
|
||||
<Button
|
||||
testID="selectServiceButton"
|
||||
label={toNiceDomain(serviceUrl)}
|
||||
accessibilityHint={_(msg`Press to change hosting provider`)}
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
style={[
|
||||
a.w_full,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.rounded_sm,
|
||||
a.px_md,
|
||||
a.pr_sm,
|
||||
a.gap_xs,
|
||||
{paddingVertical: isAndroid ? 14 : 9},
|
||||
]}
|
||||
onPress={onPressSelectService}>
|
||||
{({hovered, pressed}) => {
|
||||
const interacted = hovered || pressed
|
||||
return (
|
||||
<>
|
||||
<View style={a.pr_xs}>
|
||||
<Globe
|
||||
size="md"
|
||||
fill={
|
||||
interacted ? t.palette.contrast_800 : t.palette.contrast_500
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<Text style={[a.text_md]}>{toNiceDomain(serviceUrl)}</Text>
|
||||
<View
|
||||
style={[
|
||||
a.rounded_sm,
|
||||
interacted
|
||||
? t.atoms.bg_contrast_300
|
||||
: t.atoms.bg_contrast_100,
|
||||
{marginLeft: 'auto', padding: 6},
|
||||
]}>
|
||||
<Pencil
|
||||
size="sm"
|
||||
style={{
|
||||
color: interacted
|
||||
? t.palette.contrast_800
|
||||
: t.palette.contrast_500,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</Button>
|
||||
{minimal ? (
|
||||
<View style={[a.flex_row, a.align_center, a.flex_wrap]}>
|
||||
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
||||
You are creating an account on{' '}
|
||||
</Text>
|
||||
<Button
|
||||
label={toNiceDomain(serviceUrl)}
|
||||
accessibilityHint={_(msg`Press to change hosting provider`)}
|
||||
onPress={onPressSelectService}
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="tiny"
|
||||
style={[a.px_xs, {marginLeft: tokens.space.xs * -1}]}>
|
||||
<ButtonText style={[a.text_sm]}>
|
||||
{toNiceDomain(serviceUrl)}
|
||||
</ButtonText>
|
||||
<ButtonIcon icon={PencilIcon} />
|
||||
</Button>
|
||||
</View>
|
||||
) : (
|
||||
<Button
|
||||
testID="selectServiceButton"
|
||||
label={toNiceDomain(serviceUrl)}
|
||||
accessibilityHint={_(msg`Press to change hosting provider`)}
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
style={[
|
||||
a.w_full,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.rounded_sm,
|
||||
a.pl_md,
|
||||
a.pr_sm,
|
||||
a.gap_xs,
|
||||
{paddingVertical: isAndroid ? 14 : 8},
|
||||
]}
|
||||
onPress={onPressSelectService}>
|
||||
{({hovered, pressed}) => {
|
||||
const interacted = hovered || pressed
|
||||
return (
|
||||
<>
|
||||
<View style={a.pr_xs}>
|
||||
<GlobeIcon
|
||||
size="md"
|
||||
fill={
|
||||
interacted
|
||||
? t.palette.contrast_800
|
||||
: t.palette.contrast_500
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<Text style={[a.text_md]}>{toNiceDomain(serviceUrl)}</Text>
|
||||
<View
|
||||
style={[
|
||||
a.rounded_sm,
|
||||
interacted
|
||||
? t.atoms.bg_contrast_300
|
||||
: t.atoms.bg_contrast_100,
|
||||
{marginLeft: 'auto', padding: 6},
|
||||
]}>
|
||||
<PencilIcon
|
||||
size="sm"
|
||||
style={{
|
||||
color: interacted
|
||||
? t.palette.contrast_800
|
||||
: t.palette.contrast_500,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,10 +11,12 @@ import React from 'react'
|
||||
import {formatDistance, Locale} from 'date-fns'
|
||||
import {
|
||||
ca,
|
||||
da,
|
||||
de,
|
||||
el,
|
||||
enGB,
|
||||
es,
|
||||
eu,
|
||||
fi,
|
||||
fr,
|
||||
gl,
|
||||
@@ -30,6 +32,7 @@ import {
|
||||
ptBR,
|
||||
ro,
|
||||
ru,
|
||||
sv,
|
||||
th,
|
||||
tr,
|
||||
uk,
|
||||
@@ -50,10 +53,12 @@ const locales: Record<AppLanguage, Locale | undefined> = {
|
||||
an: undefined,
|
||||
ast: undefined,
|
||||
ca,
|
||||
da,
|
||||
de,
|
||||
el,
|
||||
['en-GB']: enGB,
|
||||
es,
|
||||
eu,
|
||||
fi,
|
||||
fr,
|
||||
ga: undefined,
|
||||
@@ -71,6 +76,7 @@ const locales: Record<AppLanguage, Locale | undefined> = {
|
||||
['pt-BR']: ptBR,
|
||||
ro,
|
||||
ru,
|
||||
sv,
|
||||
th,
|
||||
tr,
|
||||
uk,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {createSinglePathSVG} from './TEMPLATE'
|
||||
|
||||
export const Ticket_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M4 5.5a.5.5 0 0 0-.5.5v2.535a.5.5 0 0 0 .25.433A3.498 3.498 0 0 1 5.5 12a3.498 3.498 0 0 1-1.75 3.032.5.5 0 0 0-.25.433V18a.5.5 0 0 0 .5.5h16a.5.5 0 0 0 .5-.5v-2.535a.5.5 0 0 0-.25-.433A3.498 3.498 0 0 1 18.5 12a3.5 3.5 0 0 1 1.75-3.032.5.5 0 0 0 .25-.433V6a.5.5 0 0 0-.5-.5H4ZM2.5 6A1.5 1.5 0 0 1 4 4.5h16A1.5 1.5 0 0 1 21.5 6v3.17a.5.5 0 0 1-.333.472 2.501 2.501 0 0 0 0 4.716.5.5 0 0 1 .333.471V18a1.5 1.5 0 0 1-1.5 1.5H4A1.5 1.5 0 0 1 2.5 18v-3.17a.5.5 0 0 1 .333-.472 2.501 2.501 0 0 0 0-4.716.5.5 0 0 1-.333-.471V6Zm12 2a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Zm0 4a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Zm0 4a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Z',
|
||||
path: 'M3 4a1 1 0 0 0-1 1v4.17a1 1 0 0 0 .667.944 2.001 2.001 0 0 1 0 3.772A1 1 0 0 0 2 14.83V19a1 1 0 0 0 1 1h18a1 1 0 0 0 1-1v-4.17a1 1 0 0 0-.667-.944 2.001 2.001 0 0 1 0-3.772A1 1 0 0 0 22 9.17V5a1 1 0 0 0-1-1H3Zm1 4.535V6h16v2.535A4 4 0 0 0 18 12c0 1.482.805 2.773 2 3.465V18H4v-2.535A4 4 0 0 0 6 12a4 4 0 0 0-2-3.465ZM15 15a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm-1-3a1 1 0 1 1 2 0 1 1 0 0 1-2 0Zm1-5a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z',
|
||||
})
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {ScrollView} from 'react-native-gesture-handler'
|
||||
import {ScrollView, View} from 'react-native'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
@@ -12,6 +11,7 @@ import {
|
||||
import {useTrendingTopics} from '#/state/queries/trending/useTrendingTopics'
|
||||
import {useTrendingConfig} from '#/state/trending-config'
|
||||
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
|
||||
import {atoms as a, useGutters, useTheme} from '#/alf'
|
||||
import {Button, ButtonIcon} from '#/components/Button'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||
@@ -42,81 +42,87 @@ export function Inner() {
|
||||
|
||||
return error || noTopics ? null : (
|
||||
<View style={[t.atoms.border_contrast_low, a.border_t]}>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
decelerationRate="fast">
|
||||
<View style={[gutters, a.flex_row, a.align_center, a.gap_lg]}>
|
||||
<View style={{paddingLeft: 4, paddingRight: 2}}>
|
||||
<Graph size="sm" />
|
||||
</View>
|
||||
{isLoading ? (
|
||||
<View style={[a.py_lg, a.flex_row, a.gap_lg, a.align_center]}>
|
||||
<LoadingPlaceholder
|
||||
width={80}
|
||||
height={undefined}
|
||||
style={{alignSelf: 'stretch'}}
|
||||
/>
|
||||
<LoadingPlaceholder
|
||||
width={50}
|
||||
height={undefined}
|
||||
style={{alignSelf: 'stretch'}}
|
||||
/>
|
||||
<LoadingPlaceholder
|
||||
width={120}
|
||||
height={undefined}
|
||||
style={{alignSelf: 'stretch'}}
|
||||
/>
|
||||
<LoadingPlaceholder
|
||||
width={30}
|
||||
height={undefined}
|
||||
style={{alignSelf: 'stretch'}}
|
||||
/>
|
||||
<LoadingPlaceholder
|
||||
width={180}
|
||||
height={undefined}
|
||||
style={{alignSelf: 'stretch'}}
|
||||
/>
|
||||
<Text
|
||||
style={[t.atoms.text_contrast_medium, a.text_sm, a.font_bold]}>
|
||||
{' '}
|
||||
</Text>
|
||||
<BlockDrawerGesture>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
decelerationRate="fast">
|
||||
<View style={[gutters, a.flex_row, a.align_center, a.gap_lg]}>
|
||||
<View style={{paddingLeft: 4, paddingRight: 2}}>
|
||||
<Graph size="sm" />
|
||||
</View>
|
||||
) : !trending?.topics ? null : (
|
||||
<>
|
||||
{trending.topics.map(topic => (
|
||||
<TrendingTopicLink
|
||||
key={topic.link}
|
||||
topic={topic}
|
||||
onPress={() => {
|
||||
logEvent('trendingTopic:click', {context: 'interstitial'})
|
||||
}}>
|
||||
<View style={[a.py_lg]}>
|
||||
<Text
|
||||
style={[
|
||||
t.atoms.text,
|
||||
a.text_sm,
|
||||
a.font_bold,
|
||||
{opacity: 0.7}, // NOTE: we use opacity 0.7 instead of a color to match the color of the home pager tab bar
|
||||
]}>
|
||||
{topic.topic}
|
||||
</Text>
|
||||
</View>
|
||||
</TrendingTopicLink>
|
||||
))}
|
||||
<Button
|
||||
label={_(msg`Hide trending topics`)}
|
||||
size="tiny"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
shape="round"
|
||||
onPress={() => trendingPrompt.open()}>
|
||||
<ButtonIcon icon={X} />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
{isLoading ? (
|
||||
<View style={[a.py_lg, a.flex_row, a.gap_lg, a.align_center]}>
|
||||
<LoadingPlaceholder
|
||||
width={80}
|
||||
height={undefined}
|
||||
style={{alignSelf: 'stretch'}}
|
||||
/>
|
||||
<LoadingPlaceholder
|
||||
width={50}
|
||||
height={undefined}
|
||||
style={{alignSelf: 'stretch'}}
|
||||
/>
|
||||
<LoadingPlaceholder
|
||||
width={120}
|
||||
height={undefined}
|
||||
style={{alignSelf: 'stretch'}}
|
||||
/>
|
||||
<LoadingPlaceholder
|
||||
width={30}
|
||||
height={undefined}
|
||||
style={{alignSelf: 'stretch'}}
|
||||
/>
|
||||
<LoadingPlaceholder
|
||||
width={180}
|
||||
height={undefined}
|
||||
style={{alignSelf: 'stretch'}}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
t.atoms.text_contrast_medium,
|
||||
a.text_sm,
|
||||
a.font_bold,
|
||||
]}>
|
||||
{' '}
|
||||
</Text>
|
||||
</View>
|
||||
) : !trending?.topics ? null : (
|
||||
<>
|
||||
{trending.topics.map(topic => (
|
||||
<TrendingTopicLink
|
||||
key={topic.link}
|
||||
topic={topic}
|
||||
onPress={() => {
|
||||
logEvent('trendingTopic:click', {context: 'interstitial'})
|
||||
}}>
|
||||
<View style={[a.py_lg]}>
|
||||
<Text
|
||||
style={[
|
||||
t.atoms.text,
|
||||
a.text_sm,
|
||||
a.font_bold,
|
||||
{opacity: 0.7}, // NOTE: we use opacity 0.7 instead of a color to match the color of the home pager tab bar
|
||||
]}>
|
||||
{topic.topic}
|
||||
</Text>
|
||||
</View>
|
||||
</TrendingTopicLink>
|
||||
))}
|
||||
<Button
|
||||
label={_(msg`Hide trending topics`)}
|
||||
size="tiny"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
shape="round"
|
||||
onPress={() => trendingPrompt.open()}>
|
||||
<ButtonIcon icon={X} />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</BlockDrawerGesture>
|
||||
|
||||
<Prompt.Basic
|
||||
control={trendingPrompt}
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
import React, {useEffect} from 'react'
|
||||
import {ScrollView, View} from 'react-native'
|
||||
import {AppBskyEmbedVideo, AtUri} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {VIDEO_FEED_URI} from '#/lib/constants'
|
||||
import {makeCustomFeedLink} from '#/lib/routes/links'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {useTrendingSettingsApi} from '#/state/preferences/trending'
|
||||
import {usePostFeedQuery} from '#/state/queries/post-feed'
|
||||
import {RQKEY} from '#/state/queries/post-feed'
|
||||
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
|
||||
import {atoms as a, useGutters, useTheme} from '#/alf'
|
||||
import {Button, ButtonIcon} from '#/components/Button'
|
||||
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||
import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending2'
|
||||
import {Link} from '#/components/Link'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {
|
||||
CompactVideoPostCard,
|
||||
CompactVideoPostCardPlaceholder,
|
||||
} from '#/components/VideoPostCard'
|
||||
|
||||
const CARD_WIDTH = 100
|
||||
|
||||
const FEED_DESC = `feedgen|${VIDEO_FEED_URI}`
|
||||
const FEED_PARAMS: {
|
||||
feedCacheKey: 'discover'
|
||||
} = {
|
||||
feedCacheKey: 'discover',
|
||||
}
|
||||
|
||||
export function TrendingVideos() {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const gutters = useGutters([0, 'base'])
|
||||
const {data, isLoading, error} = usePostFeedQuery(FEED_DESC, FEED_PARAMS)
|
||||
|
||||
// Refetch on unmount if nothing else is using this query.
|
||||
const queryClient = useQueryClient()
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
const query = queryClient
|
||||
.getQueryCache()
|
||||
.find({queryKey: RQKEY(FEED_DESC, FEED_PARAMS)})
|
||||
if (query && query.getObserversCount() <= 1) {
|
||||
query.fetch()
|
||||
}
|
||||
}
|
||||
}, [queryClient])
|
||||
|
||||
const {setTrendingVideoDisabled} = useTrendingSettingsApi()
|
||||
const trendingPrompt = Prompt.usePromptControl()
|
||||
|
||||
const onConfirmHide = React.useCallback(() => {
|
||||
setTrendingVideoDisabled(true)
|
||||
logEvent('trendingVideos:hide', {context: 'interstitial:discover'})
|
||||
}, [setTrendingVideoDisabled])
|
||||
|
||||
if (error) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.pt_lg,
|
||||
a.pb_lg,
|
||||
a.border_t,
|
||||
t.atoms.border_contrast_low,
|
||||
t.atoms.bg_contrast_25,
|
||||
]}>
|
||||
<View
|
||||
style={[
|
||||
gutters,
|
||||
a.pb_sm,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.justify_between,
|
||||
]}>
|
||||
<View style={[a.flex_1, a.flex_row, a.align_center, a.gap_xs]}>
|
||||
<Graph />
|
||||
<Text style={[a.text_md, a.font_bold, a.leading_snug]}>
|
||||
<Trans>Trending Videos</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
<Button
|
||||
label={_(msg`Dismiss this section`)}
|
||||
size="tiny"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
shape="round"
|
||||
onPress={() => trendingPrompt.open()}>
|
||||
<ButtonIcon icon={X} />
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<BlockDrawerGesture>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
decelerationRate="fast"
|
||||
snapToInterval={CARD_WIDTH + a.gap_sm.gap}>
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.gap_sm,
|
||||
{
|
||||
paddingLeft: gutters.paddingLeft,
|
||||
paddingRight: gutters.paddingRight,
|
||||
},
|
||||
]}>
|
||||
{isLoading ? (
|
||||
Array(10)
|
||||
.fill(0)
|
||||
.map((_, i) => (
|
||||
<View key={i} style={[{width: CARD_WIDTH}]}>
|
||||
<CompactVideoPostCardPlaceholder />
|
||||
</View>
|
||||
))
|
||||
) : error || !data ? (
|
||||
<Text>
|
||||
<Trans>Whoops! Trending videos failed to load.</Trans>
|
||||
</Text>
|
||||
) : (
|
||||
<VideoCards data={data} />
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</BlockDrawerGesture>
|
||||
|
||||
<Prompt.Basic
|
||||
control={trendingPrompt}
|
||||
title={_(msg`Hide trending videos?`)}
|
||||
description={_(msg`You can update this later from your settings.`)}
|
||||
confirmButtonCta={_(msg`Hide`)}
|
||||
onConfirm={onConfirmHide}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function VideoCards({
|
||||
data,
|
||||
}: {
|
||||
data: Exclude<ReturnType<typeof usePostFeedQuery>['data'], undefined>
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const items = React.useMemo(() => {
|
||||
return data.pages
|
||||
.flatMap(page => page.slices)
|
||||
.map(slice => slice.items[0])
|
||||
.filter(Boolean)
|
||||
.filter(item => AppBskyEmbedVideo.isView(item.post.embed))
|
||||
.slice(0, 8)
|
||||
}, [data])
|
||||
const href = React.useMemo(() => {
|
||||
const urip = new AtUri(VIDEO_FEED_URI)
|
||||
return makeCustomFeedLink(urip.host, urip.rkey, undefined, 'discover')
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
{items.map(item => (
|
||||
<View key={item.post.uri} style={[{width: CARD_WIDTH}]}>
|
||||
<CompactVideoPostCard
|
||||
post={item.post}
|
||||
moderation={item.moderation}
|
||||
sourceContext={{
|
||||
type: 'feedgen',
|
||||
uri: VIDEO_FEED_URI,
|
||||
sourceInterstitial: 'discover',
|
||||
}}
|
||||
onInteract={() => {
|
||||
logEvent('videoCard:click', {
|
||||
context: 'interstitial:discover',
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
|
||||
<View style={[{width: CARD_WIDTH * 2}]}>
|
||||
<Link
|
||||
to={href}
|
||||
label={_(msg`View more`)}
|
||||
style={[
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
a.flex_1,
|
||||
a.rounded_md,
|
||||
t.atoms.bg,
|
||||
]}>
|
||||
{({pressed}) => (
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.gap_md,
|
||||
{
|
||||
opacity: pressed ? 0.6 : 1,
|
||||
},
|
||||
]}>
|
||||
<Text style={[a.text_md]}>
|
||||
<Trans>View more</Trans>
|
||||
</Text>
|
||||
<View
|
||||
style={[
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
a.rounded_full,
|
||||
{
|
||||
width: 34,
|
||||
height: 34,
|
||||
backgroundColor: t.palette.primary_500,
|
||||
},
|
||||
]}>
|
||||
<ButtonIcon icon={ChevronRight} />
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</Link>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -41,6 +41,7 @@ export class FeedViewPostsSlice {
|
||||
isFallbackMarker: boolean
|
||||
isOrphan: boolean
|
||||
rootUri: string
|
||||
feedPostUri: string
|
||||
|
||||
constructor(feedPost: FeedViewPost) {
|
||||
const {post, reply, reason} = feedPost
|
||||
@@ -48,6 +49,7 @@ export class FeedViewPostsSlice {
|
||||
this.isIncompleteThread = false
|
||||
this.isFallbackMarker = false
|
||||
this.isOrphan = false
|
||||
this.feedPostUri = post.uri
|
||||
if (AppBskyFeedDefs.isPostView(reply?.root)) {
|
||||
this.rootUri = reply.root.uri
|
||||
} else {
|
||||
|
||||
@@ -23,7 +23,9 @@ export class AuthorFeedAPI implements FeedAPI {
|
||||
|
||||
get params() {
|
||||
const params = {...this._params}
|
||||
params.includePins = params.filter !== 'posts_with_media'
|
||||
params.includePins =
|
||||
params.filter === 'posts_with_replies' ||
|
||||
params.filter === 'posts_and_author_threads'
|
||||
return params
|
||||
}
|
||||
|
||||
|
||||
@@ -194,7 +194,12 @@ export async function post(
|
||||
}
|
||||
|
||||
async function resolveRT(agent: BskyAgent, richtext: RichText) {
|
||||
let rt = new RichText({text: richtext.text.trimEnd()}, {cleanNewlines: true})
|
||||
const trimmedText = richtext.text
|
||||
// Trim leading whitespace-only lines (but don't break ASCII art).
|
||||
.replace(/^(\s*\n)+/, '')
|
||||
// Trim any trailing whitespace.
|
||||
.trimEnd()
|
||||
let rt = new RichText({text: trimmedText}, {cleanNewlines: true})
|
||||
await rt.detectFacets(agent)
|
||||
|
||||
rt = shortenLinks(rt)
|
||||
|
||||
+20
-24
@@ -1,27 +1,23 @@
|
||||
// import {init} from '@bitdrift/react-native'
|
||||
// import {Statsig} from 'statsig-react-native-expo'
|
||||
// export {debug, error, info, warn} from '@bitdrift/react-native'
|
||||
import {init, SessionStrategy} from '@bitdrift/react-native'
|
||||
import {Statsig} from 'statsig-react-native-expo'
|
||||
export {debug, error, info, warn} from '@bitdrift/react-native'
|
||||
|
||||
// import {initPromise} from './statsig/statsig'
|
||||
import {initPromise} from './statsig/statsig'
|
||||
|
||||
// const BITDRIFT_API_KEY = process.env.BITDRIFT_API_KEY
|
||||
const BITDRIFT_API_KEY = process.env.BITDRIFT_API_KEY
|
||||
|
||||
// initPromise.then(() => {
|
||||
// let isEnabled = false
|
||||
// try {
|
||||
// if (Statsig.checkGate('enable_bitdrift')) {
|
||||
// isEnabled = true
|
||||
// }
|
||||
// } catch (e) {
|
||||
// // Statsig may complain about it being called too early.
|
||||
// }
|
||||
// if (isEnabled && BITDRIFT_API_KEY) {
|
||||
// init(BITDRIFT_API_KEY, {url: 'https://api-bsky.bitdrift.io'})
|
||||
// }
|
||||
// })
|
||||
|
||||
// TODO: Reenable when the build issue is fixed.
|
||||
export function debug(_message: string) {}
|
||||
export function error(_message: string) {}
|
||||
export function info(_message: string) {}
|
||||
export function warn(_message: string) {}
|
||||
initPromise.then(() => {
|
||||
let isEnabled = false
|
||||
try {
|
||||
if (Statsig.checkGate('enable_bitdrift')) {
|
||||
isEnabled = true
|
||||
}
|
||||
} catch (e) {
|
||||
// Statsig may complain about it being called too early.
|
||||
}
|
||||
if (isEnabled && BITDRIFT_API_KEY) {
|
||||
init(BITDRIFT_API_KEY, SessionStrategy.Activity, {
|
||||
url: 'https://api-bsky.bitdrift.io',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -52,6 +52,8 @@ export const MAX_DM_GRAPHEME_LENGTH = 1000
|
||||
// but increasing limit per user feedback
|
||||
export const MAX_ALT_TEXT = 2000
|
||||
|
||||
export const MAX_REPORT_REASON_GRAPHEME_LENGTH = 2000
|
||||
|
||||
export function IS_TEST_USER(handle?: string) {
|
||||
return handle && handle?.endsWith('.test')
|
||||
}
|
||||
@@ -63,6 +65,21 @@ export function IS_PROD_SERVICE(url?: string) {
|
||||
export const PROD_DEFAULT_FEED = (rkey: string) =>
|
||||
`at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/${rkey}`
|
||||
|
||||
export const STAGING_DEFAULT_FEED = (rkey: string) =>
|
||||
`at://did:plc:yofh3kx63drvfljkibw5zuxo/app.bsky.feed.generator/${rkey}`
|
||||
|
||||
export const PROD_FEEDS = [
|
||||
`feedgen|${PROD_DEFAULT_FEED('whats-hot')}`,
|
||||
`feedgen|${PROD_DEFAULT_FEED('thevids')}`,
|
||||
]
|
||||
|
||||
export const STAGING_FEEDS = [
|
||||
`feedgen|${STAGING_DEFAULT_FEED('whats-hot')}`,
|
||||
`feedgen|${STAGING_DEFAULT_FEED('thevids')}`,
|
||||
]
|
||||
|
||||
export const FEEDBACK_FEEDS = [...PROD_FEEDS, ...STAGING_FEEDS]
|
||||
|
||||
export const POST_IMG_MAX = {
|
||||
width: 2000,
|
||||
height: 2000,
|
||||
@@ -109,6 +126,11 @@ export const BSKY_FEED_OWNER_DIDS = [
|
||||
|
||||
export const DISCOVER_FEED_URI =
|
||||
'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot'
|
||||
export const VIDEO_FEED_URI =
|
||||
'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/thevids'
|
||||
export const STAGING_VIDEO_FEED_URI =
|
||||
'at://did:plc:yofh3kx63drvfljkibw5zuxo/app.bsky.feed.generator/thevids'
|
||||
export const VIDEO_FEED_URIS = [VIDEO_FEED_URI, STAGING_VIDEO_FEED_URI]
|
||||
export const DISCOVER_SAVED_FEED = {
|
||||
type: 'feed',
|
||||
value: DISCOVER_FEED_URI,
|
||||
@@ -119,6 +141,11 @@ export const TIMELINE_SAVED_FEED = {
|
||||
value: 'following',
|
||||
pinned: true,
|
||||
}
|
||||
export const VIDEO_SAVED_FEED = {
|
||||
type: 'feed',
|
||||
value: VIDEO_FEED_URI,
|
||||
pinned: true,
|
||||
}
|
||||
|
||||
export const RECOMMENDED_SAVED_FEEDS: Pick<
|
||||
AppBskyActorDefs.SavedFeed,
|
||||
|
||||
@@ -8,8 +8,8 @@ export function getDataUriSize(uri: string): number {
|
||||
return Math.round((uri.length * 3) / 4)
|
||||
}
|
||||
|
||||
export function isUriImage(uri: string) {
|
||||
return /\.(jpg|jpeg|png).*$/.test(uri)
|
||||
export function isUriImage(uri: string): boolean {
|
||||
return /\.(jpg|jpeg|png|webp).*$/.test(uri)
|
||||
}
|
||||
|
||||
export function blobToDataUri(blob: Blob): Promise<string> {
|
||||
|
||||
@@ -19,9 +19,13 @@ export function makeProfileLink(
|
||||
export function makeCustomFeedLink(
|
||||
did: string,
|
||||
rkey: string,
|
||||
...segments: string[]
|
||||
segment?: string | undefined,
|
||||
feedCacheKey?: 'discover' | 'explore' | undefined,
|
||||
) {
|
||||
return [`/profile`, did, 'feed', rkey, ...segments].join('/')
|
||||
return (
|
||||
[`/profile`, did, 'feed', rkey, ...(segment ? [segment] : [])].join('/') +
|
||||
(feedCacheKey ? `?feedCacheKey=${encodeURIComponent(feedCacheKey)}` : '')
|
||||
)
|
||||
}
|
||||
|
||||
export function makeListLink(did: string, rkey: string, ...segments: string[]) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {NavigationState, PartialState} from '@react-navigation/native'
|
||||
import type {NativeStackNavigationProp} from '@react-navigation/native-stack'
|
||||
|
||||
import {VideoFeedSourceContext} from '#/screens/VideoFeed/types'
|
||||
|
||||
export type {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
|
||||
export type CommonNavigatorParams = {
|
||||
@@ -20,7 +22,11 @@ export type CommonNavigatorParams = {
|
||||
PostLikedBy: {name: string; rkey: string}
|
||||
PostRepostedBy: {name: string; rkey: string}
|
||||
PostQuotes: {name: string; rkey: string}
|
||||
ProfileFeed: {name: string; rkey: string}
|
||||
ProfileFeed: {
|
||||
name: string
|
||||
rkey: string
|
||||
feedCacheKey?: 'discover' | 'explore' | undefined
|
||||
}
|
||||
ProfileFeedLikedBy: {name: string; rkey: string}
|
||||
ProfileLabelerLikedBy: {name: string}
|
||||
Debug: undefined
|
||||
@@ -57,6 +63,7 @@ export type CommonNavigatorParams = {
|
||||
StarterPackShort: {code: string}
|
||||
StarterPackWizard: undefined
|
||||
StarterPackEdit: {rkey?: string}
|
||||
VideoFeed: VideoFeedSourceContext
|
||||
}
|
||||
|
||||
export type BottomTabNavigatorParams = CommonNavigatorParams & {
|
||||
|
||||
@@ -13,7 +13,12 @@ export type LogEvents = {
|
||||
withPassword: boolean
|
||||
}
|
||||
'account:loggedOut': {
|
||||
logContext: 'SwitchAccount' | 'Settings' | 'SignupQueued' | 'Deactivated'
|
||||
logContext:
|
||||
| 'SwitchAccount'
|
||||
| 'Settings'
|
||||
| 'SignupQueued'
|
||||
| 'Deactivated'
|
||||
| 'Takendown'
|
||||
scope: 'current' | 'every'
|
||||
}
|
||||
'notifications:openApp': {}
|
||||
@@ -131,16 +136,16 @@ export type LogEvents = {
|
||||
doesPosterFollowLiker: boolean | undefined
|
||||
likerClout: number | undefined
|
||||
postClout: number | undefined
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||
}
|
||||
'post:repost': {
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||
}
|
||||
'post:unlike': {
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||
}
|
||||
'post:unrepost': {
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||
}
|
||||
'post:mute': {}
|
||||
'post:unmute': {}
|
||||
@@ -163,6 +168,27 @@ export type LogEvents = {
|
||||
| 'FeedInterstitial'
|
||||
| 'ProfileHeaderSuggestedFollows'
|
||||
| 'PostOnboardingFindFollows'
|
||||
| 'ImmersiveVideo'
|
||||
}
|
||||
'suggestedUser:follow': {
|
||||
logContext:
|
||||
| 'Explore'
|
||||
| 'InterstitialDiscover'
|
||||
| 'InterstitialProfile'
|
||||
| 'Profile'
|
||||
location: 'Card' | 'Profile'
|
||||
recId?: number
|
||||
position: number
|
||||
}
|
||||
'suggestedUser:press': {
|
||||
logContext: 'Explore' | 'InterstitialDiscover' | 'InterstitialProfile'
|
||||
recId?: number
|
||||
position: number
|
||||
}
|
||||
'suggestedUser:seen': {
|
||||
logContext: 'Explore' | 'InterstitialDiscover' | 'InterstitialProfile'
|
||||
recId?: number
|
||||
position: number
|
||||
}
|
||||
'profile:unfollow': {
|
||||
logContext:
|
||||
@@ -179,6 +205,7 @@ export type LogEvents = {
|
||||
| 'FeedInterstitial'
|
||||
| 'ProfileHeaderSuggestedFollows'
|
||||
| 'PostOnboardingFindFollows'
|
||||
| 'ImmersiveVideo'
|
||||
}
|
||||
'chat:create': {
|
||||
logContext: 'ProfileHeader' | 'NewChatDialog' | 'SendViaChatDialog'
|
||||
@@ -218,7 +245,6 @@ export type LogEvents = {
|
||||
domain: string
|
||||
}
|
||||
|
||||
'feed:interstitial:profileCard:press': {}
|
||||
'feed:interstitial:feedCard:press': {}
|
||||
|
||||
'profile:header:suggestedFollowsCard:press': {}
|
||||
@@ -249,6 +275,15 @@ export type LogEvents = {
|
||||
'recommendedTopic:click': {
|
||||
context: 'explore'
|
||||
}
|
||||
'trendingVideos:show': {
|
||||
context: 'settings'
|
||||
}
|
||||
'trendingVideos:hide': {
|
||||
context: 'settings' | 'interstitial:discover' | 'interstitial:explore'
|
||||
}
|
||||
'videoCard:click': {
|
||||
context: 'interstitial:discover' | 'interstitial:explore' | 'feed'
|
||||
}
|
||||
|
||||
'progressGuide:hide': {}
|
||||
'progressGuide:followDialog:open': {}
|
||||
|
||||
@@ -3,5 +3,8 @@ export type Gate =
|
||||
| 'debug_show_feedcontext'
|
||||
| 'debug_subscriptions'
|
||||
| 'new_postonboarding'
|
||||
| 'onboarding_add_video_feed'
|
||||
| 'remove_show_latest_button'
|
||||
| 'test_gate_1'
|
||||
| 'test_gate_2'
|
||||
| 'trending_topics_beta'
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import React from 'react'
|
||||
import {Platform} from 'react-native'
|
||||
import {AppState, AppStateStatus} from 'react-native'
|
||||
import {sha256} from 'js-sha256'
|
||||
import {Statsig, StatsigProvider} from 'statsig-react-native-expo'
|
||||
|
||||
import {BUNDLE_DATE, BUNDLE_IDENTIFIER, IS_TESTFLIGHT} from '#/lib/app-info'
|
||||
// TODO: Reenable when the build issue is fixed.
|
||||
// import * as bitdrift from '#/lib/bitdrift'
|
||||
import * as bitdrift from '#/lib/bitdrift'
|
||||
import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import * as persisted from '#/state/persisted'
|
||||
@@ -108,8 +106,7 @@ export function logEvent<E extends keyof LogEvents>(
|
||||
console.groupCollapsed(eventName)
|
||||
console.log(fullMetadata)
|
||||
console.groupEnd()
|
||||
// TODO: Reenable when the build issue is fixed.
|
||||
// bitdrift.info(eventName, fullMetadata)
|
||||
bitdrift.info(eventName, fullMetadata)
|
||||
} catch (e) {
|
||||
// A log should never interrupt the calling code, whatever happens.
|
||||
logger.error('Failed to log an event', {message: e})
|
||||
@@ -191,13 +188,9 @@ export function useDangerousSetGate(): (
|
||||
}
|
||||
|
||||
function toStatsigUser(did: string | undefined): StatsigUser {
|
||||
let userID: string | undefined
|
||||
if (did) {
|
||||
userID = sha256(did)
|
||||
}
|
||||
const languagePrefs = persisted.get('languagePrefs')
|
||||
return {
|
||||
userID,
|
||||
userID: did,
|
||||
platform: Platform.OS as 'ios' | 'android' | 'web',
|
||||
custom: {
|
||||
refSrc,
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
|
||||
export async function loadString(key: string): Promise<string | null> {
|
||||
try {
|
||||
return await AsyncStorage.getItem(key)
|
||||
} catch {
|
||||
// not sure why this would fail... even reading the RN docs I'm unclear
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveString(key: string, value: string): Promise<boolean> {
|
||||
try {
|
||||
await AsyncStorage.setItem(key, value)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function load(key: string): Promise<any | null> {
|
||||
try {
|
||||
const str = await AsyncStorage.getItem(key)
|
||||
if (typeof str !== 'string') {
|
||||
return null
|
||||
}
|
||||
return JSON.parse(str)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function save(key: string, value: any): Promise<boolean> {
|
||||
try {
|
||||
await AsyncStorage.setItem(key, JSON.stringify(value))
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function remove(key: string): Promise<void> {
|
||||
try {
|
||||
await AsyncStorage.removeItem(key)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
export async function clear(): Promise<void> {
|
||||
try {
|
||||
await AsyncStorage.clear()
|
||||
} catch {}
|
||||
}
|
||||
@@ -19,6 +19,10 @@ export function createFullHandle(name: string, domain: string): string {
|
||||
return `${name}.${domain}`
|
||||
}
|
||||
|
||||
export function maxServiceHandleLength(domain: string): number {
|
||||
return 30 - `.${(domain || '').replace(/^[.]+/, '')}`.length
|
||||
}
|
||||
|
||||
export function isInvalidHandle(handle: string): boolean {
|
||||
return handle === 'handle.invalid'
|
||||
}
|
||||
@@ -38,7 +42,11 @@ export interface IsValidHandle {
|
||||
}
|
||||
|
||||
// More checks from https://github.com/bluesky-social/atproto/blob/main/packages/pds/src/handle/index.ts#L72
|
||||
export function validateHandle(str: string, userDomain: string): IsValidHandle {
|
||||
export function validateHandle(
|
||||
str: string,
|
||||
userDomain: string,
|
||||
isServiceHandle?: boolean,
|
||||
): IsValidHandle {
|
||||
const fullHandle = createFullHandle(str, userDomain)
|
||||
|
||||
const results = {
|
||||
@@ -46,7 +54,7 @@ export function validateHandle(str: string, userDomain: string): IsValidHandle {
|
||||
!str || (VALIDATE_REGEX.test(fullHandle) && !str.includes('.')),
|
||||
hyphenStartOrEnd: !str.startsWith('-') && !str.endsWith('-'),
|
||||
frontLength: str.length >= 3,
|
||||
totalLength: fullHandle.length <= 253,
|
||||
totalLength: fullHandle.length <= (isServiceHandle ? 30 : 253),
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export const getKeys = Object.keys as <T extends object>(
|
||||
obj: T,
|
||||
) => Array<keyof T>
|
||||
@@ -161,6 +161,8 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
|
||||
return AppLanguage.ast
|
||||
case 'ca':
|
||||
return AppLanguage.ca
|
||||
case 'da':
|
||||
return AppLanguage.da
|
||||
case 'de':
|
||||
return AppLanguage.de
|
||||
case 'el':
|
||||
@@ -169,6 +171,8 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
|
||||
return AppLanguage.en_GB
|
||||
case 'es':
|
||||
return AppLanguage.es
|
||||
case 'eu':
|
||||
return AppLanguage.eu
|
||||
case 'fi':
|
||||
return AppLanguage.fi
|
||||
case 'fr':
|
||||
@@ -203,6 +207,8 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
|
||||
return AppLanguage.ro
|
||||
case 'ru':
|
||||
return AppLanguage.ru
|
||||
case 'sv':
|
||||
return AppLanguage.sv
|
||||
case 'th':
|
||||
return AppLanguage.th
|
||||
case 'tr':
|
||||
|
||||
@@ -14,11 +14,13 @@ import {AppLanguage} from '#/locale/languages'
|
||||
import {messages as messagesAn} from '#/locale/locales/an/messages'
|
||||
import {messages as messagesAst} from '#/locale/locales/ast/messages'
|
||||
import {messages as messagesCa} from '#/locale/locales/ca/messages'
|
||||
import {messages as messagesDa} from '#/locale/locales/da/messages'
|
||||
import {messages as messagesDe} from '#/locale/locales/de/messages'
|
||||
import {messages as messagesEl} from '#/locale/locales/el/messages'
|
||||
import {messages as messagesEn} from '#/locale/locales/en/messages'
|
||||
import {messages as messagesEn_GB} from '#/locale/locales/en-GB/messages'
|
||||
import {messages as messagesEs} from '#/locale/locales/es/messages'
|
||||
import {messages as messagesEu} from '#/locale/locales/eu/messages'
|
||||
import {messages as messagesFi} from '#/locale/locales/fi/messages'
|
||||
import {messages as messagesFr} from '#/locale/locales/fr/messages'
|
||||
import {messages as messagesGa} from '#/locale/locales/ga/messages'
|
||||
@@ -36,6 +38,7 @@ import {messages as messagesPl} from '#/locale/locales/pl/messages'
|
||||
import {messages as messagesPt_BR} from '#/locale/locales/pt-BR/messages'
|
||||
import {messages as messagesRo} from '#/locale/locales/ro/messages'
|
||||
import {messages as messagesRu} from '#/locale/locales/ru/messages'
|
||||
import {messages as messagesSv} from '#/locale/locales/sv/messages'
|
||||
import {messages as messagesTh} from '#/locale/locales/th/messages'
|
||||
import {messages as messagesTr} from '#/locale/locales/tr/messages'
|
||||
import {messages as messagesUk} from '#/locale/locales/uk/messages'
|
||||
@@ -74,6 +77,14 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.da: {
|
||||
i18n.loadAndActivate({locale, messages: messagesDa})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-pluralrules/locale-data/da'),
|
||||
import('@formatjs/intl-numberformat/locale-data/da'),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.de: {
|
||||
i18n.loadAndActivate({locale, messages: messagesDe})
|
||||
await Promise.all([
|
||||
@@ -106,6 +117,14 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.eu: {
|
||||
i18n.loadAndActivate({locale, messages: messagesEu})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-pluralrules/locale-data/eu'),
|
||||
import('@formatjs/intl-numberformat/locale-data/eu'),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.fi: {
|
||||
i18n.loadAndActivate({locale, messages: messagesFi})
|
||||
await Promise.all([
|
||||
@@ -238,6 +257,14 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.sv: {
|
||||
i18n.loadAndActivate({locale, messages: messagesSv})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-pluralrules/locale-data/sv'),
|
||||
import('@formatjs/intl-numberformat/locale-data/sv'),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.th: {
|
||||
i18n.loadAndActivate({locale, messages: messagesTh})
|
||||
await Promise.all([
|
||||
|
||||
@@ -24,6 +24,10 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
mod = await import(`./locales/ca/messages`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.da: {
|
||||
mod = await import(`./locales/da/messages`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.de: {
|
||||
mod = await import(`./locales/de/messages`)
|
||||
break
|
||||
@@ -40,6 +44,10 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
mod = await import(`./locales/es/messages`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.eu: {
|
||||
mod = await import(`./locales/eu/messages`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.fi: {
|
||||
mod = await import(`./locales/fi/messages`)
|
||||
break
|
||||
@@ -108,6 +116,10 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
mod = await import(`./locales/ru/messages`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.sv: {
|
||||
mod = await import(`./locales/sv/messages`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.th: {
|
||||
mod = await import(`./locales/th/messages`)
|
||||
break
|
||||
|
||||
@@ -9,10 +9,12 @@ export enum AppLanguage {
|
||||
an = 'an',
|
||||
ast = 'ast',
|
||||
ca = 'ca',
|
||||
da = 'da',
|
||||
de = 'de',
|
||||
el = 'el',
|
||||
en_GB = 'en-GB',
|
||||
es = 'es',
|
||||
eu = 'eu',
|
||||
fi = 'fi',
|
||||
fr = 'fr',
|
||||
ga = 'ga',
|
||||
@@ -30,6 +32,7 @@ export enum AppLanguage {
|
||||
pt_BR = 'pt-BR',
|
||||
ro = 'ro',
|
||||
ru = 'ru',
|
||||
sv = 'sv',
|
||||
th = 'th',
|
||||
tr = 'tr',
|
||||
uk = 'uk',
|
||||
@@ -49,10 +52,12 @@ export const APP_LANGUAGES: AppLanguageConfig[] = [
|
||||
{code2: AppLanguage.an, name: 'Aragonés – Aragonese'},
|
||||
{code2: AppLanguage.ast, name: 'Asturianu – Asturian'},
|
||||
{code2: AppLanguage.ca, name: 'Català – Catalan'},
|
||||
{code2: AppLanguage.da, name: 'Dansk – Danish'},
|
||||
{code2: AppLanguage.de, name: 'Deutsch – German'},
|
||||
{code2: AppLanguage.el, name: 'Ελληνικά – Greek'},
|
||||
{code2: AppLanguage.en_GB, name: 'English (UK)'},
|
||||
{code2: AppLanguage.es, name: 'Español – Spanish'},
|
||||
{code2: AppLanguage.eu, name: 'Euskera – Basque'},
|
||||
{code2: AppLanguage.fi, name: 'Suomi – Finnish'},
|
||||
{code2: AppLanguage.fr, name: 'Français – French'},
|
||||
{code2: AppLanguage.ga, name: 'Gaeilge – Irish'},
|
||||
@@ -70,6 +75,7 @@ export const APP_LANGUAGES: AppLanguageConfig[] = [
|
||||
{code2: AppLanguage.pt_BR, name: 'Português (BR) – Portuguese (BR)'},
|
||||
{code2: AppLanguage.ro, name: 'Română – Romanian'},
|
||||
{code2: AppLanguage.ru, name: 'Русский – Russian'},
|
||||
{code2: AppLanguage.sv, name: 'Svenska – Swedish'},
|
||||
{code2: AppLanguage.th, name: 'ภาษาไทย – Thai'},
|
||||
{code2: AppLanguage.tr, name: 'Türkçe – Turkish'},
|
||||
{code2: AppLanguage.uk, name: 'Українська – Ukrainian'},
|
||||
|
||||
@@ -3497,7 +3497,7 @@ msgstr "Rechistro de publicación invalido u no compatible"
|
||||
|
||||
#: src/screens/Login/LoginForm.tsx:90
|
||||
#: src/screens/Login/LoginForm.tsx:149
|
||||
msgid "Invalid username or password"
|
||||
msgid "Incorrect username or password"
|
||||
msgstr "Nombre d'usuario u clau no validos"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:91
|
||||
@@ -6417,7 +6417,7 @@ msgstr "Salir?"
|
||||
#: src/view/shell/bottom-bar/BottomBarWeb.tsx:205
|
||||
#: src/view/shell/NavSignupCard.tsx:47
|
||||
#: src/view/shell/NavSignupCard.tsx:52
|
||||
msgid "Sign up"
|
||||
msgid "Create account"
|
||||
msgstr "Crear cuenta"
|
||||
|
||||
#: src/components/moderation/ScreenHider.tsx:91
|
||||
|
||||
@@ -3694,7 +3694,7 @@ msgstr ""
|
||||
|
||||
#: src/screens/Login/LoginForm.tsx:90
|
||||
#: src/screens/Login/LoginForm.tsx:149
|
||||
msgid "Invalid username or password"
|
||||
msgid "Incorrect username or password"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:91
|
||||
@@ -6861,7 +6861,7 @@ msgstr "¿Quies zarrar la sesión?"
|
||||
#: src/view/shell/bottom-bar/BottomBarWeb.tsx:205
|
||||
#: src/view/shell/NavSignupCard.tsx:47
|
||||
#: src/view/shell/NavSignupCard.tsx:52
|
||||
msgid "Sign up"
|
||||
msgid "Create account"
|
||||
msgstr "Rexistrase"
|
||||
|
||||
#: src/components/moderation/ScreenHider.tsx:91
|
||||
|
||||
@@ -1936,7 +1936,7 @@ msgstr "Tanca el diàleg"
|
||||
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:137
|
||||
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:173
|
||||
msgid "Close emoji picker"
|
||||
msgstr ""
|
||||
msgstr "Tanca el selector d'emojis"
|
||||
|
||||
#: src/components/dialogs/GifSelect.tsx:169
|
||||
msgid "Close GIF dialog"
|
||||
@@ -2429,7 +2429,7 @@ msgstr "Creat {0}"
|
||||
|
||||
#: src/screens/List/ListHiddenScreen.tsx:127
|
||||
msgid "Creator has been blocked"
|
||||
msgstr ""
|
||||
msgstr "S'ha bloquejat al creador"
|
||||
|
||||
#: src/screens/Onboarding/index.tsx:26
|
||||
#: src/screens/Onboarding/state.ts:99
|
||||
@@ -3049,7 +3049,7 @@ msgstr "Ensenyament"
|
||||
|
||||
#: src/screens/List/ListHiddenScreen.tsx:141
|
||||
msgid "Either the creator of this list has blocked you or you have blocked the creator."
|
||||
msgstr ""
|
||||
msgstr "El creador d'aquesta llista t'ha bloquejat o l'has bloquejat tu."
|
||||
|
||||
#: src/screens/Settings/AccountSettings.tsx:60
|
||||
#: src/screens/Signup/StepInfo/index.tsx:170
|
||||
@@ -4457,7 +4457,7 @@ msgstr "Registre de publicació no vàlid o no admès"
|
||||
|
||||
#: src/screens/Login/LoginForm.tsx:90
|
||||
#: src/screens/Login/LoginForm.tsx:149
|
||||
msgid "Invalid username or password"
|
||||
msgid "Incorrect username or password"
|
||||
msgstr "Nom d'usuari o contrasenya incorrectes"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:91
|
||||
@@ -4837,7 +4837,7 @@ msgstr "Llista per <0/>"
|
||||
|
||||
#: src/view/com/profile/ProfileSubpageHeader.tsx:154
|
||||
msgid "List by you"
|
||||
msgstr ""
|
||||
msgstr "Llista teva"
|
||||
|
||||
#: src/view/screens/ProfileList.tsx:455
|
||||
msgid "List deleted"
|
||||
@@ -8301,7 +8301,7 @@ msgstr "Tancar la sessió?"
|
||||
#: src/view/shell/bottom-bar/BottomBarWeb.tsx:205
|
||||
#: src/view/shell/NavSignupCard.tsx:47
|
||||
#: src/view/shell/NavSignupCard.tsx:52
|
||||
msgid "Sign up"
|
||||
msgid "Create account"
|
||||
msgstr "Registra't"
|
||||
|
||||
#: src/view/shell/NavSignupCard.tsx:47
|
||||
@@ -8675,7 +8675,7 @@ msgstr "Toca per canviar la icona de l'aplicació"
|
||||
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:138
|
||||
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:174
|
||||
msgid "Tap to close the emoji picker"
|
||||
msgstr ""
|
||||
msgstr "Toca per a tancar el selector d'emojis"
|
||||
|
||||
#: src/components/ProgressGuide/Toast.tsx:156
|
||||
msgid "Tap to dismiss"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3931,7 +3931,7 @@ msgstr "Ungültiger oder nicht unterstützter Beitragrekord"
|
||||
|
||||
#: src/screens/Login/LoginForm.tsx:90
|
||||
#: src/screens/Login/LoginForm.tsx:149
|
||||
msgid "Invalid username or password"
|
||||
msgid "Incorrect username or password"
|
||||
msgstr "Ungültiger Benutzername oder Passwort"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:91
|
||||
@@ -7634,7 +7634,7 @@ msgstr ""
|
||||
#: src/view/shell/bottom-bar/BottomBarWeb.tsx:205
|
||||
#: src/view/shell/NavSignupCard.tsx:47
|
||||
#: src/view/shell/NavSignupCard.tsx:52
|
||||
msgid "Sign up"
|
||||
msgid "Create account"
|
||||
msgstr "Registrieren"
|
||||
|
||||
#: src/view/shell/NavSignupCard.tsx:47
|
||||
|
||||
@@ -3499,7 +3499,7 @@ msgstr "Μη έγκυρη ή μη υποστηριζόμενη εγγραφή α
|
||||
|
||||
#: src/screens/Login/LoginForm.tsx:90
|
||||
#: src/screens/Login/LoginForm.tsx:149
|
||||
msgid "Invalid username or password"
|
||||
msgid "Incorrect username or password"
|
||||
msgstr "Μη έγκυρο όνομα χρήστη ή κωδικός πρόσβασης"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:91
|
||||
@@ -6430,7 +6430,7 @@ msgstr "Αποσύνδεση;"
|
||||
#: src/view/shell/bottom-bar/BottomBarWeb.tsx:205
|
||||
#: src/view/shell/NavSignupCard.tsx:47
|
||||
#: src/view/shell/NavSignupCard.tsx:52
|
||||
msgid "Sign up"
|
||||
msgid "Create account"
|
||||
msgstr "Εγγραφή"
|
||||
|
||||
#: src/components/moderation/ScreenHider.tsx:91
|
||||
|
||||
@@ -3694,7 +3694,7 @@ msgstr ""
|
||||
|
||||
#: src/screens/Login/LoginForm.tsx:90
|
||||
#: src/screens/Login/LoginForm.tsx:149
|
||||
msgid "Invalid username or password"
|
||||
msgid "Incorrect username or password"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:91
|
||||
@@ -6861,7 +6861,7 @@ msgstr ""
|
||||
#: src/view/shell/bottom-bar/BottomBarWeb.tsx:205
|
||||
#: src/view/shell/NavSignupCard.tsx:47
|
||||
#: src/view/shell/NavSignupCard.tsx:52
|
||||
msgid "Sign up"
|
||||
msgid "Create account"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/moderation/ScreenHider.tsx:91
|
||||
|
||||
@@ -3694,7 +3694,7 @@ msgstr ""
|
||||
|
||||
#: src/screens/Login/LoginForm.tsx:90
|
||||
#: src/screens/Login/LoginForm.tsx:149
|
||||
msgid "Invalid username or password"
|
||||
msgid "Incorrect username or password"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:91
|
||||
@@ -6861,7 +6861,7 @@ msgstr ""
|
||||
#: src/view/shell/bottom-bar/BottomBarWeb.tsx:205
|
||||
#: src/view/shell/NavSignupCard.tsx:47
|
||||
#: src/view/shell/NavSignupCard.tsx:52
|
||||
msgid "Sign up"
|
||||
msgid "Create account"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/moderation/ScreenHider.tsx:91
|
||||
|
||||
+117
-117
@@ -1,6 +1,5 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"POT-Creation-Date: 2023-12-22 01:56+0530\n"
|
||||
msgstr "POT-Creation-Date: 2023-12-22 01:56+0530\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
@@ -9,13 +8,14 @@ msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: brodieavoult\n"
|
||||
"Last-Translator: hogaza\n"
|
||||
"Language-Team: brodieavoult\n"
|
||||
"Plural-Forms: \n"
|
||||
"Plural-Forms:\n"
|
||||
" \n"
|
||||
|
||||
#: src/components/ProgressGuide/FollowDialog.tsx:567
|
||||
msgid "(active)"
|
||||
msgstr ""
|
||||
msgstr "(activo)"
|
||||
|
||||
#: src/screens/Messages/components/ChatListItem.tsx:130
|
||||
msgid "(contains embedded content)"
|
||||
@@ -37,11 +37,11 @@ msgstr "{0, plural, one {# día} other {# días}}"
|
||||
|
||||
#: src/screens/Profile/ProfileFollowers.tsx:40
|
||||
msgid "{0, plural, one {# follower} other {# followers}}"
|
||||
msgstr ""
|
||||
msgstr "{0, plural, one {# seguidor} other {# seguidores}}"
|
||||
|
||||
#: src/screens/Profile/ProfileFollows.tsx:40
|
||||
msgid "{0, plural, one {# following} other {# following}}"
|
||||
msgstr ""
|
||||
msgstr "{0, plural, one {# siguiendo} other {# siguiendo}}"
|
||||
|
||||
#: src/lib/hooks/useTimeAgo.ts:146
|
||||
msgid "{0, plural, one {# hour} other {# hours}}"
|
||||
@@ -65,15 +65,15 @@ msgstr "{0, plural, one {# hora} other {# horas}}"
|
||||
|
||||
#: src/components/moderation/LabelsOnMe.tsx:53
|
||||
msgid "{0, plural, one {# label has} other {# labels have}} been placed on this account"
|
||||
msgstr ""
|
||||
msgstr "Se ha marcado esta cuenta con {0, plural, one {# etiqueta} other {# etiquetas}}"
|
||||
|
||||
#: src/components/moderation/LabelsOnMe.tsx:62
|
||||
msgid "{0, plural, one {# label has} other {# labels have}} been placed on this content"
|
||||
msgstr ""
|
||||
msgstr "Se ha marcado este contenido con {0, plural, one {# etiqueta} other {# etiquetas}}"
|
||||
|
||||
#: src/screens/Post/PostLikedBy.tsx:41
|
||||
msgid "{0, plural, one {# like} other {# likes}}"
|
||||
msgstr ""
|
||||
msgstr "{0, plural, one {# me gusta} other {# me gusta}}"
|
||||
|
||||
#: src/lib/hooks/useTimeAgo.ts:136
|
||||
msgid "{0, plural, one {# minute} other {# minutes}}"
|
||||
@@ -85,7 +85,7 @@ msgstr "{0, plural, one {# mes} other {# meses}}"
|
||||
|
||||
#: src/screens/Post/PostQuotes.tsx:41
|
||||
msgid "{0, plural, one {# quote} other {# quotes}}"
|
||||
msgstr ""
|
||||
msgstr "{0, plural, one {# cita} other {# citas}}"
|
||||
|
||||
#: src/screens/Post/PostRepostedBy.tsx:41
|
||||
msgid "{0, plural, one {# repost} other {# reposts}}"
|
||||
@@ -115,7 +115,7 @@ msgstr "{0, plural, one {siguiendo} other {siguiendo}}"
|
||||
|
||||
#: src/view/com/post-thread/PostThreadItem.tsx:447
|
||||
msgid "{0, plural, one {like} other {likes}}"
|
||||
msgstr "{0, plural, one {\"me gusta\"} other {\"me gusta\"}}"
|
||||
msgstr "{0, plural, one {me gusta} other {me gusta}}"
|
||||
|
||||
#: src/components/FeedCard.tsx:213
|
||||
#: src/view/com/feeds/FeedSourceCard.tsx:303
|
||||
@@ -264,11 +264,11 @@ msgstr "{firstAuthorLink} y <0>{additionalAuthorsCount, plural, one {{formattedA
|
||||
|
||||
#: src/view/com/notifications/NotificationFeedItem.tsx:327
|
||||
msgid "{firstAuthorLink} and <0>{additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}}</0> liked your custom feed"
|
||||
msgstr "{firstAuthorLink} y <0>{additionalAuthorsCount, plural, one {{formattedAuthorsCount} más} other {{formattedAuthorsCount} más}}</0> dieron \"me gusta\" a tu feed"
|
||||
msgstr "A {firstAuthorLink} y <0>{additionalAuthorsCount, plural, one {{formattedAuthorsCount} más} other {{formattedAuthorsCount} más}}</0> les gusta tu feed"
|
||||
|
||||
#: src/view/com/notifications/NotificationFeedItem.tsx:223
|
||||
msgid "{firstAuthorLink} and <0>{additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}}</0> liked your post"
|
||||
msgstr "{firstAuthorLink} y <0>{additionalAuthorsCount, plural, one {{formattedAuthorsCount} más} other {{formattedAuthorsCount} más}}</0> dieron \"me gusta\" a tu publicación"
|
||||
msgstr "A {firstAuthorLink} y <0>{additionalAuthorsCount, plural, one {{formattedAuthorsCount} más} other {{formattedAuthorsCount} más}}</0> les gusta tu publicación"
|
||||
|
||||
#: src/view/com/notifications/NotificationFeedItem.tsx:247
|
||||
msgid "{firstAuthorLink} and <0>{additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}}</0> reposted your post"
|
||||
@@ -288,11 +288,11 @@ msgstr "{firstAuthorLink} te siguió de vuelta"
|
||||
|
||||
#: src/view/com/notifications/NotificationFeedItem.tsx:339
|
||||
msgid "{firstAuthorLink} liked your custom feed"
|
||||
msgstr "{firstAuthorLink} dio \"me gusta\" a tu feed personalizado"
|
||||
msgstr "A {firstAuthorLink} le gusta tu feed personalizado"
|
||||
|
||||
#: src/view/com/notifications/NotificationFeedItem.tsx:235
|
||||
msgid "{firstAuthorLink} liked your post"
|
||||
msgstr "{firstAuthorLink} dio \"me gusta\" a tu publicación"
|
||||
msgstr "A {firstAuthorLink} le gusta tu publicación"
|
||||
|
||||
#: src/view/com/notifications/NotificationFeedItem.tsx:259
|
||||
msgid "{firstAuthorLink} reposted your post"
|
||||
@@ -308,11 +308,11 @@ msgstr "{firstAuthorName} y {formattedAuthorsCount} más te siguieron"
|
||||
|
||||
#: src/view/com/notifications/NotificationFeedItem.tsx:320
|
||||
msgid "{firstAuthorName} and {additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}} liked your custom feed"
|
||||
msgstr "{firstAuthorName} y {formattedAuthorsCount} más dieron \"me gusta\" a tu feed personalizado"
|
||||
msgstr "A {firstAuthorName} y {formattedAuthorsCount} más les gusta tu feed personalizado"
|
||||
|
||||
#: src/view/com/notifications/NotificationFeedItem.tsx:216
|
||||
msgid "{firstAuthorName} and {additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}} liked your post"
|
||||
msgstr "{firstAuthorName} y {formattedAuthorsCount} más dieron \"me gusta\" a tu publicación"
|
||||
msgstr "A {firstAuthorName} y {formattedAuthorsCount} les gusta tu publicación"
|
||||
|
||||
#: src/view/com/notifications/NotificationFeedItem.tsx:240
|
||||
msgid "{firstAuthorName} and {additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}} reposted your post"
|
||||
@@ -332,11 +332,11 @@ msgstr "{firstAuthorName} te siguió de vuelta"
|
||||
|
||||
#: src/view/com/notifications/NotificationFeedItem.tsx:325
|
||||
msgid "{firstAuthorName} liked your custom feed"
|
||||
msgstr "{firstAuthorName} dio \"me gusta\" a tu feed personalizado"
|
||||
msgstr "A {firstAuthorName} le gusta tu feed personalizado"
|
||||
|
||||
#: src/view/com/notifications/NotificationFeedItem.tsx:221
|
||||
msgid "{firstAuthorName} liked your post"
|
||||
msgstr "{firstAuthorName} dio \"me gusta\" a tu publicación"
|
||||
msgstr "A {firstAuthorName} le gusta tu publicación"
|
||||
|
||||
#: src/view/com/notifications/NotificationFeedItem.tsx:245
|
||||
msgid "{firstAuthorName} reposted your post"
|
||||
@@ -363,7 +363,7 @@ msgstr "No se puede enviar un mensaje a {handle}"
|
||||
|
||||
#: src/view/shell/bottom-bar/BottomBarWeb.tsx:260
|
||||
msgid "{notificationCount} unread items"
|
||||
msgstr ""
|
||||
msgstr "{notificationCount} elementos sin leer"
|
||||
|
||||
#: src/view/shell/Drawer.tsx:448
|
||||
msgid "{numUnreadNotifications} unread"
|
||||
@@ -532,16 +532,16 @@ msgstr "Cuenta seguida"
|
||||
|
||||
#: src/view/com/profile/ProfileMenu.tsx:108
|
||||
msgid "Account muted"
|
||||
msgstr "Cuenta muteada"
|
||||
msgstr "Cuenta silenciada"
|
||||
|
||||
#: src/components/moderation/ModerationDetailsDialog.tsx:101
|
||||
#: src/lib/moderation/useModerationCauseDescription.ts:96
|
||||
msgid "Account Muted"
|
||||
msgstr "Cuenta muteada"
|
||||
msgstr "Cuenta silenciada"
|
||||
|
||||
#: src/components/moderation/ModerationDetailsDialog.tsx:87
|
||||
msgid "Account Muted by List"
|
||||
msgstr "Cuenta muteada por lista"
|
||||
msgstr "Cuenta silenciada por lista"
|
||||
|
||||
#: src/screens/Settings/Settings.tsx:428
|
||||
msgid "Account options"
|
||||
@@ -645,7 +645,7 @@ msgstr "Agregar nueva publicación"
|
||||
#: src/view/screens/ProfileList.tsx:903
|
||||
#: src/view/screens/ProfileList.tsx:921
|
||||
msgid "Add people"
|
||||
msgstr ""
|
||||
msgstr "Añadir personas"
|
||||
|
||||
#: src/screens/StarterPack/Wizard/index.tsx:197
|
||||
#~ msgid "Add people to your starter pack that you think others will enjoy following"
|
||||
@@ -710,7 +710,7 @@ msgstr "El contenido para adultos solo se puede habilitar a través de la web en
|
||||
|
||||
#: src/components/moderation/LabelPreference.tsx:242
|
||||
msgid "Adult content is disabled."
|
||||
msgstr "El contenido adulto esta desactivado."
|
||||
msgstr "El contenido adulto está desactivado."
|
||||
|
||||
#: src/view/com/composer/labels/LabelsBtn.tsx:139
|
||||
#: src/view/com/composer/labels/LabelsBtn.tsx:197
|
||||
@@ -727,7 +727,7 @@ msgstr "Avanzado"
|
||||
|
||||
#: src/view/screens/Notifications.tsx:86
|
||||
msgid "All"
|
||||
msgstr ""
|
||||
msgstr "Todas"
|
||||
|
||||
#: src/screens/StarterPack/StarterPackScreen.tsx:381
|
||||
msgid "All accounts have been followed!"
|
||||
@@ -754,7 +754,7 @@ msgstr "Permitir nuevos mensajes de"
|
||||
|
||||
#: src/components/dialogs/PostInteractionSettingsDialog.tsx:314
|
||||
msgid "Allow quote posts"
|
||||
msgstr ""
|
||||
msgstr "Permitir citas"
|
||||
|
||||
#: src/components/dialogs/PostInteractionSettingsDialog.tsx:359
|
||||
msgid "Allow replies from:"
|
||||
@@ -933,7 +933,7 @@ msgstr "Cualquiera puede interactuar"
|
||||
#: src/screens/Settings/AppIconSettings/SettingsListItem.tsx:18
|
||||
#: src/screens/Settings/AppIconSettings/SettingsListItem.tsx:23
|
||||
msgid "App Icon"
|
||||
msgstr ""
|
||||
msgstr "Icono de la app"
|
||||
|
||||
#: src/screens/Settings/LanguageSettings.tsx:80
|
||||
msgid "App Language"
|
||||
@@ -1165,7 +1165,7 @@ msgstr "Antes de contactar a otro usuario, debes verificar tu correo electrónic
|
||||
|
||||
#: src/screens/Search/components/ExploreTrendingTopics.tsx:74
|
||||
msgid "BETA"
|
||||
msgstr ""
|
||||
msgstr "BETA"
|
||||
|
||||
#: src/components/dialogs/BirthDateSettings.tsx:106
|
||||
#: src/screens/Settings/AccountSettings.tsx:109
|
||||
@@ -1289,11 +1289,11 @@ msgstr "Bluesky no mostrará tu perfil o posts a usuarios que no hayan iniciado
|
||||
|
||||
#: src/screens/Settings/AppIconSettings/index.tsx:99
|
||||
msgid "Bluesky+"
|
||||
msgstr ""
|
||||
msgstr "Bluesky+"
|
||||
|
||||
#: src/screens/Settings/AppIconSettings/index.tsx:102
|
||||
msgid "Bluesky+ icons"
|
||||
msgstr ""
|
||||
msgstr "Iconos Bluesky+"
|
||||
|
||||
#: src/lib/moderation/useLabelBehaviorDescription.ts:53
|
||||
msgid "Blur images"
|
||||
@@ -1339,15 +1339,15 @@ msgstr "Explorar otros feeds"
|
||||
|
||||
#: src/components/TrendingTopics.tsx:176
|
||||
msgid "Browse posts about {displayName}"
|
||||
msgstr ""
|
||||
msgstr "Explorar posts sobre {displayName}"
|
||||
|
||||
#: src/components/TrendingTopics.tsx:184
|
||||
msgid "Browse posts tagged with {displayName}"
|
||||
msgstr ""
|
||||
msgstr "Explorar posts etiquetados con {displayName}"
|
||||
|
||||
#: src/components/TrendingTopics.tsx:222
|
||||
msgid "Browse topic {displayName}"
|
||||
msgstr ""
|
||||
msgstr "Explorar el tema {displayName}"
|
||||
|
||||
#: src/view/com/auth/SplashScreen.web.tsx:168
|
||||
msgid "Business"
|
||||
@@ -1371,7 +1371,7 @@ msgstr "Por {0}"
|
||||
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:447
|
||||
msgid "By <0>{0}</0>"
|
||||
msgstr ""
|
||||
msgstr "Por <0>{0}</0>"
|
||||
|
||||
#: src/screens/Signup/StepInfo/Policies.tsx:80
|
||||
#~ msgid "By creating an account you agree to the {els}."
|
||||
@@ -1504,12 +1504,12 @@ msgstr "Cambiar"
|
||||
|
||||
#: src/screens/Settings/AppIconSettings/index.tsx:39
|
||||
msgid "Change app icon"
|
||||
msgstr ""
|
||||
msgstr "Cambiar icono de la app"
|
||||
|
||||
#: src/screens/Settings/AppIconSettings/index.tsx:38
|
||||
#: src/screens/Settings/AppIconSettings/index.tsx:221
|
||||
msgid "Change app icon to \"{0}\""
|
||||
msgstr ""
|
||||
msgstr "Cambiar icono de la app a \"{0}\""
|
||||
|
||||
#: src/screens/Settings/AccountSettings.tsx:97
|
||||
#: src/screens/Settings/AccountSettings.tsx:101
|
||||
@@ -1578,7 +1578,7 @@ msgstr "Configuración de chat"
|
||||
|
||||
#: src/components/dms/ConvoMenu.tsx:84
|
||||
msgid "Chat unmuted"
|
||||
msgstr "Chat demuteado"
|
||||
msgstr "Chat no silenciado"
|
||||
|
||||
#: src/screens/SignupQueued.tsx:78
|
||||
#: src/screens/SignupQueued.tsx:82
|
||||
@@ -1649,7 +1649,7 @@ msgstr "Elige este color como tu avatar"
|
||||
#: src/view/screens/Feeds.tsx:728
|
||||
#: src/view/screens/Search/Explore.tsx:411
|
||||
msgid "Choose your own timeline! Feeds built by the community help you find content you love."
|
||||
msgstr ""
|
||||
msgstr "Elige tu propia cronología: encuentra el contenido que más te gusta gracias a los feeds de la comunidad."
|
||||
|
||||
#: src/screens/Signup/StepInfo/index.tsx:201
|
||||
msgid "Choose your password"
|
||||
@@ -1761,11 +1761,11 @@ msgstr "Cerrar ventana"
|
||||
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:137
|
||||
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:173
|
||||
msgid "Close emoji picker"
|
||||
msgstr ""
|
||||
msgstr "Cerrar el selector de emoji"
|
||||
|
||||
#: src/components/dialogs/GifSelect.tsx:169
|
||||
msgid "Close GIF dialog"
|
||||
msgstr "Cerrar ventana de GIF"
|
||||
msgstr "Cerrar la ventana de GIF"
|
||||
|
||||
#: src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx:31
|
||||
msgid "Close image"
|
||||
@@ -1934,7 +1934,7 @@ msgstr "Contactar a soporte"
|
||||
|
||||
#: src/screens/Settings/ContentAndMediaSettings.tsx:49
|
||||
msgid "Content & Media"
|
||||
msgstr ""
|
||||
msgstr "Contenido y medios"
|
||||
|
||||
#: src/screens/Settings/AccessibilitySettings.tsx:109
|
||||
#: src/screens/Settings/Settings.tsx:175
|
||||
@@ -2212,7 +2212,7 @@ msgstr "Creado {0}"
|
||||
|
||||
#: src/screens/List/ListHiddenScreen.tsx:127
|
||||
msgid "Creator has been blocked"
|
||||
msgstr ""
|
||||
msgstr "Se ha bloqueado al creador"
|
||||
|
||||
#: src/screens/Onboarding/index.tsx:26
|
||||
#: src/screens/Onboarding/state.ts:99
|
||||
@@ -2286,7 +2286,7 @@ msgstr "Por Defecto"
|
||||
|
||||
#: src/screens/Settings/AppIconSettings/index.tsx:75
|
||||
msgid "Default icons"
|
||||
msgstr ""
|
||||
msgstr "Iconos por defecto"
|
||||
|
||||
#: src/components/dms/MessageMenu.tsx:151
|
||||
#: src/screens/Settings/AppPasswords.tsx:212
|
||||
@@ -2720,7 +2720,7 @@ msgstr "Editar los detalles de la lista"
|
||||
|
||||
#: src/view/com/modals/CreateOrEditList.tsx:230
|
||||
msgid "Edit Moderation List"
|
||||
msgstr "Editar lista de Moderación"
|
||||
msgstr "Editar lista de moderación"
|
||||
|
||||
#: src/Navigation.tsx:296
|
||||
#: src/view/screens/Feeds.tsx:515
|
||||
@@ -2792,7 +2792,7 @@ msgstr "Educación"
|
||||
|
||||
#: src/screens/List/ListHiddenScreen.tsx:141
|
||||
msgid "Either the creator of this list has blocked you or you have blocked the creator."
|
||||
msgstr ""
|
||||
msgstr "La persona que creó esta lista te ha bloqueado, o tú la has bloqueado a ella."
|
||||
|
||||
#: src/screens/Settings/AccountSettings.tsx:60
|
||||
#: src/screens/Signup/StepInfo/index.tsx:170
|
||||
@@ -2910,7 +2910,7 @@ msgstr "Habilitar solo esta fuente"
|
||||
#: src/screens/Settings/ContentAndMediaSettings.tsx:122
|
||||
#: src/screens/Settings/ContentAndMediaSettings.tsx:136
|
||||
msgid "Enable trending topics"
|
||||
msgstr ""
|
||||
msgstr "Activar las tendencias"
|
||||
|
||||
#: src/screens/Messages/Settings.tsx:130
|
||||
#: src/screens/Messages/Settings.tsx:133
|
||||
@@ -3261,7 +3261,7 @@ msgstr "Feed por {0}"
|
||||
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:353
|
||||
msgid "Feed menu"
|
||||
msgstr ""
|
||||
msgstr "Menú de feed"
|
||||
|
||||
#: src/view/screens/Feeds.tsx:709
|
||||
#~ msgid "Feed offline"
|
||||
@@ -3308,7 +3308,7 @@ msgstr "¡Feeds actualizados!"
|
||||
|
||||
#: src/screens/Search/components/ExploreRecommendations.tsx:54
|
||||
msgid "Feeds we think you might like."
|
||||
msgstr ""
|
||||
msgstr "Creemos que estos estos feeds te gustarán."
|
||||
|
||||
#: src/view/com/modals/ChangeHandle.tsx:468
|
||||
#~ msgid "File Contents"
|
||||
@@ -3340,7 +3340,7 @@ msgstr "Buscar cuentas para seguir"
|
||||
#: src/components/ProgressGuide/FollowDialog.tsx:88
|
||||
#: src/components/ProgressGuide/FollowDialog.tsx:417
|
||||
msgid "Find people to follow"
|
||||
msgstr ""
|
||||
msgstr "Buscar a gente para seguir"
|
||||
|
||||
#: src/view/screens/Search/Search.tsx:595
|
||||
msgid "Find posts and users on Bluesky"
|
||||
@@ -3405,7 +3405,7 @@ msgstr "Seguir {name}"
|
||||
#: src/components/ProgressGuide/List.tsx:52
|
||||
#: src/state/shell/progress-guide.tsx:229
|
||||
msgid "Follow 10 accounts"
|
||||
msgstr ""
|
||||
msgstr "Sigue 10 cuentas"
|
||||
|
||||
#: src/components/ProgressGuide/List.tsx:69
|
||||
msgid "Follow 7 accounts"
|
||||
@@ -3856,13 +3856,13 @@ msgstr "¿Ocultar esta respuesta?"
|
||||
#: src/screens/Search/components/ExploreTrendingTopics.tsx:83
|
||||
#: src/view/shell/desktop/SidebarTrendingTopics.tsx:62
|
||||
msgid "Hide trending topics"
|
||||
msgstr ""
|
||||
msgstr "Ocultar las tendencias"
|
||||
|
||||
#: src/components/interstitials/Trending.tsx:99
|
||||
#: src/screens/Search/components/ExploreTrendingTopics.tsx:135
|
||||
#: src/view/shell/desktop/SidebarTrendingTopics.tsx:108
|
||||
msgid "Hide trending topics?"
|
||||
msgstr ""
|
||||
msgstr "¿Quieres ocultar las tendencias?"
|
||||
|
||||
#: src/view/com/notifications/NotificationFeedItem.tsx:592
|
||||
msgid "Hide user list"
|
||||
@@ -4085,7 +4085,7 @@ msgstr "Registro de publicación inválido o no compatible"
|
||||
|
||||
#: src/screens/Login/LoginForm.tsx:90
|
||||
#: src/screens/Login/LoginForm.tsx:149
|
||||
msgid "Invalid username or password"
|
||||
msgid "Incorrect username or password"
|
||||
msgstr "Nombre de usuario o contraseña no válidos"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:91
|
||||
@@ -4322,11 +4322,11 @@ msgstr "Claro"
|
||||
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:514
|
||||
msgid "Like"
|
||||
msgstr ""
|
||||
msgstr "Me gusta"
|
||||
|
||||
#: src/view/com/util/post-ctrls/PostCtrls.tsx:311
|
||||
msgid "Like ({0, plural, one {# like} other {# likes}})"
|
||||
msgstr ""
|
||||
msgstr "Like ({0, plural, one {# me gusta} other {# me gusta}})"
|
||||
|
||||
#: src/components/ProgressGuide/List.tsx:63
|
||||
msgid "Like 10 posts"
|
||||
@@ -4339,7 +4339,7 @@ msgstr "Dale \"me gusta\" a 10 publicaciones para entrenar el feed de Descubrimi
|
||||
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:501
|
||||
msgid "Like feed"
|
||||
msgstr ""
|
||||
msgstr "Me gusta este feed"
|
||||
|
||||
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:275
|
||||
msgid "Like this feed"
|
||||
@@ -4349,18 +4349,18 @@ msgstr "Dar \"me gusta\" a este feed"
|
||||
#: src/Navigation.tsx:236
|
||||
#: src/Navigation.tsx:241
|
||||
msgid "Liked by"
|
||||
msgstr "Le ha gustado a"
|
||||
msgstr "Le gusta a"
|
||||
|
||||
#: src/screens/Post/PostLikedBy.tsx:38
|
||||
#: src/screens/Profile/ProfileLabelerLikedBy.tsx:29
|
||||
#: src/view/screens/ProfileFeedLikedBy.tsx:30
|
||||
msgid "Liked By"
|
||||
msgstr "Le ha gustado a"
|
||||
msgstr "Le gusta a"
|
||||
|
||||
#: src/components/FeedCard.tsx:213
|
||||
#: src/view/com/feeds/FeedSourceCard.tsx:303
|
||||
msgid "Liked by {0, plural, one {# user} other {# users}}"
|
||||
msgstr ""
|
||||
msgstr "Le gusta a {0, plural, one {# usuario} other {# usuarios}}"
|
||||
|
||||
#: src/view/com/feeds/FeedSourceCard.tsx:268
|
||||
#~ msgid "Liked by {0} {1}"
|
||||
@@ -4375,7 +4375,7 @@ msgstr ""
|
||||
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:295
|
||||
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:309
|
||||
msgid "Liked by {likeCount, plural, one {# user} other {# users}}"
|
||||
msgstr ""
|
||||
msgstr "Le gusta a {likeCount, plural, one {# usuario} other {# usuarios}}"
|
||||
|
||||
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:287
|
||||
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:301
|
||||
@@ -4393,7 +4393,7 @@ msgstr ""
|
||||
|
||||
#: src/view/screens/Profile.tsx:234
|
||||
msgid "Likes"
|
||||
msgstr "\"Me gusta\""
|
||||
msgstr "Me gusta"
|
||||
|
||||
#: src/view/com/post-thread/PostThreadItem.tsx:217
|
||||
msgid "Likes on this post"
|
||||
@@ -4402,7 +4402,7 @@ msgstr "\"Me gusta\" en este post"
|
||||
#: src/view/com/post-thread/PostThread.tsx:626
|
||||
#: src/view/com/post-thread/PostThread.tsx:631
|
||||
msgid "Linear"
|
||||
msgstr ""
|
||||
msgstr "Lineal"
|
||||
|
||||
#: src/Navigation.tsx:198
|
||||
msgid "List"
|
||||
@@ -4423,11 +4423,11 @@ msgstr "Lista por {0}"
|
||||
|
||||
#: src/view/com/profile/ProfileSubpageHeader.tsx:156
|
||||
msgid "List by <0/>"
|
||||
msgstr ""
|
||||
msgstr "Lista de {0}"
|
||||
|
||||
#: src/view/com/profile/ProfileSubpageHeader.tsx:154
|
||||
msgid "List by you"
|
||||
msgstr ""
|
||||
msgstr "Lista tuya"
|
||||
|
||||
#: src/view/screens/ProfileList.tsx:455
|
||||
msgid "List deleted"
|
||||
@@ -4523,7 +4523,7 @@ msgstr "Acceder a una cuenta que no está en la lista"
|
||||
|
||||
#: src/view/shell/desktop/RightNav.tsx:121
|
||||
msgid "Logo by @sawaratsuki.bsky.social"
|
||||
msgstr ""
|
||||
msgstr "Logo de @sawaratsuki.bsky.social</0>"
|
||||
|
||||
#: src/view/shell/desktop/RightNav.tsx:103
|
||||
#~ msgid "Logo by <0/>"
|
||||
@@ -4532,7 +4532,7 @@ msgstr ""
|
||||
#: src/view/shell/desktop/RightNav.tsx:118
|
||||
#: src/view/shell/Drawer.tsx:629
|
||||
msgid "Logo by <0>@sawaratsuki.bsky.social</0>"
|
||||
msgstr "Logo por <0>@sawaratsuki.bsky.social</0>"
|
||||
msgstr "Logo de <0>@sawaratsuki.bsky.social</0>"
|
||||
|
||||
#: src/components/RichText.tsx:219
|
||||
msgid "Long press to open tag menu for #{tag}"
|
||||
@@ -4540,7 +4540,7 @@ msgstr "Mantén presionado para abrir el menú de etiquetas para #{tag}"
|
||||
|
||||
#: src/screens/Login/SetNewPasswordForm.tsx:110
|
||||
msgid "Looks like XXXXX-XXXXX"
|
||||
msgstr "Es como XXXXX-XXXXX"
|
||||
msgstr "Es algo así: XXXXX-XXXXX"
|
||||
|
||||
#: src/screens/Feeds/NoSavedFeedsOfAnyType.tsx:39
|
||||
msgid "Looks like you haven't saved any feeds! Use our recommendations or browse more below."
|
||||
@@ -4598,7 +4598,7 @@ msgstr "Usuarios mencionados"
|
||||
|
||||
#: src/view/screens/Notifications.tsx:99
|
||||
msgid "Mentions"
|
||||
msgstr ""
|
||||
msgstr "Menciones"
|
||||
|
||||
#: src/components/Menu/index.tsx:96
|
||||
#: src/view/screens/Search/Search.tsx:863
|
||||
@@ -4972,7 +4972,7 @@ msgstr "Nuevo nombre de usuario"
|
||||
#: src/view/screens/Lists.tsx:66
|
||||
#: src/view/screens/ModerationModlists.tsx:66
|
||||
msgid "New list"
|
||||
msgstr ""
|
||||
msgstr "Nueva lista"
|
||||
|
||||
#: src/components/dms/NewMessagesPill.tsx:92
|
||||
msgid "New messages"
|
||||
@@ -5394,31 +5394,31 @@ msgstr "Abrir el creador de avatares"
|
||||
|
||||
#: src/screens/Settings/AccountSettings.tsx:127
|
||||
msgid "Open change handle dialog"
|
||||
msgstr "Abrir diálogo de cambio de nombre de usuario"
|
||||
msgstr "Abrir el diálogo de cambio de nombre de usuario"
|
||||
|
||||
#: src/screens/Messages/components/ChatListItem.tsx:272
|
||||
#: src/screens/Messages/components/ChatListItem.tsx:273
|
||||
msgid "Open conversation options"
|
||||
msgstr "Abrir opciones de conversación"
|
||||
msgstr "Abrir las opciones de conversación"
|
||||
|
||||
#: src/components/Layout/Header/index.tsx:145
|
||||
msgid "Open drawer menu"
|
||||
msgstr ""
|
||||
msgstr "Abrir el menú lateral"
|
||||
|
||||
#: src/screens/Messages/components/MessageInput.web.tsx:181
|
||||
#: src/view/com/composer/Composer.tsx:1228
|
||||
#: src/view/com/composer/Composer.tsx:1229
|
||||
msgid "Open emoji picker"
|
||||
msgstr "Abrir selector de emoji"
|
||||
msgstr "Abrir el selector de emoji"
|
||||
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:190
|
||||
msgid "Open feed info screen"
|
||||
msgstr ""
|
||||
msgstr "Abrir la pantalla de información del feed"
|
||||
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:290
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:295
|
||||
msgid "Open feed options menu"
|
||||
msgstr "Abrir menú de opciones del feed"
|
||||
msgstr "Abrir el menú de opciones del feed"
|
||||
|
||||
#: src/screens/Settings/Settings.tsx:208
|
||||
msgid "Open helpdesk in browser"
|
||||
@@ -5745,7 +5745,7 @@ msgstr "Imágenes destinadas a adultos."
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:519
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:526
|
||||
msgid "Pin feed"
|
||||
msgstr ""
|
||||
msgstr "Anclar el feed"
|
||||
|
||||
#: src/view/screens/ProfileList.tsx:685
|
||||
msgid "Pin to home"
|
||||
@@ -5767,7 +5767,7 @@ msgstr "Anclado"
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:162
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:174
|
||||
msgid "Pinned {0} to Home"
|
||||
msgstr ""
|
||||
msgstr "{0} anclados en inicio"
|
||||
|
||||
#: src/view/screens/SavedFeeds.tsx:123
|
||||
msgid "Pinned Feeds"
|
||||
@@ -6097,11 +6097,11 @@ msgstr "Público"
|
||||
|
||||
#: src/view/com/lists/MyLists.tsx:77
|
||||
msgid "Public, sharable lists of users to mute or block in bulk."
|
||||
msgstr ""
|
||||
msgstr "Listas públicas y compartibles para bloquear o silenciar a usuarios en grupo."
|
||||
|
||||
#: src/view/com/lists/MyLists.tsx:72
|
||||
msgid "Public, sharable lists which can be used to drive feeds."
|
||||
msgstr ""
|
||||
msgstr "Listas públicas y compartibles que se pueden usar para impulsar feeds."
|
||||
|
||||
#: src/view/screens/ModerationModlists.tsx:75
|
||||
#~ msgid "Public, shareable lists of users to mute or block in bulk."
|
||||
@@ -6230,7 +6230,7 @@ msgstr "Búsquedas Recientes"
|
||||
|
||||
#: src/screens/Search/components/ExploreRecommendations.tsx:50
|
||||
msgid "Recommended"
|
||||
msgstr ""
|
||||
msgstr "Recomendaciones"
|
||||
|
||||
#: src/screens/Messages/components/MessageListError.tsx:20
|
||||
msgid "Reconnect"
|
||||
@@ -6426,7 +6426,7 @@ msgstr "Responder"
|
||||
|
||||
#: src/view/com/util/post-ctrls/PostCtrls.tsx:263
|
||||
msgid "Reply ({0, plural, one {# reply} other {# replies}})"
|
||||
msgstr ""
|
||||
msgstr "Responder ({0, plural, one {# respuesta} other {# respuestas}})"
|
||||
|
||||
#: src/view/screens/PreferencesFollowingFeed.tsx:142
|
||||
#~ msgid "Reply Filters"
|
||||
@@ -6452,7 +6452,7 @@ msgstr "La configuración de respuestas es elegida por el autor del hilo"
|
||||
|
||||
#: src/view/com/post-thread/PostThread.tsx:648
|
||||
msgid "Reply sorting"
|
||||
msgstr ""
|
||||
msgstr "Orden de las respuestas"
|
||||
|
||||
#: src/view/com/post/Post.tsx:204
|
||||
#: src/view/com/posts/PostFeedItem.tsx:553
|
||||
@@ -6576,7 +6576,7 @@ msgstr "Republicar"
|
||||
|
||||
#: src/view/com/util/post-ctrls/RepostButton.tsx:74
|
||||
msgid "Repost ({0, plural, one {# repost} other {# reposts}})"
|
||||
msgstr ""
|
||||
msgstr "Republicar ({0, plural, one {# repost} other {# reposts}})"
|
||||
|
||||
#: src/screens/StarterPack/StarterPackScreen.tsx:548
|
||||
#: src/view/com/util/post-ctrls/RepostButton.tsx:148
|
||||
@@ -6860,15 +6860,15 @@ msgstr "Buscar"
|
||||
|
||||
#: src/components/ProgressGuide/FollowDialog.tsx:745
|
||||
msgid "Search by name or interest"
|
||||
msgstr ""
|
||||
msgstr "Buscar por nombre o interés"
|
||||
|
||||
#: src/view/screens/Feeds.tsx:441
|
||||
msgid "Search feeds"
|
||||
msgstr ""
|
||||
msgstr "Buscar feeds"
|
||||
|
||||
#: src/components/ProgressGuide/FollowDialog.tsx:575
|
||||
msgid "Search for \"{interestsDisplayName}\"{activeText}"
|
||||
msgstr ""
|
||||
msgstr "Buscar \"{interestsDisplayName}\"{activeText}"
|
||||
|
||||
#: src/view/shell/desktop/Search.tsx:201
|
||||
msgid "Search for \"{query}\""
|
||||
@@ -7148,7 +7148,7 @@ msgstr "Dirección del servidor"
|
||||
|
||||
#: src/screens/Settings/AppIconSettings/index.tsx:178
|
||||
msgid "Set app icon to {0}"
|
||||
msgstr ""
|
||||
msgstr "Cambiar el icono de la app a {0}"
|
||||
|
||||
#: src/screens/Moderation/index.tsx:290
|
||||
msgid "Set birthdate"
|
||||
@@ -7305,7 +7305,7 @@ msgstr "Compartir código QR"
|
||||
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:470
|
||||
msgid "Share this feed"
|
||||
msgstr ""
|
||||
msgstr "Compartir este feed"
|
||||
|
||||
#: src/screens/StarterPack/StarterPackScreen.tsx:416
|
||||
msgid "Share this starter pack"
|
||||
@@ -7436,11 +7436,11 @@ msgstr "Mostrar respuestas"
|
||||
|
||||
#: src/view/com/post-thread/PostThread.tsx:622
|
||||
msgid "Show replies as"
|
||||
msgstr ""
|
||||
msgstr "Mostrar las respuestas como"
|
||||
|
||||
#: src/screens/Settings/ThreadPreferences.tsx:151
|
||||
msgid "Show replies as threaded"
|
||||
msgstr ""
|
||||
msgstr "Mostrar las respuestas como hilos"
|
||||
|
||||
#: src/screens/Settings/ThreadPreferences.tsx:126
|
||||
msgid "Show replies by people you follow before all other replies"
|
||||
@@ -7569,8 +7569,8 @@ msgstr "¿Cerrar sesión?"
|
||||
#: src/view/shell/bottom-bar/BottomBarWeb.tsx:205
|
||||
#: src/view/shell/NavSignupCard.tsx:47
|
||||
#: src/view/shell/NavSignupCard.tsx:52
|
||||
msgid "Sign up"
|
||||
msgstr "Crear cuenta"
|
||||
msgid "Create account"
|
||||
msgstr "Crear una cuenta"
|
||||
|
||||
#: src/view/shell/NavSignupCard.tsx:47
|
||||
#~ msgid "Sign up or sign in to join the conversation"
|
||||
@@ -7655,7 +7655,7 @@ msgstr "¡Se produjo un error!"
|
||||
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:538
|
||||
msgid "Something wrong? Let us know."
|
||||
msgstr ""
|
||||
msgstr "¿Algo va mal? Avísanos."
|
||||
|
||||
#: src/App.native.tsx:122
|
||||
#: src/App.web.tsx:97
|
||||
@@ -7719,12 +7719,12 @@ msgstr "Iniciar un nuevo chat"
|
||||
#: src/view/screens/ProfileList.tsx:817
|
||||
#: src/view/screens/ProfileList.tsx:938
|
||||
msgid "Start adding people"
|
||||
msgstr ""
|
||||
msgstr "Empieza a añadir personas"
|
||||
|
||||
#: src/view/screens/ProfileList.tsx:824
|
||||
#: src/view/screens/ProfileList.tsx:945
|
||||
msgid "Start adding people!"
|
||||
msgstr ""
|
||||
msgstr "¡Empieza a añadir personas!"
|
||||
|
||||
#: src/components/dms/dialogs/SearchablePeopleList.tsx:353
|
||||
msgid "Start chat with {displayName}"
|
||||
@@ -7750,7 +7750,7 @@ msgstr "Paquete de inicio de {0}"
|
||||
|
||||
#: src/view/com/profile/ProfileSubpageHeader.tsx:182
|
||||
msgid "Starter pack by <0/>"
|
||||
msgstr ""
|
||||
msgstr "Paquete de inicio de <0/>"
|
||||
|
||||
#: src/components/StarterPack/StarterPackCard.tsx:80
|
||||
#: src/view/com/profile/ProfileSubpageHeader.tsx:180
|
||||
@@ -7902,12 +7902,12 @@ msgstr "Solo etiquetas"
|
||||
|
||||
#: src/screens/Settings/AppIconSettings/index.tsx:216
|
||||
msgid "Tap to change app icon"
|
||||
msgstr ""
|
||||
msgstr "Toca para cambiar el icono de la app"
|
||||
|
||||
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:138
|
||||
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:174
|
||||
msgid "Tap to close the emoji picker"
|
||||
msgstr ""
|
||||
msgstr "Toca para cerrar el selector de emoji"
|
||||
|
||||
#: src/components/ProgressGuide/Toast.tsx:156
|
||||
msgid "Tap to dismiss"
|
||||
@@ -7936,11 +7936,11 @@ msgstr "Toca para ver la imagen completa"
|
||||
|
||||
#: src/state/shell/progress-guide.tsx:233
|
||||
msgid "Task complete - 10 follows!"
|
||||
msgstr ""
|
||||
msgstr "¡Tarea completada: 10 cuentas seguidas!"
|
||||
|
||||
#: src/state/shell/progress-guide.tsx:223
|
||||
msgid "Task complete - 10 likes!"
|
||||
msgstr "¡Tarea completada - 10 \"me gusta\"!"
|
||||
msgstr "¡Tarea completada: 10 me gusta!"
|
||||
|
||||
#: src/components/ProgressGuide/List.tsx:64
|
||||
msgid "Teach our algorithm what you like"
|
||||
@@ -8051,7 +8051,7 @@ msgstr "La cuenta podrá interactuar contigo tras desbloquearla."
|
||||
#: src/screens/Settings/AppIconSettings/index.tsx:41
|
||||
#: src/screens/Settings/AppIconSettings/index.tsx:222
|
||||
msgid "The app will be restarted"
|
||||
msgstr ""
|
||||
msgstr "Se reiniciará la app"
|
||||
|
||||
#: src/components/moderation/ModerationDetailsDialog.tsx:127
|
||||
#~ msgid "the author"
|
||||
@@ -8417,7 +8417,7 @@ msgstr "Esta lista - creada por <0>{0}</0> - contiene posibles violaciones de la
|
||||
|
||||
#: src/view/screens/ProfileList.tsx:933
|
||||
msgid "This list is empty."
|
||||
msgstr ""
|
||||
msgstr "Esta lista está vacía"
|
||||
|
||||
#: src/screens/Profile/ErrorState.tsx:40
|
||||
msgid "This moderation service is unavailable. See below for more details. If this issue persists, contact us."
|
||||
@@ -8524,7 +8524,7 @@ msgstr "Esto eliminará tu publicación de esta cita para todos los usuarios y l
|
||||
#: src/view/com/post-thread/PostThread.tsx:606
|
||||
#: src/view/com/post-thread/PostThread.tsx:609
|
||||
msgid "Thread options"
|
||||
msgstr ""
|
||||
msgstr "Opciones de hilos"
|
||||
|
||||
#: src/screens/Settings/ContentAndMediaSettings.tsx:66
|
||||
#: src/screens/Settings/ContentAndMediaSettings.tsx:69
|
||||
@@ -8542,7 +8542,7 @@ msgstr "Preferencias de hilos"
|
||||
#: src/view/com/post-thread/PostThread.tsx:636
|
||||
#: src/view/com/post-thread/PostThread.tsx:641
|
||||
msgid "Threaded"
|
||||
msgstr ""
|
||||
msgstr "En hilos"
|
||||
|
||||
#: src/screens/Settings/ThreadPreferences.tsx:142
|
||||
msgid "Threaded mode"
|
||||
@@ -8604,7 +8604,7 @@ msgstr "Top"
|
||||
|
||||
#: src/Navigation.tsx:383
|
||||
msgid "Topic"
|
||||
msgstr ""
|
||||
msgstr "Tema"
|
||||
|
||||
#: src/view/com/modals/EditImage.tsx:272
|
||||
#~ msgid "Transformations"
|
||||
@@ -8622,7 +8622,7 @@ msgstr "Traducir"
|
||||
#: src/screens/Search/components/ExploreTrendingTopics.tsx:69
|
||||
#: src/view/shell/desktop/SidebarTrendingTopics.tsx:59
|
||||
msgid "Trending"
|
||||
msgstr ""
|
||||
msgstr "Tendencias"
|
||||
|
||||
#: src/view/com/util/error/ErrorScreen.tsx:103
|
||||
msgctxt "action"
|
||||
@@ -8716,7 +8716,7 @@ msgstr "Deshacer repost"
|
||||
|
||||
#: src/view/com/util/post-ctrls/RepostButton.tsx:68
|
||||
msgid "Undo repost ({0, plural, one {# repost} other {# reposts}})"
|
||||
msgstr ""
|
||||
msgstr "Deshacer republicación ({0, plural, one {# repost} other {# reposts}})"
|
||||
|
||||
#: src/view/com/profile/FollowButton.tsx:60
|
||||
msgctxt "action"
|
||||
@@ -8742,7 +8742,7 @@ msgstr "No me gusta"
|
||||
|
||||
#: src/view/com/util/post-ctrls/PostCtrls.tsx:305
|
||||
msgid "Unlike ({0, plural, one {# like} other {# likes}})"
|
||||
msgstr ""
|
||||
msgstr "No me gusta ({0, plural, one {# me gusta} other {# me gusta}})"
|
||||
|
||||
#: src/view/screens/ProfileFeed.tsx:576
|
||||
#~ msgid "Unlike this feed"
|
||||
@@ -8800,7 +8800,7 @@ msgstr "Desfijar"
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:519
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:526
|
||||
msgid "Unpin feed"
|
||||
msgstr ""
|
||||
msgstr "Desfijar feed"
|
||||
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:309
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:311
|
||||
@@ -8818,7 +8818,7 @@ msgstr "Desfijar lista de moderación"
|
||||
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:164
|
||||
msgid "Unpinned {0} from Home"
|
||||
msgstr ""
|
||||
msgstr "{0} desfijado de inicio"
|
||||
|
||||
#: src/view/screens/ProfileList.tsx:352
|
||||
msgid "Unpinned from your feeds"
|
||||
@@ -9284,7 +9284,7 @@ msgstr "No pudimos encontrar resultados para ese hashtag."
|
||||
|
||||
#: src/screens/Topic.tsx:178
|
||||
msgid "We couldn't find any results for that topic."
|
||||
msgstr ""
|
||||
msgstr "No hemos encontrado resultados para ese tema."
|
||||
|
||||
#: src/screens/Messages/Conversation.tsx:103
|
||||
msgid "We couldn't load this conversation"
|
||||
@@ -9398,7 +9398,7 @@ msgstr "¿Cómo quieres llamar a tu paquete de inicio?"
|
||||
|
||||
#: src/screens/Search/components/ExploreTrendingTopics.tsx:79
|
||||
msgid "What people are posting about."
|
||||
msgstr ""
|
||||
msgstr "Los temas del momento."
|
||||
|
||||
#: src/view/com/auth/SplashScreen.tsx:38
|
||||
#: src/view/com/auth/SplashScreen.web.tsx:99
|
||||
@@ -9586,7 +9586,7 @@ msgstr "Puedes reactivar tu cuenta para continuar iniciando sesión. Tu perfil y
|
||||
#: src/screens/Search/components/ExploreTrendingTopics.tsx:136
|
||||
#: src/view/shell/desktop/SidebarTrendingTopics.tsx:109
|
||||
msgid "You can update this later from your settings."
|
||||
msgstr ""
|
||||
msgstr "Puedes cambiar esta opción en Ajustes."
|
||||
|
||||
#: src/view/com/profile/ProfileFollowers.tsx:94
|
||||
msgid "You do not have any followers."
|
||||
@@ -9832,7 +9832,7 @@ msgstr "Has elegido ocultar una palabra o etiqueta dentro de esta publicación."
|
||||
|
||||
#: src/state/shell/progress-guide.tsx:234
|
||||
msgid "You've found some people to follow"
|
||||
msgstr ""
|
||||
msgstr "Has encontrado algunas personas a las que seguir"
|
||||
|
||||
#: src/view/com/posts/FollowingEndOfFeed.tsx:44
|
||||
msgid "You've reached the end of your feed! Find some more accounts to follow."
|
||||
@@ -9880,7 +9880,7 @@ msgstr "Tu elección será guardada. Puedes cambiar esto en los ajustes luego."
|
||||
|
||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:496
|
||||
msgid "Your current handle <0>{0}</0> will automatically remain reserved for you. You can switch back to it at any time from this account."
|
||||
msgstr ""
|
||||
msgstr "Tu nombre de usuario actual, <0>{0}</0>, quedará automáticamente reservado para ti. Puedes volver a él cuando quieras desde esta cuenta."
|
||||
|
||||
#: src/screens/Onboarding/StepFollowingFeed.tsx:62
|
||||
#~ msgid "Your default feed is \"Following\""
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1437,7 +1437,7 @@ msgstr "Sulje valintaikkuna"
|
||||
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:137
|
||||
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:173
|
||||
msgid "Close emoji picker"
|
||||
msgstr ""
|
||||
msgstr "Sulje emojinvalitsin"
|
||||
|
||||
#: src/components/dialogs/GifSelect.tsx:169
|
||||
msgid "Close GIF dialog"
|
||||
@@ -1823,7 +1823,7 @@ msgstr "Luotu {0}"
|
||||
|
||||
#: src/screens/List/ListHiddenScreen.tsx:127
|
||||
msgid "Creator has been blocked"
|
||||
msgstr ""
|
||||
msgstr "Tekijä on estetty"
|
||||
|
||||
#: src/screens/Onboarding/index.tsx:26
|
||||
#: src/screens/Onboarding/state.ts:99
|
||||
@@ -2321,7 +2321,7 @@ msgstr "Koulutus"
|
||||
|
||||
#: src/screens/List/ListHiddenScreen.tsx:141
|
||||
msgid "Either the creator of this list has blocked you or you have blocked the creator."
|
||||
msgstr ""
|
||||
msgstr "Joko listan tekijä on estänyt sinut tai sitten sinä olet estänyt listan tekijän."
|
||||
|
||||
#: src/screens/Settings/AccountSettings.tsx:60
|
||||
#: src/screens/Signup/StepInfo/index.tsx:170
|
||||
@@ -3409,8 +3409,8 @@ msgstr "Virheellinen tai ei-tuettu julkaisutietue"
|
||||
|
||||
#: src/screens/Login/LoginForm.tsx:90
|
||||
#: src/screens/Login/LoginForm.tsx:149
|
||||
msgid "Invalid username or password"
|
||||
msgstr "Virheellinen käyttäjätunnus tai salasana"
|
||||
msgid "Incorrect username or password"
|
||||
msgstr "Väärä käyttäjätunnus tai salasana"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:91
|
||||
msgid "Invalid Verification Code"
|
||||
@@ -4549,7 +4549,7 @@ msgstr "Hups!"
|
||||
|
||||
#: src/screens/Onboarding/StepFinished.tsx:260
|
||||
msgid "Open"
|
||||
msgstr "Avaa"
|
||||
msgstr "Avoin"
|
||||
|
||||
#: src/view/com/posts/AviFollowButton.tsx:86
|
||||
msgid "Open {name} profile shortcut menu"
|
||||
@@ -4576,7 +4576,7 @@ msgstr "Avaa alavalikko"
|
||||
#: src/view/com/composer/Composer.tsx:1228
|
||||
#: src/view/com/composer/Composer.tsx:1229
|
||||
msgid "Open emoji picker"
|
||||
msgstr "Avaa emoji-valitsin"
|
||||
msgstr "Avaa emojinvalitsin"
|
||||
|
||||
#: src/screens/Profile/components/ProfileFeedHeader.tsx:190
|
||||
msgid "Open feed info screen"
|
||||
@@ -6282,8 +6282,8 @@ msgstr "Kirjaudutaanko ulos?"
|
||||
#: src/view/shell/bottom-bar/BottomBarWeb.tsx:205
|
||||
#: src/view/shell/NavSignupCard.tsx:47
|
||||
#: src/view/shell/NavSignupCard.tsx:52
|
||||
msgid "Sign up"
|
||||
msgstr "Rekisteröidy"
|
||||
msgid "Create account"
|
||||
msgstr "Luo tili"
|
||||
|
||||
#: src/components/moderation/ScreenHider.tsx:91
|
||||
#: src/lib/moderation/useGlobalLabelStrings.ts:28
|
||||
@@ -6543,7 +6543,7 @@ msgstr "Napauta vaihtaaksesi sovelluskuvakkeen"
|
||||
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:138
|
||||
#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:174
|
||||
msgid "Tap to close the emoji picker"
|
||||
msgstr ""
|
||||
msgstr "Napauta sulkeaksesi emojinvalitsimen"
|
||||
|
||||
#: src/components/ProgressGuide/Toast.tsx:156
|
||||
msgid "Tap to dismiss"
|
||||
|
||||
+841
-624
File diff suppressed because it is too large
Load Diff
+782
-794
File diff suppressed because it is too large
Load Diff
@@ -4085,7 +4085,7 @@ msgstr "Rexistro de chío inválido ou non compatíbel"
|
||||
|
||||
#: src/screens/Login/LoginForm.tsx:90
|
||||
#: src/screens/Login/LoginForm.tsx:149
|
||||
msgid "Invalid username or password"
|
||||
msgid "Incorrect username or password"
|
||||
msgstr "Alcume ou contrasinal incorrectos"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:91
|
||||
@@ -7574,7 +7574,7 @@ msgstr ""
|
||||
#: src/view/shell/bottom-bar/BottomBarWeb.tsx:205
|
||||
#: src/view/shell/NavSignupCard.tsx:47
|
||||
#: src/view/shell/NavSignupCard.tsx:52
|
||||
msgid "Sign up"
|
||||
msgid "Create account"
|
||||
msgstr "Crear conta"
|
||||
|
||||
#: src/view/shell/NavSignupCard.tsx:47
|
||||
|
||||
@@ -3965,7 +3965,7 @@ msgstr "अमान्य या असमर्थित पोस्ट र
|
||||
|
||||
#: src/screens/Login/LoginForm.tsx:90
|
||||
#: src/screens/Login/LoginForm.tsx:149
|
||||
msgid "Invalid username or password"
|
||||
msgid "Incorrect username or password"
|
||||
msgstr "अमान्य उपयोगकर्ता नाम या पासवर्ड"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:91
|
||||
@@ -7248,7 +7248,7 @@ msgstr "साइन आउट करें?"
|
||||
#: src/view/shell/bottom-bar/BottomBarWeb.tsx:205
|
||||
#: src/view/shell/NavSignupCard.tsx:47
|
||||
#: src/view/shell/NavSignupCard.tsx:52
|
||||
msgid "Sign up"
|
||||
msgid "Create account"
|
||||
msgstr "साइन अप करें"
|
||||
|
||||
#: src/components/moderation/ScreenHider.tsx:91
|
||||
|
||||
+2279
-1603
File diff suppressed because it is too large
Load Diff
@@ -4196,7 +4196,7 @@ msgstr "Catatan postingan tidak valid atau tidak didukung"
|
||||
|
||||
#: src/screens/Login/LoginForm.tsx:90
|
||||
#: src/screens/Login/LoginForm.tsx:149
|
||||
msgid "Invalid username or password"
|
||||
msgid "Incorrect username or password"
|
||||
msgstr "Nama pengguna atau kata sandi salah"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:91
|
||||
@@ -7717,7 +7717,7 @@ msgstr ""
|
||||
#: src/view/shell/bottom-bar/BottomBarWeb.tsx:205
|
||||
#: src/view/shell/NavSignupCard.tsx:47
|
||||
#: src/view/shell/NavSignupCard.tsx:52
|
||||
msgid "Sign up"
|
||||
msgid "Create account"
|
||||
msgstr "Daftar"
|
||||
|
||||
#: src/view/shell/NavSignupCard.tsx:47
|
||||
|
||||
+871
-654
File diff suppressed because it is too large
Load Diff
+839
-622
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user