Merge remote-tracking branch 'origin/main' into samuel/alf-login

zio/stable
Samuel Newman 2024-03-19 19:51:35 +00:00
commit 4794ab6b9a
83 changed files with 4447 additions and 4712 deletions

View File

@ -0,0 +1,55 @@
---
name: Bundle and Deploy EAS Update
on:
workflow_dispatch:
inputs:
runtimeVersion:
type: string
description: Runtime version (in x.x.x format) that this update is for
required: true
jobs:
bundleDeploy:
name: Bundle and Deploy EAS Update
runs-on: ubuntu-latest
steps:
- name: 🧐 Validate version
run: |
[[ "${{ github.event.inputs.runtimeVersion }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && echo "Version is valid" || exit 1
- name: ⬇️ Checkout
uses: actions/checkout@v4
- name: 🔧 Setup Node
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: yarn
- name: ⚙️ Install Dependencies
run: yarn install
- name: 🪛 Install jq
uses: dcarbone/install-jq-action@v2
- name: ⛏️ Setup Expo
run: yarn global add eas-cli-local-build-plugin
- name: 🔤 Compile Translations
run: yarn intl:build
- name: ✏️ Write environment variables
run: |
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "$json" > google-services.json
- name: 🏗️ Create Bundle
run: yarn export
- name: 📦 Package Bundle and 🚀 Deploy
run: yarn make-deploy-bundle
env:
DENIS_API_KEY: ${{ secrets.DENIS_API_KEY }}
RUNTIME_VERSION: ${{ github.event.inputs.runtimeVersion }}

View File

@ -4,6 +4,7 @@ import {
linkRequiresWarning,
isPossiblyAUrl,
splitApexDomain,
isTrustedUrl,
} from '../../../src/lib/strings/url-helpers'
describe('linkRequiresWarning', () => {
@ -74,6 +75,10 @@ describe('linkRequiresWarning', () => {
// bad uri inputs, default to true
['', '', true],
['example.com', 'example.com', true],
['/profile', 'Username', false],
['#', 'Show More', false],
['https://docs.bsky.app', 'https://docs.bsky.app', false],
['https://bsky.app/compose/intent?text=test', 'Compose a post', false],
]
it.each(cases)(
@ -139,3 +144,36 @@ describe('splitApexDomain', () => {
},
)
})
describe('isTrustedUrl', () => {
const cases = [
['#', true],
['#profile', true],
['/', true],
['/profile', true],
['/profile/', true],
['/profile/bob.test', true],
['https://bsky.app', true],
['https://bsky.app/', true],
['https://bsky.app/profile/bob.test', true],
['https://www.bsky.app', true],
['https://www.bsky.app/', true],
['https://docs.bsky.app', true],
['https://bsky.social', true],
['https://bsky.social/blog', true],
['https://blueskyweb.xyz', true],
['https://blueskyweb.zendesk.com', true],
['http://bsky.app', true],
['http://bsky.social', true],
['http://blueskyweb.xyz', true],
['http://blueskyweb.zendesk.com', true],
['https://google.com', false],
['https://docs.google.com', false],
['https://google.com/#', false],
]
it.each(cases)('given input uri %p, returns %p', (str, expected) => {
const output = isTrustedUrl(str)
expect(output).toEqual(expected)
})
})

View File

@ -0,0 +1,18 @@
-----BEGIN CERTIFICATE-----
MIIC0TCCAbmgAwIBAgIJcMN2yt5KNDqTMA0GCSqGSIb3DQEBCwUAMBIxEDAOBgNV
BAMTB0JsdWVza3kwHhcNMjQwMzE0MDA1OTU4WhcNMzQwMzE0MDA1OTU4WjASMRAw
DgYDVQQDEwdCbHVlc2t5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
izSAWEc3wRoa3eTBEh/kE9pH0d6jhEGw9GrYfei60MHT1pSq2cTdyUM1yUZchAeW
gFFtqFxX0pfIZQyMlIZbjkaOxOqzWhB0aCsxngnhbSahFwRxkVwTAuonhqIpaLBL
hrCCCQ2IfZUpy8QeasqlTlmvmijuCC34fXxJlxNcj8SqzIZi+civ7U5PMPfIMMnD
tCDIBy1vxMk57m25X2ikcWUFW64qNVLkFAL36xEnmFTL4Ivqpz23gUcUIe1zbesY
jAgDtlwnAE7mU3oagCUDcSuOveT4POhT35Xp3Y/07I68kmXtrPxwd5k0L0zbisEm
poKZ87E2X29BitihicMpBwIDAQABoyowKDAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0l
AQH/BAwwCgYIKwYBBQUHAwMwDQYJKoZIhvcNAQELBQADggEBAED1gdMF0yr8Gy87
RgyaeVpPySwSsO0selmXXrcmOWgiPA05lubyhFEa4P5kdzBEByG2MT+pJkjGYpvK
XRnqXM5VvdS2RhYYFH0cFOIUqBKwCnzViCMuGQeoGUx4oPcKFS0PQ1WjW2d4pS75
51GBfB6LOepsCHUG0A9XEk7EAyUWc4M2ITCJsTtJh8CVn2pTks2q14ETDs86YQv4
peDaJv8nhIe8oQkeGn2o/P/ctkwJg/uBydQUsWgjjGTQZTilVjGTW1mwDr9FucAE
d5gKIk4rtR/3Zd/NDdqp8PrkoWeVM7Hwr789/mpUOeqa/j7YNkDYQh7x+M/odd1D
KY0bQEQ=
-----END CERTIFICATE-----

View File

@ -41,10 +41,12 @@
"intl:extract": "lingui extract",
"intl:compile": "lingui compile",
"nuke": "rm -rf ./node_modules && rm -rf ./ios && rm -rf ./android",
"update-extensions": "scripts/updateExtensions.sh"
"update-extensions": "bash scripts/updateExtensions.sh",
"export": "npx expo export",
"make-deploy-bundle": "bash scripts/bundleUpdate.sh"
},
"dependencies": {
"@atproto/api": "^0.12.0",
"@atproto/api": "^0.12.1",
"@bam.tech/react-native-image-resizer": "^3.0.4",
"@braintree/sanitize-url": "^6.0.2",
"@emoji-mart/react": "^1.1.1",

View File

@ -1,8 +1,56 @@
diff --git a/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/ImagePickerModule.kt b/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/ImagePickerModule.kt
index 3f50f8c..ee47fa1 100644
--- a/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/ImagePickerModule.kt
+++ b/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/ImagePickerModule.kt
@@ -33,7 +33,9 @@ import kotlin.coroutines.resumeWithException
// TODO(@bbarthec): rename to ExpoImagePicker
private const val moduleName = "ExponentImagePicker"
+
class ImagePickerModule : Module() {
+ private var isPickerOpen = false
override fun definition() = ModuleDefinition {
Name(moduleName)
@@ -129,6 +131,11 @@ class ImagePickerModule : Module() {
options: ImagePickerOptions
): Any {
return try {
+ if(isPickerOpen) {
+ return ImagePickerResponse(canceled = true)
+ }
+
+ isPickerOpen = true
var result = launchPicker(pickerLauncher)
if (
!options.allowsMultipleSelection &&
@@ -143,6 +150,8 @@ class ImagePickerModule : Module() {
mediaHandler.readExtras(result.data, options)
} catch (cause: OperationCanceledException) {
return ImagePickerResponse(canceled = true)
+ } finally {
+ isPickerOpen = false
}
}
diff --git a/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/contracts/ImageLibraryContract.kt b/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/contracts/ImageLibraryContract.kt
index ff15c91..41aaf12 100644
index ff15c91..9763012 100644
--- a/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/contracts/ImageLibraryContract.kt
+++ b/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/contracts/ImageLibraryContract.kt
@@ -26,51 +26,26 @@ import java.io.Serializable
@@ -5,12 +5,7 @@ import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.net.Uri
-import androidx.activity.result.PickVisualMediaRequest
-import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia
-import androidx.activity.result.contract.ActivityResultContracts.PickMultipleVisualMedia
import expo.modules.imagepicker.ImagePickerOptions
-import expo.modules.imagepicker.MediaTypes
-import expo.modules.imagepicker.UNLIMITED_SELECTION
import expo.modules.imagepicker.getAllDataUris
import expo.modules.imagepicker.toMediaType
import expo.modules.kotlin.activityresult.AppContextActivityResultContract
@@ -26,51 +21,26 @@ import java.io.Serializable
* @see [androidx.activity.result.contract.ActivityResultContracts.GetMultipleContents]
*/
internal class ImageLibraryContract(

View File

@ -1,92 +1,19 @@
diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm
index 9dca6a5..090bda5 100644
--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm
+++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm
@@ -266,11 +266,10 @@ - (void)textViewDidChange:(__unused UITextView *)textView
diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
index b09e653..d290dab 100644
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
+++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
@@ -198,6 +198,14 @@ - (void)refreshControlValueChanged
[self setCurrentRefreshingState:super.refreshing];
_refreshingProgrammatically = NO;
- (void)textViewDidChangeSelection:(__unused UITextView *)textView
{
- if (_lastStringStateWasUpdatedWith && ![_lastStringStateWasUpdatedWith isEqual:_backedTextInputView.attributedText]) {
+ if (![_lastStringStateWasUpdatedWith isEqual:_backedTextInputView.attributedText]) {
[self textViewDidChange:_backedTextInputView];
_ignoreNextTextInputCall = YES;
}
- _lastStringStateWasUpdatedWith = _backedTextInputView.attributedText;
[self textViewProbablyDidChangeSelection];
}
diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.mm
index 1f06b79..ab458f3 100644
--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.mm
+++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.mm
@@ -87,7 +87,7 @@ - (void)invalidateContentSize
return;
}
- CGSize maximumSize = self.layoutMetrics.frame.size;
+ CGSize maximumSize = self.layoutMetrics.contentFrame.size;
if (_maximumNumberOfLines == 1) {
maximumSize.width = CGFLOAT_MAX;
@@ -158,6 +158,8 @@ - (void)uiManagerWillPerformMounting
[attributedText insertAttributedString:propertyAttributedText atIndex:0];
}
+ [self postprocessAttributedText:attributedText];
+
NSAttributedString *newAttributedText;
if (![_previousAttributedText isEqualToAttributedString:attributedText]) {
// We have to follow `set prop` pattern:
@@ -191,6 +193,52 @@ - (void)uiManagerWillPerformMounting
}];
}
+- (void)postprocessAttributedText:(NSMutableAttributedString *)attributedText
+{
+ __block CGFloat maximumLineHeight = 0;
+
+ [attributedText enumerateAttribute:NSParagraphStyleAttributeName
+ inRange:NSMakeRange(0, attributedText.length)
+ options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
+ usingBlock:^(NSParagraphStyle *paragraphStyle, __unused NSRange range, __unused BOOL *stop) {
+ if (!paragraphStyle) {
+ return;
+ if (@available(iOS 17.4, *)) {
+ if (_currentRefreshingState) {
+ UIImpactFeedbackGenerator *feedbackGenerator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight];
+ [feedbackGenerator prepare];
+ [feedbackGenerator impactOccurred];
+ }
+ }
+
+ maximumLineHeight = MAX(paragraphStyle.maximumLineHeight, maximumLineHeight);
+ }];
+
+ if (maximumLineHeight == 0) {
+ // `lineHeight` was not specified, nothing to do.
+ return;
+ }
+
+ __block CGFloat maximumFontLineHeight = 0;
+
+ [attributedText enumerateAttribute:NSFontAttributeName
+ inRange:NSMakeRange(0, attributedText.length)
+ options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
+ usingBlock:^(UIFont *font, NSRange range, __unused BOOL *stop) {
+ if (!font) {
+ return;
+ }
+
+ if (maximumFontLineHeight <= font.lineHeight) {
+ maximumFontLineHeight = font.lineHeight;
+ }
+ }];
+
+ if (maximumLineHeight < maximumFontLineHeight) {
+ return;
+ }
+
+ CGFloat baseLineOffset = maximumLineHeight / 2.0 - maximumFontLineHeight / 2.0;
+
+ [attributedText addAttribute:NSBaselineOffsetAttributeName
+ value:@(baseLineOffset)
+ range:NSMakeRange(0, attributedText.length)];
+}
+
#pragma mark -
- (NSAttributedString *)measurableAttributedText
if (_onRefresh) {
_onRefresh(nil);
}

View File

@ -1,5 +1,5 @@
# TextInput Patch
# RefreshControl Patch
Patching `RCTBaseTextShadowInput.mm` from https://github.com/facebook/react-native/pull/38359. This fixes some text
getting cut off inside the composer. This was merged in December, so we should be able to remove this patch when RN
ships the next release.
Patching `RCTRefreshControl.mm` temporarily to play an impact haptic on refresh when using iOS 17.4 or higher. Since
17.4, there has been a regression somewhere causing haptics to not play on iOS on refresh. Should monitor for an update
in the RN repo: https://github.com/facebook/react-native/issues/43388

View File

@ -0,0 +1,104 @@
const crypto = require('crypto')
const fs = require('fs')
const fsp = fs.promises
const path = require('path')
const DIST_DIR = './dist'
const BUNDLES_DIR = '/_expo/static/js'
const IOS_BUNDLE_DIR = path.join(DIST_DIR, BUNDLES_DIR, '/ios')
const ANDROID_BUNDLE_DIR = path.join(DIST_DIR, BUNDLES_DIR, '/android')
const METADATA_PATH = path.join(DIST_DIR, '/metadata.json')
const DEST_DIR = './bundleTempDir'
// Weird, don't feel like figuring out _why_ it wants this
const METADATA = require(`../${METADATA_PATH}`)
const IOS_METADATA_ASSETS = METADATA.fileMetadata.ios.assets
const ANDROID_METADATA_ASSETS = METADATA.fileMetadata.android.assets
const getMd5 = async path => {
return new Promise(res => {
const hash = crypto.createHash('md5')
const rStream = fs.createReadStream(path)
rStream.on('data', data => {
hash.update(data)
})
rStream.on('end', () => {
res(hash.digest('hex'))
})
})
}
const moveFiles = async () => {
console.log('Making directory...')
await fsp.mkdir(DEST_DIR)
await fsp.mkdir(path.join(DEST_DIR, '/assets'))
console.log('Getting ios md5...')
const iosCurrPath = path.join(
IOS_BUNDLE_DIR,
(await fsp.readdir(IOS_BUNDLE_DIR))[0],
)
const iosMd5 = await getMd5(iosCurrPath)
const iosNewPath = `bundles/${iosMd5}.bundle`
console.log('Copying ios bundle...')
await fsp.cp(iosCurrPath, path.join(DEST_DIR, iosNewPath))
console.log('Getting android md5...')
const androidCurrPath = path.join(
ANDROID_BUNDLE_DIR,
(await fsp.readdir(ANDROID_BUNDLE_DIR))[0],
)
const androidMd5 = await getMd5(androidCurrPath)
const androidNewPath = `bundles/${androidMd5}.bundle`
console.log('Copying android bundle...')
await fsp.cp(androidCurrPath, path.join(DEST_DIR, androidNewPath))
const iosAssets = []
const androidAssets = []
console.log('Getting ios asset md5s and moving them...')
for (const asset of IOS_METADATA_ASSETS) {
const currPath = path.join(DIST_DIR, asset.path)
const md5 = await getMd5(currPath)
const withExtPath = `assets/${md5}.${asset.ext}`
iosAssets.push(withExtPath)
await fsp.cp(currPath, path.join(DEST_DIR, withExtPath))
}
console.log('Getting android asset md5s and moving them...')
for (const asset of ANDROID_METADATA_ASSETS) {
const currPath = path.join(DIST_DIR, asset.path)
const md5 = await getMd5(currPath)
const withExtPath = `assets/${md5}.${asset.ext}`
androidAssets.push(withExtPath)
await fsp.cp(currPath, path.join(DEST_DIR, withExtPath))
}
const result = {
version: 0,
bundler: 'metro',
fileMetadata: {
ios: {
bundle: iosNewPath,
assets: iosAssets,
},
android: {
bundle: androidNewPath,
assets: androidAssets,
},
},
}
console.log('Writing metadata...')
await fsp.writeFile(
path.join(DEST_DIR, 'metadata.json'),
JSON.stringify(result),
)
console.log('Finished!')
console.log('Metadata:', result)
}
moveFiles()

View File

@ -0,0 +1,26 @@
#!/bin/bash
set -o errexit
set -o pipefail
set -o nounset
rm -rf bundleTempDir
rm -rf bundle.tar.gz
echo "Creating tarball..."
node scripts/bundleUpdate.js
cd bundleTempDir || exit
BUNDLE_VERSION=$(date +%s)
DEPLOYMENT_URL="https://updates.bsky.app/v1/upload?runtime-version=$RUNTIME_VERSION&bundle-version=$BUNDLE_VERSION"
tar czvf bundle.tar.gz ./*
echo "Deploying to $DEPLOYMENT_URL..."
curl -o - --form "bundle=@./bundle.tar.gz" --user "bsky:$DENIS_API_KEY" --basic "$DEPLOYMENT_URL"
cd ..
rm -rf bundleTempDir
rm -rf bundle.tar.gz

View File

@ -16,6 +16,7 @@ type BreakpointName = keyof typeof breakpoints
const breakpoints: {
[key: string]: number
} = {
gtPhone: 500,
gtMobile: 800,
gtTablet: 1300,
}
@ -26,6 +27,7 @@ function getActiveBreakpoints({width}: {width: number}) {
return {
active: active[active.length - 1],
gtPhone: active.includes('gtPhone'),
gtMobile: active.includes('gtMobile'),
gtTablet: active.includes('gtTablet'),
}
@ -39,6 +41,7 @@ export const Context = React.createContext<{
theme: themes.Theme
breakpoints: {
active: BreakpointName | undefined
gtPhone: boolean
gtMobile: boolean
gtTablet: boolean
}
@ -47,6 +50,7 @@ export const Context = React.createContext<{
theme: themes.light,
breakpoints: {
active: undefined,
gtPhone: false,
gtMobile: false,
gtTablet: false,
},

View File

@ -1,5 +1,5 @@
import React from 'react'
import type {AccessibilityProps} from 'react-native'
import type {AccessibilityProps, GestureResponderEvent} from 'react-native'
import {BottomSheetProps} from '@gorhom/bottom-sheet'
import {ViewStyleProp} from '#/alf'
@ -10,9 +10,15 @@ type A11yProps = Required<AccessibilityProps>
* Mutated by useImperativeHandle to provide a public API for controlling the
* dialog. The methods here will actually become the handlers defined within
* the `Dialog.Outer` component.
*
* `Partial<GestureResponderEvent>` here allows us to add this directly to the
* `onPress` prop of a button, for example. If this type was not added, we
* would need to create a function to wrap `.open()` with.
*/
export type DialogControlRefProps = {
open: (options?: DialogControlOpenOptions) => void
open: (
options?: DialogControlOpenOptions & Partial<GestureResponderEvent>,
) => void
close: (callback?: () => void) => void
}

View File

@ -0,0 +1,90 @@
import React from 'react'
import {CenteredView} from 'view/com/util/Views'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
import {View} from 'react-native'
import {Button} from '#/components/Button'
import {useNavigation} from '@react-navigation/core'
import {NavigationProp} from 'lib/routes/types'
import {StackActions} from '@react-navigation/native'
import {router} from '#/routes'
export function Error({
title,
message,
onRetry,
}: {
title?: string
message?: string
onRetry?: () => unknown
}) {
const navigation = useNavigation<NavigationProp>()
const t = useTheme()
const {gtMobile} = useBreakpoints()
const canGoBack = navigation.canGoBack()
const onGoBack = React.useCallback(() => {
if (canGoBack) {
navigation.goBack()
} else {
navigation.navigate('HomeTab')
// Checking the state for routes ensures that web doesn't encounter errors while going back
if (navigation.getState()?.routes) {
navigation.dispatch(StackActions.push(...router.matchPath('/')))
} else {
navigation.navigate('HomeTab')
navigation.dispatch(StackActions.popToTop())
}
}
}, [navigation, canGoBack])
return (
<CenteredView
style={[
a.flex_1,
a.align_center,
!gtMobile ? a.justify_between : a.gap_5xl,
t.atoms.border_contrast_low,
{paddingTop: 175, paddingBottom: 110},
]}
sideBorders>
<View style={[a.w_full, a.align_center, a.gap_lg]}>
<Text style={[a.font_bold, a.text_3xl]}>{title}</Text>
<Text
style={[
a.text_md,
a.text_center,
t.atoms.text_contrast_high,
{lineHeight: 1.4},
gtMobile && {width: 450},
]}>
{message}
</Text>
</View>
<View style={[a.gap_md, gtMobile ? {width: 350} : [a.w_full, a.px_lg]]}>
{onRetry && (
<Button
variant="solid"
color="primary"
label="Click here"
onPress={onRetry}
size="large"
style={[a.rounded_sm, a.overflow_hidden, {paddingVertical: 10}]}>
Retry
</Button>
)}
<Button
variant="solid"
color={onRetry ? 'secondary' : 'primary'}
label="Click here"
onPress={onGoBack}
size="large"
style={[a.rounded_sm, a.overflow_hidden, {paddingVertical: 10}]}>
Go Back
</Button>
</View>
</CenteredView>
)
}

View File

@ -104,7 +104,7 @@ export function Default({
}: LabelingServiceProps & ViewStyleProp) {
return (
<Outer style={style}>
<Avatar />
<Avatar avatar={labeler.creator.avatar} />
<Content>
<Title
value={getLabelingServiceTitle({

View File

@ -1,26 +1,28 @@
import React from 'react'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {View} from 'react-native'
import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro'
import {CenteredView} from 'view/com/util/Views'
import {Loader} from '#/components/Loader'
import {Trans} from '@lingui/macro'
import {cleanError} from 'lib/strings/errors'
import {Button} from '#/components/Button'
import {Text} from '#/components/Typography'
import {StackActions} from '@react-navigation/native'
import {router} from '#/routes'
import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped'
import {Error} from '#/components/Error'
export function ListFooter({
isFetching,
isError,
error,
onRetry,
height,
}: {
isFetching: boolean
isError: boolean
isFetching?: boolean
isError?: boolean
error?: string
onRetry?: () => Promise<unknown>
height?: number
}) {
const t = useTheme()
@ -29,11 +31,10 @@ export function ListFooter({
style={[
a.w_full,
a.align_center,
a.justify_center,
a.border_t,
a.pb_lg,
t.atoms.border_contrast_low,
{height: 180},
{height: height ?? 180, paddingTop: 30},
]}>
{isFetching ? (
<Loader size="xl" />
@ -53,11 +54,12 @@ function ListFooterMaybeError({
error,
onRetry,
}: {
isError: boolean
isError?: boolean
error?: string
onRetry?: () => Promise<unknown>
}) {
const t = useTheme()
const {_} = useLingui()
if (!isError) return null
@ -83,7 +85,7 @@ function ListFooterMaybeError({
</Text>
<Button
variant="gradient"
label="Press to retry"
label={_(msg`Press to retry`)}
style={[
a.align_center,
a.justify_center,
@ -93,7 +95,7 @@ function ListFooterMaybeError({
a.py_sm,
]}
onPress={onRetry}>
Retry
<Trans>Retry</Trans>
</Button>
</View>
</View>
@ -128,42 +130,39 @@ export function ListMaybePlaceholder({
isLoading,
isEmpty,
isError,
empty,
error,
notFoundType = 'page',
emptyTitle,
emptyMessage,
errorTitle,
errorMessage,
emptyType = 'page',
onRetry,
}: {
isLoading: boolean
isEmpty: boolean
isError: boolean
empty?: string
error?: string
notFoundType?: 'page' | 'results'
isEmpty?: boolean
isError?: boolean
emptyTitle?: string
emptyMessage?: string
errorTitle?: string
errorMessage?: string
emptyType?: 'page' | 'results'
onRetry?: () => Promise<unknown>
}) {
const navigation = useNavigationDeduped()
const t = useTheme()
const {_} = useLingui()
const {gtMobile, gtTablet} = useBreakpoints()
const {_} = useLingui()
const canGoBack = navigation.canGoBack()
const onGoBack = React.useCallback(() => {
if (canGoBack) {
navigation.goBack()
} else {
navigation.navigate('HomeTab')
// Checking the state for routes ensures that web doesn't encounter errors while going back
if (navigation.getState()?.routes) {
navigation.dispatch(StackActions.push(...router.matchPath('/')))
} else {
navigation.navigate('HomeTab')
navigation.dispatch(StackActions.popToTop())
if (!isLoading && isError) {
return (
<Error
title={errorTitle ?? _(msg`Oops!`)}
message={errorMessage ?? _(`Something went wrong!`)}
onRetry={onRetry}
/>
)
}
}
}, [navigation, canGoBack])
if (!isEmpty) return null
if (isLoading) {
return (
<CenteredView
style={[
@ -175,74 +174,28 @@ export function ListMaybePlaceholder({
]}
sideBorders={gtMobile}
topBorder={!gtTablet}>
{isLoading ? (
<View style={[a.w_full, a.align_center, {top: 100}]}>
<Loader size="xl" />
</View>
) : (
<>
<View style={[a.w_full, a.align_center, a.gap_lg]}>
<Text style={[a.font_bold, a.text_3xl]}>
{isError ? (
<Trans>Oops!</Trans>
) : isEmpty ? (
<>
{notFoundType === 'results' ? (
<Trans>No results found</Trans>
) : (
<Trans>Page not found</Trans>
)}
</>
) : undefined}
</Text>
{isError ? (
<Text
style={[a.text_md, a.text_center, t.atoms.text_contrast_high]}>
{error ? error : <Trans>Something went wrong!</Trans>}
</Text>
) : isEmpty ? (
<Text
style={[a.text_md, a.text_center, t.atoms.text_contrast_high]}>
{empty ? (
empty
) : (
<Trans>
We're sorry! We can't find the page you were looking for.
</Trans>
)}
</Text>
) : undefined}
</View>
<View
style={[a.gap_md, !gtMobile ? [a.w_full, a.px_lg] : {width: 350}]}>
{isError && onRetry && (
<Button
variant="solid"
color="primary"
label="Click here"
onPress={onRetry}
size="large"
style={[
a.rounded_sm,
a.overflow_hidden,
{paddingVertical: 10},
]}>
Retry
</Button>
)}
<Button
variant="solid"
color={isError && onRetry ? 'secondary' : 'primary'}
label="Click here"
onPress={onGoBack}
size="large"
style={[a.rounded_sm, a.overflow_hidden, {paddingVertical: 10}]}>
Go Back
</Button>
</View>
</>
)}
</CenteredView>
)
}
if (isEmpty) {
return (
<Error
title={
emptyTitle ??
(emptyType === 'results'
? _(msg`No results found`)
: _(msg`Page not found`))
}
message={
emptyMessage ??
_(msg`We're sorry! We can't find the page you were looking for.`)
}
onRetry={onRetry}
/>
)
}
}

View File

@ -17,7 +17,7 @@ import {
ItemIconProps,
} from '#/components/Menu/types'
import {Button, ButtonText} from '#/components/Button'
import {msg} from '@lingui/macro'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {isNative} from 'platform/detection'
@ -209,7 +209,9 @@ function Cancel() {
variant="ghost"
color="secondary"
onPress={() => control.close()}>
<ButtonText>Cancel</ButtonText>
<ButtonText>
<Trans>Cancel</Trans>
</ButtonText>
</Button>
)
}

View File

@ -5,12 +5,13 @@ import {useLingui} from '@lingui/react'
import {AppBskyLabelerDefs} from '@atproto/api'
export {useDialogControl as useReportDialogControl} from '#/components/Dialog'
import {getLabelingServiceTitle} from '#/lib/moderation'
import {atoms as a, useTheme} from '#/alf'
import {atoms as a, useTheme, useBreakpoints} from '#/alf'
import {Text} from '#/components/Typography'
import {Button, useButtonContext} from '#/components/Button'
import {Divider} from '#/components/Divider'
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
import * as LabelingServiceCard from '#/components/LabelingServiceCard'
import {ReportDialogProps} from './types'
@ -22,31 +23,29 @@ export function SelectLabelerView({
}) {
const t = useTheme()
const {_} = useLingui()
const {gtMobile} = useBreakpoints()
return (
<View style={[a.gap_lg]}>
<View style={[a.justify_center, a.gap_sm]}>
<View style={[a.justify_center, gtMobile ? a.gap_sm : a.gap_xs]}>
<Text style={[a.text_2xl, a.font_bold]}>
<Trans>Select moderation service</Trans>
<Trans>Select moderator</Trans>
</Text>
<Text style={[a.text_md, t.atoms.text_contrast_medium]}>
<Trans>Who do you want to send this report to?</Trans>
<Trans>To whom would you like to send this report?</Trans>
</Text>
</View>
<Divider />
<View style={[a.gap_sm, {marginHorizontal: a.p_md.padding * -1}]}>
<View style={[a.gap_xs, {marginHorizontal: a.p_md.padding * -1}]}>
{props.labelers.map(labeler => {
return (
<Button
key={labeler.creator.did}
label={_(msg`Send report to ${labeler.creator.displayName}`)}
onPress={() => props.onSelectLabeler(labeler.creator.did)}>
<LabelerButton
title={labeler.creator.displayName || labeler.creator.handle}
description={labeler.creator.description || ''}
/>
<LabelerButton labeler={labeler} />
</Button>
)
})}
@ -56,11 +55,9 @@ export function SelectLabelerView({
}
function LabelerButton({
title,
description,
labeler,
}: {
title: string
description: string
labeler: AppBskyLabelerDefs.LabelerViewDetailed
}) {
const t = useTheme()
const {hovered, pressed} = useButtonContext()
@ -75,41 +72,21 @@ function LabelerButton({
}, [t])
return (
<View
style={[
a.w_full,
a.flex_row,
a.align_center,
a.justify_between,
a.p_md,
a.rounded_md,
{paddingRight: 70},
interacted && styles.interacted,
]}>
<View style={[a.flex_1, a.gap_xs]}>
<Text style={[a.text_md, a.font_bold, t.atoms.text_contrast_medium]}>
{title}
</Text>
<Text style={[a.leading_tight, {maxWidth: 400}]} numberOfLines={3}>
{description}
</Text>
</View>
<View
style={[
a.absolute,
a.inset_0,
a.justify_center,
a.pr_md,
{left: 'auto'},
]}>
<ChevronRight
size="md"
fill={
hovered ? t.palette.primary_500 : t.atoms.text_contrast_low.color
}
<LabelingServiceCard.Outer
style={[a.p_md, a.rounded_sm, interacted && styles.interacted]}>
<LabelingServiceCard.Avatar avatar={labeler.creator.avatar} />
<LabelingServiceCard.Content>
<LabelingServiceCard.Title
value={getLabelingServiceTitle({
displayName: labeler.creator.displayName,
handle: labeler.creator.handle,
})}
/>
</View>
</View>
<Text
style={[t.atoms.text_contrast_medium, a.text_sm, a.font_semibold]}>
@{labeler.creator.handle}
</Text>
</LabelingServiceCard.Content>
</LabelingServiceCard.Outer>
)
}

View File

@ -9,7 +9,7 @@ import {DMCA_LINK} from '#/components/ReportDialog/const'
import {Link} from '#/components/Link'
export {useDialogControl as useReportDialogControl} from '#/components/Dialog'
import {atoms as a, useTheme} from '#/alf'
import {atoms as a, useTheme, useBreakpoints} from '#/alf'
import {Text} from '#/components/Typography'
import {
Button,
@ -35,6 +35,7 @@ export function SelectReportOptionView({
}) {
const t = useTheme()
const {_} = useLingui()
const {gtMobile} = useBreakpoints()
const allReportOptions = useReportOptions()
const reportOptions = allReportOptions[props.params.type]
@ -76,7 +77,7 @@ export function SelectReportOptionView({
</Button>
) : null}
<View style={[a.justify_center, a.gap_sm]}>
<View style={[a.justify_center, gtMobile ? a.gap_sm : a.gap_xs]}>
<Text style={[a.text_2xl, a.font_bold]}>{i18n.title}</Text>
<Text style={[a.text_md, t.atoms.text_contrast_medium]}>
{i18n.description}

View File

@ -264,7 +264,9 @@ export function TagMenu({
variant="ghost"
color="secondary"
onPress={() => control.close()}>
<ButtonText>Cancel</ButtonText>
<ButtonText>
<Trans>Cancel</Trans>
</ButtonText>
</Button>
</>
)}

View File

@ -1,48 +1,64 @@
import React from 'react'
import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro'
import {View} from 'react-native'
import * as Dialog from '#/components/Dialog'
import {Text} from '../Typography'
import {DateInput} from '#/view/com/util/forms/DateInput'
import {logger} from '#/logger'
import {
usePreferencesQuery,
usePreferencesSetBirthDateMutation,
UsePreferencesQueryResponse,
} from '#/state/queries/preferences'
import {Button, ButtonText} from '../Button'
import {Button, ButtonIcon, ButtonText} from '../Button'
import {atoms as a, useTheme} from '#/alf'
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
import {cleanError} from '#/lib/strings/errors'
import {ActivityIndicator, View} from 'react-native'
import {isIOS, isWeb} from '#/platform/detection'
import {Loader} from '#/components/Loader'
export function BirthDateSettingsDialog({
control,
preferences,
}: {
control: Dialog.DialogControlProps
preferences: UsePreferencesQueryResponse | undefined
}) {
const t = useTheme()
const {_} = useLingui()
const {isPending, isError, error, mutateAsync} =
usePreferencesSetBirthDateMutation()
const {isLoading, error, data: preferences} = usePreferencesQuery()
return (
<Dialog.Outer control={control}>
<Dialog.Handle />
<Dialog.ScrollableInner label={_(msg`My Birthday`)}>
{preferences && !isPending ? (
<BirthdayInner
control={control}
preferences={preferences}
isError={isError}
error={error}
setBirthDate={mutateAsync}
<View style={[a.gap_sm, a.pb_lg]}>
<Text style={[a.text_2xl, a.font_bold]}>
<Trans>My Birthday</Trans>
</Text>
<Text style={[a.text_md, t.atoms.text_contrast_medium]}>
<Trans>This information is not shared with other users.</Trans>
</Text>
</View>
{isLoading ? (
<Loader size="xl" />
) : error || !preferences ? (
<ErrorMessage
message={
error?.toString() ||
_(
msg`We were unable to load your birth date preferences. Please try again.`,
)
}
style={[a.rounded_sm]}
/>
) : (
<ActivityIndicator size="large" style={a.my_5xl} />
<BirthdayInner control={control} preferences={preferences} />
)}
<Dialog.Close />
</Dialog.ScrollableInner>
</Dialog.Outer>
)
@ -51,20 +67,18 @@ export function BirthDateSettingsDialog({
function BirthdayInner({
control,
preferences,
isError,
error,
setBirthDate,
}: {
control: Dialog.DialogControlProps
preferences: UsePreferencesQueryResponse
isError: boolean
error: unknown
setBirthDate: (args: {birthDate: Date}) => Promise<unknown>
}) {
const {_} = useLingui()
const [date, setDate] = React.useState(preferences.birthDate || new Date())
const t = useTheme()
const {
isPending,
isError,
error,
mutateAsync: setBirthDate,
} = usePreferencesSetBirthDateMutation()
const hasChanged = date !== preferences.birthDate
const onSave = React.useCallback(async () => {
@ -74,21 +88,13 @@ function BirthdayInner({
await setBirthDate({birthDate: date})
}
control.close()
} catch (e) {
logger.error(`setBirthDate failed`, {message: e})
} catch (e: any) {
logger.error(`setBirthDate failed`, {message: e.message})
}
}, [date, setBirthDate, control, hasChanged])
return (
<View style={a.gap_lg} testID="birthDateSettingsDialog">
<View style={[a.gap_sm]}>
<Text style={[a.text_2xl, a.font_bold]}>
<Trans>My Birthday</Trans>
</Text>
<Text style={t.atoms.text_contrast_medium}>
<Trans>This information is not shared with other users.</Trans>
</Text>
</View>
<View style={isIOS && [a.w_full, a.align_center]}>
<DateInput
handleAsUTC
@ -103,6 +109,7 @@ function BirthdayInner({
accessibilityLabelledBy="birthDate"
/>
</View>
{isError ? (
<ErrorMessage message={cleanError(error)} style={[a.rounded_sm]} />
) : undefined}
@ -110,13 +117,14 @@ function BirthdayInner({
<View style={isWeb && [a.flex_row, a.justify_end]}>
<Button
label={hasChanged ? _(msg`Save birthday`) : _(msg`Done`)}
size={isWeb ? 'small' : 'medium'}
size="medium"
onPress={onSave}
variant="solid"
color="primary">
<ButtonText>
{hasChanged ? <Trans>Save</Trans> : <Trans>Done</Trans>}
</ButtonText>
{isPending && <ButtonIcon icon={Loader} />}
</Button>
</View>
</View>

View File

@ -12,7 +12,7 @@ import {
} from '#/state/queries/preferences'
import {getLabelStrings} from '#/lib/moderation/useLabelInfo'
import {useTheme, atoms as a} from '#/alf'
import {useTheme, atoms as a, useBreakpoints} from '#/alf'
import {Text} from '#/components/Typography'
import {InlineLink} from '#/components/Link'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '../icons/CircleInfo'
@ -29,6 +29,7 @@ export function ModerationLabelPref({
}) {
const {_, i18n} = useLingui()
const t = useTheme()
const {gtPhone} = useBreakpoints()
const isGlobalLabel = !labelValueDefinition.definedBy
const {identifier} = labelValueDefinition
@ -57,6 +58,7 @@ export function ModerationLabelPref({
adultOnly && !preferences?.moderationPrefs.adultContentEnabled
// are there any reasons we cant configure this label here?
const cantConfigure = isGlobalLabel || adultDisabled
const showConfig = !disabled && (gtPhone || !cantConfigure)
// adjust the pref based on whether warn is available
let prefAdjusted = pref
@ -85,9 +87,19 @@ export function ModerationLabelPref({
)
return (
<View style={[a.flex_row, a.gap_sm, a.px_lg, a.py_lg, a.justify_between]}>
<View
style={[
a.flex_row,
a.gap_md,
a.px_lg,
a.py_lg,
a.justify_between,
a.flex_wrap,
]}>
<View style={[a.gap_xs, a.flex_1]}>
<Text style={[a.font_bold]}>{labelStrings.name}</Text>
<Text style={[a.font_bold, gtPhone ? a.text_sm : a.text_md]}>
{labelStrings.name}
</Text>
<Text style={[t.atoms.text_contrast_medium, a.leading_snug]}>
{labelStrings.description}
</Text>
@ -113,11 +125,20 @@ export function ModerationLabelPref({
</View>
)}
</View>
{disabled ? (
<></>
) : cantConfigure ? (
<View style={[{minHeight: 35}, a.px_sm, a.py_md]}>
<Text style={[a.font_bold, t.atoms.text_contrast_medium]}>
{showConfig && (
<View style={[gtPhone ? undefined : a.w_full]}>
{cantConfigure ? (
<View
style={[
{minHeight: 35},
a.px_md,
a.py_md,
a.rounded_sm,
a.border,
t.atoms.border_contrast_low,
]}>
<Text style={[a.font_bold, t.atoms.text_contrast_low]}>
{currentPrefLabel}
</Text>
</View>
@ -150,5 +171,7 @@ export function ModerationLabelPref({
</View>
)}
</View>
)}
</View>
)
}

View File

@ -4,6 +4,23 @@ import TLDs from 'tlds'
import psl from 'psl'
export const BSKY_APP_HOST = 'https://bsky.app'
const BSKY_TRUSTED_HOSTS = [
'bsky.app',
'bsky.social',
'blueskyweb.xyz',
'blueskyweb.zendesk.com',
...(__DEV__ ? ['localhost:19006', 'localhost:8100'] : []),
]
/*
* This will allow any BSKY_TRUSTED_HOSTS value by itself or with a subdomain.
* It will also allow relative paths like /profile as well as #.
*/
const TRUSTED_REGEX = new RegExp(
`^(http(s)?://(([\\w-]+\\.)?${BSKY_TRUSTED_HOSTS.join(
'|([\\w-]+\\.)?',
)})|/|#)`,
)
export function isValidDomain(str: string): boolean {
return !!TLDs.find(tld => {
@ -86,6 +103,10 @@ export function isExternalUrl(url: string): boolean {
return external || rss
}
export function isTrustedUrl(url: string): boolean {
return TRUSTED_REGEX.test(url)
}
export function isBskyPostUrl(url: string): boolean {
if (isBskyAppUrl(url)) {
try {
@ -163,8 +184,8 @@ export function feedUriToHref(url: string): string {
export function linkRequiresWarning(uri: string, label: string) {
const labelDomain = labelToDomain(label)
// If the uri started with a / we know it is internal.
if (isRelativeUrl(uri)) {
// We should trust any relative URL or a # since we know it links to internal content
if (isRelativeUrl(uri) || uri === '#') {
return false
}
@ -176,18 +197,11 @@ export function linkRequiresWarning(uri: string, label: string) {
}
const host = urip.hostname.toLowerCase()
// Hosts that end with bsky.app or bsky.social should be trusted by default.
if (
host.endsWith('bsky.app') ||
host.endsWith('bsky.social') ||
host.endsWith('blueskyweb.xyz')
) {
// if this is a link to internal content,
// warn if it represents itself as a URL to another app
if (isTrustedUrl(uri)) {
// if this is a link to internal content, warn if it represents itself as a URL to another app
return !!labelDomain && labelDomain !== host && isPossiblyAUrl(labelDomain)
} else {
// if this is a link to external content,
// warn if the label doesnt match the target
// if this is a link to external content, warn if the label doesnt match the target
if (!labelDomain) {
return true
}

View File

@ -244,12 +244,12 @@ msgstr "Avançat"
#: src/view/screens/Feeds.tsx:666
msgid "All the feeds you've saved, right in one place."
msgstr ""
msgstr "Tots els canals que has desat, en un sol lloc."
#: src/view/com/auth/login/ForgotPasswordForm.tsx:221
#: src/view/com/modals/ChangePassword.tsx:168
msgid "Already have a code?"
msgstr ""
msgstr "Ja tens un codi?"
#: src/view/com/auth/login/ChooseAccountForm.tsx:98
msgid "Already signed in as @{0}"
@ -287,7 +287,7 @@ msgstr "i"
#: src/screens/Onboarding/index.tsx:32
msgid "Animals"
msgstr ""
msgstr "Animals"
#: src/view/screens/LanguageSettings.tsx:95
msgid "App Language"
@ -366,7 +366,7 @@ msgstr "Estàs escrivint en <0>{0}</0>?"
#: src/screens/Onboarding/index.tsx:26
msgid "Art"
msgstr ""
msgstr "Art"
#: src/view/com/modals/SelfLabel.tsx:123
msgid "Artistic or non-erotic nudity."
@ -393,7 +393,7 @@ msgstr "Endarrere"
#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:136
msgid "Based on your interest in {interestsText}"
msgstr ""
msgstr "Segons els teus interessos en {interestsText}"
#: src/view/screens/Settings/index.tsx:523
msgid "Basics"
@ -472,7 +472,7 @@ msgstr "Bluesky"
#: src/view/com/auth/server-input/index.tsx:150
msgid "Bluesky is an open network where you can choose your hosting provider. Custom hosting is now available in beta for developers."
msgstr ""
msgstr "Bluesky és una xarxa oberta on pots escollir el teu proveïdor d'allotjament. L'allotjament personalitzat està disponible en beta per a desenvolupadors"
#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:80
#: src/view/com/auth/onboarding/WelcomeMobile.tsx:80
@ -503,7 +503,7 @@ msgstr "Bluesky no mostrarà el teu perfil ni les publicacions als usuaris que n
#: src/screens/Onboarding/index.tsx:33
msgid "Books"
msgstr ""
msgstr "Llibres"
#: src/view/screens/Settings/index.tsx:859
msgid "Build version {0} {1}"
@ -631,11 +631,11 @@ msgstr "Canvia el meu correu"
#: src/view/screens/Settings/index.tsx:732
msgid "Change password"
msgstr ""
msgstr "Canvia la contrasenya"
#: src/view/screens/Settings/index.tsx:741
msgid "Change Password"
msgstr ""
msgstr "Canvia la contrasenya"
#: src/view/com/composer/select-language/SuggestedLanguage.tsx:73
msgid "Change post language to {0}"
@ -643,7 +643,7 @@ msgstr "Canvia l'idioma de la publicació a {0}"
#: src/view/screens/Settings/index.tsx:733
msgid "Change your Bluesky password"
msgstr ""
msgstr "Canvia la teva contrasenya de Bluesky"
#: src/view/com/modals/ChangeEmail.tsx:109
msgid "Change Your Email"
@ -652,7 +652,7 @@ msgstr "Canvia el teu correu"
#: src/screens/Deactivated.tsx:72
#: src/screens/Deactivated.tsx:76
msgid "Check my status"
msgstr ""
msgstr "Comprova el meu estat"
#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:121
msgid "Check out some recommended feeds. Tap + to add them to your list of pinned feeds."
@ -680,7 +680,7 @@ msgstr "Tria un servei"
#: src/screens/Onboarding/StepFinished.tsx:135
msgid "Choose the algorithms that power your custom feeds."
msgstr ""
msgstr "Tria els algoritmes que alimentaran els teus canals personalitzats."
#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:83
#: src/view/com/auth/onboarding/WelcomeMobile.tsx:83
@ -689,7 +689,7 @@ msgstr "Tria els algoritmes que potenciaran la teva experiència amb els canals
#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:103
msgid "Choose your main feeds"
msgstr ""
msgstr "Tria els teus canals principals"
#: src/view/com/auth/create/Step1.tsx:196
msgid "Choose your password"
@ -732,12 +732,12 @@ msgstr ""
#: src/screens/Onboarding/index.tsx:35
msgid "Climate"
msgstr ""
msgstr "Clima"
#: src/view/com/modals/ChangePassword.tsx:265
#: src/view/com/modals/ChangePassword.tsx:268
msgid "Close"
msgstr ""
msgstr "Tanca"
#: src/components/Dialog/index.web.tsx:84
#: src/components/Dialog/index.web.tsx:198
@ -790,11 +790,11 @@ msgstr "Plega la llista d'usuaris per una notificació concreta"
#: src/screens/Onboarding/index.tsx:41
msgid "Comedy"
msgstr ""
msgstr "Comèdia"
#: src/screens/Onboarding/index.tsx:27
msgid "Comics"
msgstr ""
msgstr "Còmics"
#: src/Navigation.tsx:229
#: src/view/screens/CommunityGuidelines.tsx:32
@ -803,7 +803,7 @@ msgstr "Directrius de la comunitat"
#: src/screens/Onboarding/StepFinished.tsx:148
msgid "Complete onboarding and start using your account"
msgstr ""
msgstr "Finalitza el registre i comença a utilitzar el teu compte"
#: src/view/com/auth/create/Step3.tsx:73
msgid "Complete the challenge"
@ -819,7 +819,7 @@ msgstr "Redacta una resposta"
#: src/screens/Onboarding/StepModeration/ModerationOption.tsx:67
msgid "Configure content filtering setting for category: {0}"
msgstr ""
msgstr "Configura els filtres de continguts per la categoria: {0}"
#: src/components/Prompt.tsx:124
#: src/view/com/modals/AppealLabel.tsx:98
@ -914,19 +914,19 @@ msgstr "Continua"
#: src/screens/Onboarding/StepModeration/index.tsx:115
#: src/screens/Onboarding/StepTopicalFeeds.tsx:111
msgid "Continue to next step"
msgstr ""
msgstr "Continua"
#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:167
msgid "Continue to the next step"
msgstr ""
msgstr "Continua"
#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:191
msgid "Continue to the next step without following any accounts"
msgstr ""
msgstr "Continua sense seguir cap compte"
#: src/screens/Onboarding/index.tsx:44
msgid "Cooking"
msgstr ""
msgstr "Cuina"
#: src/view/com/modals/AddAppPasswords.tsx:195
#: src/view/com/modals/InviteCodes.tsx:182
@ -1027,12 +1027,12 @@ msgstr "Crea una targeta amb una minuatura. La targeta enllaça a {url}"
#: src/screens/Onboarding/index.tsx:29
msgid "Culture"
msgstr ""
msgstr "Cultura"
#: src/view/com/auth/server-input/index.tsx:95
#: src/view/com/auth/server-input/index.tsx:96
msgid "Custom"
msgstr ""
msgstr "Personalitzat"
#: src/view/com/modals/ChangeHandle.tsx:389
msgid "Custom domain"
@ -1041,7 +1041,7 @@ msgstr "Domini personalitzat"
#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:106
#: src/view/screens/Feeds.tsx:692
msgid "Custom feeds built by the community bring you new experiences and help you find the content you love."
msgstr ""
msgstr "Els canals personalitzats fets per la comunitat et porten noves experiències i t'ajuden a trobar contingut que t'agradarà."
#: src/view/screens/PreferencesExternalEmbeds.tsx:55
msgid "Customize media from external sites."
@ -1062,7 +1062,7 @@ msgstr "Mode fosc"
#: src/view/screens/Settings/index.tsx:498
msgid "Dark Theme"
msgstr ""
msgstr "Tema fosc"
#: src/view/screens/Debug.tsx:83
msgid "Debug panel"
@ -1096,7 +1096,7 @@ msgstr "Elimina el meu compte"
#: src/view/screens/Settings/index.tsx:784
msgid "Delete My Account…"
msgstr ""
msgstr "Elimina el meu compte…"
#: src/view/com/util/forms/PostDropdownBtn.tsx:317
#: src/view/com/util/forms/PostDropdownBtn.tsx:326
@ -1136,7 +1136,7 @@ msgstr "Vols dir alguna cosa?"
#: src/view/screens/Settings/index.tsx:504
msgid "Dim"
msgstr ""
msgstr "Tènue"
#: src/view/com/composer/Composer.tsx:151
msgid "Discard"
@ -1161,7 +1161,7 @@ msgstr "Descobreix nous canals personalitzats"
#: src/view/screens/Feeds.tsx:689
msgid "Discover New Feeds"
msgstr ""
msgstr "Descobreix nous canals"
#: src/view/com/modals/EditProfile.tsx:192
msgid "Display name"
@ -1218,20 +1218,20 @@ msgstr "Fes doble toc per iniciar la sessió"
#: src/view/screens/Settings/index.tsx:755
msgid "Download Bluesky account data (repository)"
msgstr ""
msgstr "Descarrega les dades del compte de Bluesky (repositori)"
#: src/view/screens/Settings/ExportCarDialog.tsx:59
#: src/view/screens/Settings/ExportCarDialog.tsx:63
msgid "Download CAR file"
msgstr ""
msgstr "Descarrega el fitxer CAR"
#: src/view/com/composer/text-input/TextInput.web.tsx:249
msgid "Drop to add images"
msgstr ""
msgstr "Deixa anar per afegir imatges"
#: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:111
msgid "Due to Apple policies, adult content can only be enabled on the web after completing sign up."
msgstr ""
msgstr "Degut a les polítiques d'Apple, el contingut per a adults només es pot habilitar a la web després de registrar-se"
#: src/view/com/modals/EditProfile.tsx:185
msgid "e.g. Alice Roberts"
@ -1316,7 +1316,7 @@ msgstr "Edita la descripció del teu perfil"
#: src/screens/Onboarding/index.tsx:34
msgid "Education"
msgstr ""
msgstr "Ensenyament"
#: src/view/com/auth/create/Step1.tsx:176
#: src/view/com/auth/login/ForgotPasswordForm.tsx:156
@ -1357,7 +1357,7 @@ msgstr "Habilita el contingut per a adults"
#: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:76
#: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:77
msgid "Enable adult content in your feeds"
msgstr ""
msgstr "Habilita veure el contingut per adults als teus canals"
#: src/view/com/modals/EmbedConsent.tsx:97
msgid "Enable External Media"
@ -1394,7 +1394,7 @@ msgstr "Entra el codi de confirmació"
#: src/view/com/modals/ChangePassword.tsx:151
msgid "Enter the code you received to change your password."
msgstr ""
msgstr "Introdueix el codi que has rebut per canviar la teva contrasenya."
#: src/view/com/modals/ChangeHandle.tsx:371
msgid "Enter the domain you want to use"
@ -1473,12 +1473,12 @@ msgstr "Expandeix o replega la publicació completa a la qual estàs responent"
#: src/view/screens/Settings/index.tsx:753
msgid "Export my data"
msgstr ""
msgstr "Exporta les meves dades"
#: src/view/screens/Settings/ExportCarDialog.tsx:44
#: src/view/screens/Settings/index.tsx:764
msgid "Export My Data"
msgstr ""
msgstr "Exporta les meves dades"
#: src/view/com/modals/EmbedConsent.tsx:64
msgid "External Media"
@ -1559,11 +1559,11 @@ msgstr "Els canals són algoritmes personalitzats creats per usuaris que coneixe
#: src/screens/Onboarding/StepTopicalFeeds.tsx:76
msgid "Feeds can be topical as well!"
msgstr ""
msgstr "Els canals també poden ser d'actualitat!"
#: src/screens/Onboarding/StepFinished.tsx:151
msgid "Finalizing"
msgstr ""
msgstr "Finalitzant"
#: src/view/com/posts/CustomFeedEmptyState.tsx:47
#: src/view/com/posts/FollowingEmptyState.tsx:57
@ -1597,11 +1597,11 @@ msgstr "Ajusta els fils de debat."
#: src/screens/Onboarding/index.tsx:38
msgid "Fitness"
msgstr ""
msgstr "Exercici"
#: src/screens/Onboarding/StepFinished.tsx:131
msgid "Flexible"
msgstr ""
msgstr "Flexible"
#: src/view/com/modals/EditImage.tsx:115
msgid "Flip horizontal"
@ -1631,11 +1631,11 @@ msgstr "Segueix {0}"
#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:179
msgid "Follow All"
msgstr ""
msgstr "Segueix-los a tots"
#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:174
msgid "Follow selected accounts and continue to the next step"
msgstr ""
msgstr "Segueix els comptes seleccionats i continua"
#: src/view/com/auth/onboarding/RecommendedFollows.tsx:64
msgid "Follow some users to get started. We can recommend you more users based on who you find interesting."
@ -1693,7 +1693,7 @@ msgstr "Et segueix"
#: src/screens/Onboarding/index.tsx:43
msgid "Food"
msgstr ""
msgstr "Menjar"
#: src/view/com/modals/DeleteAccount.tsx:111
msgid "For security reasons, we'll need to send a confirmation code to your email address."
@ -1752,7 +1752,7 @@ msgstr "Ves enrere"
#: src/screens/Onboarding/Layout.tsx:104
#: src/screens/Onboarding/Layout.tsx:193
msgid "Go back to previous step"
msgstr ""
msgstr "Ves al pas anterior"
#: src/view/screens/Search/Search.tsx:747
#: src/view/shell/desktop/Search.tsx:262
@ -1794,15 +1794,15 @@ msgstr "Ajuda"
#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:132
msgid "Here are some accounts for you to follow"
msgstr ""
msgstr "Aquí tens uns quants comptes que pots seguir"
#: src/screens/Onboarding/StepTopicalFeeds.tsx:85
msgid "Here are some popular topical feeds. You can choose to follow as many as you like."
msgstr ""
msgstr "Aquí tens alguns canals d'actualitat populars. Pots seguir-ne tants com vulguis."
#: src/screens/Onboarding/StepTopicalFeeds.tsx:80
msgid "Here are some topical feeds based on your interests: {interestsText}. You can choose to follow as many as you like."
msgstr ""
msgstr "Aquí tens uns quants canals d'actualitat basats en els teus interesos: {interestsText}. Pots seguir-ne tants com vulguis."
#: src/view/com/modals/AddAppPasswords.tsx:153
msgid "Here is your app password."
@ -1914,7 +1914,7 @@ msgstr "Si no en selecciones cap, és apropiat per a totes les edats."
#: src/view/com/modals/ChangePassword.tsx:146
msgid "If you want to change your password, we will send you a code to verify that this is your account."
msgstr ""
msgstr "Si vols canviar la contrasenya t'enviarem un codi per verificar que aquest compte és teu."
#: src/view/com/util/images/Gallery.tsx:38
msgid "Image"
@ -2024,7 +2024,7 @@ msgstr "Codis d'invitació: 1 disponible"
#: src/screens/Onboarding/StepFollowingFeed.tsx:64
msgid "It shows posts from the people you follow as they happen."
msgstr ""
msgstr "Mostra les publicacions de les persones que segueixes cronològicament."
#: src/view/com/auth/HomeLoggedOutCTA.tsx:99
#: src/view/com/auth/SplashScreen.web.tsx:138
@ -2046,7 +2046,7 @@ msgstr "Feines"
#: src/screens/Onboarding/index.tsx:24
msgid "Journalism"
msgstr ""
msgstr "Periodisme"
#: src/view/com/composer/select-language/SelectLangBtn.tsx:104
msgid "Language selection"
@ -2101,7 +2101,7 @@ msgstr "Sortint de Bluesky"
#: src/screens/Deactivated.tsx:128
msgid "left to go."
msgstr ""
msgstr "queda."
#: src/view/screens/Settings/index.tsx:278
msgid "Legacy storage cleared, you need to restart the app now."
@ -2114,7 +2114,7 @@ msgstr "Restablirem la teva contrasenya!"
#: src/screens/Onboarding/StepFinished.tsx:151
msgid "Let's go!"
msgstr ""
msgstr "Som-hi!"
#: src/view/com/util/UserAvatar.tsx:248
#: src/view/com/util/UserBanner.tsx:62
@ -2140,7 +2140,7 @@ msgstr "Li ha agradat a"
#: src/view/screens/PostLikedBy.tsx:27
#: src/view/screens/ProfileFeedLikedBy.tsx:27
msgid "Liked By"
msgstr ""
msgstr "Li ha agradat a"
#: src/view/com/feeds/FeedSourceCard.tsx:279
msgid "Liked by {0} {1}"
@ -2152,7 +2152,7 @@ msgstr "Li ha agradat a {likeCount} {0}"
#: src/view/com/notifications/FeedItem.tsx:170
msgid "liked your custom feed"
msgstr ""
msgstr "els hi ha agradat el teu canal personalitzat"
#: src/view/com/notifications/FeedItem.tsx:171
#~ msgid "liked your custom feed{0}"
@ -2247,7 +2247,7 @@ msgstr "Registre"
#: src/screens/Deactivated.tsx:178
#: src/screens/Deactivated.tsx:181
msgid "Log out"
msgstr ""
msgstr "Desconnecta"
#: src/view/screens/Moderation.tsx:155
msgid "Logged-out visibility"
@ -2477,7 +2477,7 @@ msgstr "Els meus canals desats"
#: src/view/com/auth/server-input/index.tsx:118
msgid "my-server.com"
msgstr ""
msgstr "el-meu-servidor.com"
#: src/view/com/modals/AddAppPasswords.tsx:179
#: src/view/com/modals/CreateOrEditList.tsx:290
@ -2490,7 +2490,7 @@ msgstr "Es requereix un nom"
#: src/screens/Onboarding/index.tsx:25
msgid "Nature"
msgstr ""
msgstr "Natura"
#: src/view/com/auth/login/ForgotPasswordForm.tsx:190
#: src/view/com/auth/login/ForgotPasswordForm.tsx:219
@ -2516,7 +2516,7 @@ msgstr "No perdis mai accés als teus seguidors ni a les teves dades."
#: src/screens/Onboarding/StepFinished.tsx:119
msgid "Never lose access to your followers or data."
msgstr ""
msgstr "No perdis mai accés als teus seguidors i les teves dades."
#: src/components/dialogs/MutedWords.tsx:293
msgid "Nevermind"
@ -2541,7 +2541,7 @@ msgstr "Nova contrasenya"
#: src/view/com/modals/ChangePassword.tsx:215
msgid "New Password"
msgstr ""
msgstr "Nova contrasenya"
#: src/view/com/feeds/FeedPage.tsx:126
msgctxt "action"
@ -2577,7 +2577,7 @@ msgstr "Les respostes més noves primer"
#: src/screens/Onboarding/index.tsx:23
msgid "News"
msgstr ""
msgstr "Notícies"
#: src/view/com/auth/create/CreateAccount.tsx:172
#: src/view/com/auth/login/ForgotPasswordForm.tsx:182
@ -2687,7 +2687,7 @@ msgstr "Ostres!"
#: src/screens/Onboarding/StepInterests/index.tsx:128
msgid "Oh no! Something went wrong."
msgstr ""
msgstr "Ostres! Alguna cosa ha fallat."
#: src/view/com/auth/login/PasswordUpdatedForm.tsx:41
msgid "Okay"
@ -2721,7 +2721,7 @@ msgstr "Ostres!"
#: src/screens/Onboarding/StepFinished.tsx:115
msgid "Open"
msgstr ""
msgstr "Obre"
#: src/view/screens/Moderation.tsx:75
msgid "Open content filtering settings"
@ -2750,7 +2750,7 @@ msgstr ""
#: src/view/screens/Settings/index.tsx:804
msgid "Open storybook page"
msgstr ""
msgstr "Obre la pàgina d'historial"
#: src/view/com/util/forms/DropdownButton.tsx:154
msgid "Opens {numItems} options"
@ -2876,7 +2876,7 @@ msgstr "Pàgina no trobada"
#: src/view/screens/NotFound.tsx:42
msgid "Page Not Found"
msgstr ""
msgstr "Pàgina no trobada"
#: src/view/com/auth/create/Step1.tsx:191
#: src/view/com/auth/create/Step1.tsx:201
@ -2912,7 +2912,7 @@ msgstr "S'ha denegat el permís per accedir a la càmera. Activa'l a la configur
#: src/screens/Onboarding/index.tsx:31
msgid "Pets"
msgstr ""
msgstr "Mascotes"
#: src/view/com/auth/create/Step2.tsx:183
#~ msgid "Phone number"
@ -3010,7 +3010,7 @@ msgstr "Espera que es generi la targeta de l'enllaç"
#: src/screens/Onboarding/index.tsx:37
msgid "Politics"
msgstr ""
msgstr "Política"
#: src/view/com/modals/SelfLabel.tsx:111
msgid "Porn"
@ -3129,7 +3129,7 @@ msgstr "Protegeix el teu compte verificant el teu correu."
#: src/screens/Onboarding/StepFinished.tsx:101
msgid "Public"
msgstr ""
msgstr "Públic"
#: src/view/screens/ModerationModlists.tsx:61
msgid "Public, shareable lists of users to mute or block in bulk."
@ -3300,7 +3300,7 @@ msgstr "Informa de la publicació"
#: src/view/com/util/post-ctrls/RepostButton.tsx:61
msgctxt "action"
msgid "Repost"
msgstr "Respon"
msgstr "Republica"
#: src/view/com/util/post-ctrls/RepostButton.web.tsx:48
msgid "Repost"
@ -3317,11 +3317,11 @@ msgstr "Republica o cita la publicació"
#: src/view/screens/PostRepostedBy.tsx:27
msgid "Reposted By"
msgstr ""
msgstr "Republicat per"
#: src/view/com/posts/FeedItem.tsx:207
msgid "Reposted by {0}"
msgstr ""
msgstr "Republicat per {0}"
#: src/view/com/posts/FeedItem.tsx:206
#~ msgid "Reposted by {0})"
@ -3351,7 +3351,7 @@ msgstr "Demana un canvi"
#: src/view/com/modals/ChangePassword.tsx:239
#: src/view/com/modals/ChangePassword.tsx:241
msgid "Request Code"
msgstr ""
msgstr "Demana un codi"
#: src/view/screens/Settings/index.tsx:456
msgid "Require alt text before posting"
@ -3368,7 +3368,7 @@ msgstr "Codi de restabliment"
#: src/view/com/modals/ChangePassword.tsx:190
msgid "Reset Code"
msgstr ""
msgstr "Codi de restabliment"
#: src/view/screens/Settings/index.tsx:824
msgid "Reset onboarding"
@ -3475,7 +3475,7 @@ msgstr "Desa el canvi d'identificador a {handle}"
#: src/screens/Onboarding/index.tsx:36
msgid "Science"
msgstr ""
msgstr "Ciència"
#: src/view/screens/ProfileList.tsx:859
msgid "Scroll to top"
@ -3584,19 +3584,19 @@ msgstr "Selecciona el servei"
#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:52
msgid "Select some accounts below to follow"
msgstr ""
msgstr "Selecciona alguns d'aquests comptes per seguir-los"
#: src/view/com/auth/server-input/index.tsx:82
msgid "Select the service that hosts your data."
msgstr ""
msgstr "Selecciona el servei que allotja les teves dades."
#: src/screens/Onboarding/StepTopicalFeeds.tsx:96
msgid "Select topical feeds to follow from the list below"
msgstr ""
msgstr "Selecciona els canals d'actualitat per seguir d'aquesta llista"
#: src/screens/Onboarding/StepModeration/index.tsx:75
msgid "Select what you want to see (or not see), and well handle the rest."
msgstr ""
msgstr "Selecciona què vols veure (o què no vols veure) i nosaltres farem la resta."
#: src/view/screens/LanguageSettings.tsx:281
msgid "Select which languages you want your subscribed feeds to include. If none are selected, all languages will be shown."
@ -3608,7 +3608,7 @@ msgstr "Selecciona l'idioma de l'aplicació perquè el text predeterminat es mos
#: src/screens/Onboarding/StepInterests/index.tsx:196
msgid "Select your interests from the options below"
msgstr ""
msgstr "Selecciona els teus interesos d'entre aquestes opcions"
#: src/view/com/auth/create/Step2.tsx:155
#~ msgid "Select your phone's country"
@ -3620,11 +3620,11 @@ msgstr "Selecciona el teu idioma preferit per a les traduccions al teu canal."
#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:116
msgid "Select your primary algorithmic feeds"
msgstr ""
msgstr "Selecciona els teus canals algorítmics primaris"
#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:142
msgid "Select your secondary algorithmic feeds"
msgstr ""
msgstr "Selecciona els teus canals algorítmics secundaris"
#: src/view/com/modals/VerifyEmail.tsx:202
#: src/view/com/modals/VerifyEmail.tsx:204
@ -3659,7 +3659,7 @@ msgstr "Envia un correu amb el codi de confirmació per l'eliminació del compte
#: src/view/com/auth/server-input/index.tsx:110
msgid "Server address"
msgstr ""
msgstr "Adreça del servidor"
#: src/view/com/modals/ContentFilteringSettings.tsx:311
msgid "Set {value} for {labelGroup} content moderation policy"
@ -3685,11 +3685,11 @@ msgstr "Estableix el tema de colors a la configuració del sistema"
#: src/view/screens/Settings/index.tsx:514
msgid "Set dark theme to the dark theme"
msgstr ""
msgstr "Posa el tema fosc"
#: src/view/screens/Settings/index.tsx:507
msgid "Set dark theme to the dim theme"
msgstr ""
msgstr "Posa el tema fosc al tema atenuat"
#: src/view/com/auth/login/SetNewPasswordForm.tsx:104
msgid "Set new password"
@ -3725,7 +3725,7 @@ msgstr ""
#: src/screens/Onboarding/Layout.tsx:50
msgid "Set up your account"
msgstr ""
msgstr "Configura el teu compte"
#: src/view/com/modals/ChangeHandle.tsx:266
msgid "Sets Bluesky username"
@ -3813,15 +3813,15 @@ msgstr "Mostra les publicacions citades"
#: src/screens/Onboarding/StepFollowingFeed.tsx:118
msgid "Show quote-posts in Following feed"
msgstr ""
msgstr "Mostra les publicacions citades en el canal Seguint"
#: src/screens/Onboarding/StepFollowingFeed.tsx:134
msgid "Show quotes in Following"
msgstr ""
msgstr "Mostra els citats a Seguint"
#: src/screens/Onboarding/StepFollowingFeed.tsx:94
msgid "Show re-posts in Following feed"
msgstr ""
msgstr "Mostra les republicacions al canal Seguint"
#: src/view/screens/PreferencesFollowingFeed.tsx:119
msgid "Show Replies"
@ -3833,11 +3833,11 @@ msgstr "Mostra les respostes dels comptes que segueixes abans que les altres."
#: src/screens/Onboarding/StepFollowingFeed.tsx:86
msgid "Show replies in Following"
msgstr ""
msgstr "Mostra les respostes a Seguint"
#: src/screens/Onboarding/StepFollowingFeed.tsx:70
msgid "Show replies in Following feed"
msgstr ""
msgstr "Mostrea les respostes al canal Seguint"
#: src/view/screens/PreferencesFollowingFeed.tsx:70
msgid "Show replies with at least {value} {0}"
@ -3849,7 +3849,7 @@ msgstr "Mostra republicacions"
#: src/screens/Onboarding/StepFollowingFeed.tsx:110
msgid "Show reposts in Following"
msgstr ""
msgstr "Mostra les republicacions al canal Seguint"
#: src/view/com/util/moderation/ContentHider.tsx:67
#: src/view/com/util/moderation/PostHider.tsx:61
@ -3949,7 +3949,7 @@ msgstr "Salta aquest pas"
#: src/screens/Onboarding/StepInterests/index.tsx:232
msgid "Skip this flow"
msgstr ""
msgstr "Salta aquest flux"
#: src/view/com/auth/create/Step2.tsx:82
#~ msgid "SMS verification"
@ -3957,7 +3957,7 @@ msgstr ""
#: src/screens/Onboarding/index.tsx:40
msgid "Software Dev"
msgstr ""
msgstr "Desenvolupament de programari"
#: src/view/com/modals/ProfilePreview.tsx:62
#~ msgid "Something went wrong and we're not sure what."
@ -3985,7 +3985,7 @@ msgstr "Ordena les respostes a la mateixa publicació per:"
#: src/screens/Onboarding/index.tsx:30
msgid "Sports"
msgstr ""
msgstr "Esports"
#: src/view/com/modals/crop-image/CropImage.web.tsx:122
msgid "Square"
@ -4023,7 +4023,7 @@ msgstr "Subscriure's"
#: src/screens/Onboarding/StepAlgoFeeds/FeedCard.tsx:173
#: src/screens/Onboarding/StepAlgoFeeds/FeedCard.tsx:308
msgid "Subscribe to the {0} feed"
msgstr ""
msgstr "Subscriu-te al canal {0}"
#: src/view/screens/ProfileList.tsx:604
msgid "Subscribe to this list"
@ -4095,7 +4095,7 @@ msgstr "Toca per veure-ho completament"
#: src/screens/Onboarding/index.tsx:39
msgid "Tech"
msgstr ""
msgstr "Tecnologia"
#: src/view/shell/desktop/RightNav.tsx:81
msgid "Terms"
@ -4135,7 +4135,7 @@ msgstr "La política de drets d'autoria ha estat traslladada a <0/>"
#: src/screens/Onboarding/Layout.tsx:60
msgid "The following steps will help customize your Bluesky experience."
msgstr ""
msgstr "Els següents passos t'ajudaran a personalitzar la teva experiència a Bluesky."
#: src/view/com/post-thread/PostThread.tsx:517
msgid "The post may have been deleted."
@ -4159,7 +4159,7 @@ msgstr "Les condicions del servei han estat traslladades a "
#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:150
msgid "There are many feeds to try:"
msgstr ""
msgstr "Hi ha molts canals per provar:"
#: src/view/screens/ProfileFeed.tsx:550
msgid "There was an an issue contacting the server, please check your internet connection and try again."
@ -4239,7 +4239,7 @@ msgstr "S'ha produït un problema inesperat a l'aplicació. Fes-nos saber si aix
#: src/screens/Deactivated.tsx:106
msgid "There's been a rush of new users to Bluesky! We'll activate your account as soon as we can."
msgstr ""
msgstr "Hi ha hagut una gran quantitat d'usuaris nous a Bluesky! Activarem el teu compte tan aviat com puguem."
#: src/view/com/auth/create/Step2.tsx:55
#~ msgid "There's something wrong with this number. Please choose your country and enter your full phone number!"
@ -4247,7 +4247,7 @@ msgstr ""
#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:138
msgid "These are popular accounts you might like:"
msgstr ""
msgstr "Aquests són alguns comptes populars que et poden agradar:"
#~ msgid "This {0} has been labeled."
#~ msgstr "Aquest {0} ha estat etiquetat."
@ -4274,7 +4274,7 @@ msgstr "Aquest contingut no es pot veure sense un compte de Bluesky."
#: src/view/screens/Settings/ExportCarDialog.tsx:75
msgid "This feature is in beta. You can read more about repository exports in <0>this blogpost.</0>"
msgstr ""
msgstr "Aquesta funcionalitat està en beta. En <0>aquesta entrada al blog</0> tens més informació."
#: src/view/com/posts/FeedErrorMessage.tsx:114
msgid "This feed is currently receiving high traffic and is temporarily unavailable. Please try again later."
@ -4328,7 +4328,7 @@ msgstr "Aquest usuari està inclós a la llista <0/> que tens bloquejada"
#: src/view/com/modals/ModerationDetails.tsx:74
msgid "This user is included in the <0/> list which you have muted."
msgstr ""
msgstr "Aquest usuari està inclòs a la llista <0/> que has silenciat."
#: src/view/com/modals/ModerationDetails.tsx:74
#~ msgid "This user is included the <0/> list which you have muted."
@ -4614,7 +4614,7 @@ msgstr "Verifica el teu correu"
#: src/screens/Onboarding/index.tsx:42
msgid "Video Games"
msgstr ""
msgstr "Videojocs"
#: src/view/com/profile/ProfileHeader.tsx:662
msgid "View {0}'s avatar"
@ -4647,7 +4647,7 @@ msgstr "Adverteix"
#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:134
msgid "We also think you'll like \"For You\" by Skygaze:"
msgstr ""
msgstr "També creiem que t'agradarà el canal \"For You\" d'Skygaze:"
#: src/screens/Hashtag.tsx:132
msgid "We couldn't find any results for that hashtag."
@ -4655,11 +4655,11 @@ msgstr ""
#: src/screens/Deactivated.tsx:133
msgid "We estimate {estimatedTime} until your account is ready."
msgstr ""
msgstr "Calculem {estimatedTime} fins que el teu compte estigui llest."
#: src/screens/Onboarding/StepFinished.tsx:93
msgid "We hope you have a wonderful time. Remember, Bluesky is:"
msgstr ""
msgstr "Esperem que t'ho passis pipa. Recorda que Bluesky és:"
#: src/view/com/posts/DiscoverFallbackHeader.tsx:29
msgid "We ran out of posts from your follows. Here's the latest from <0/>."
@ -4671,15 +4671,15 @@ msgstr ""
#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:124
msgid "We recommend our \"Discover\" feed:"
msgstr ""
msgstr "Et reomanem el nostre canal \"Discover\":"
#: src/screens/Onboarding/StepInterests/index.tsx:133
msgid "We weren't able to connect. Please try again to continue setting up your account. If it continues to fail, you can skip this flow."
msgstr ""
msgstr "No ens hem pogut connectar. Torna-ho a provar per continuar configurant el teu compte. Si continua fallant, pots ometre aquest flux."
#: src/screens/Deactivated.tsx:137
msgid "We will let you know when your account is ready."
msgstr ""
msgstr "T'informarem quan el teu compte estigui llest."
#: src/view/com/modals/AppealLabel.tsx:48
msgid "We'll look into your appeal promptly."
@ -4687,7 +4687,7 @@ msgstr "Analitzarem la teva apel·lació ràpidament."
#: src/screens/Onboarding/StepInterests/index.tsx:138
msgid "We'll use this to help customize your experience."
msgstr ""
msgstr "Ho farem servir per personalitzar la teva experiència."
#: src/view/com/auth/create/CreateAccount.tsx:134
msgid "We're so excited to have you join us!"
@ -4716,7 +4716,7 @@ msgstr "Benvingut a <0>Bluesky</0>"
#: src/screens/Onboarding/StepInterests/index.tsx:130
msgid "What are your interests?"
msgstr ""
msgstr "Quins són els teus interesos?"
#: src/view/com/modals/report/Modal.tsx:169
msgid "What is the issue with this {collectionName}?"
@ -4758,7 +4758,7 @@ msgstr "Escriu la teva resposta"
#: src/screens/Onboarding/index.tsx:28
msgid "Writers"
msgstr ""
msgstr "Escriptors"
#: src/view/com/auth/create/Step2.tsx:263
#~ msgid "XXXXXX"
@ -4776,7 +4776,7 @@ msgstr "Sí"
#: src/screens/Deactivated.tsx:130
msgid "You are in line."
msgstr ""
msgstr "Estàs a la cua."
#: src/view/com/posts/FollowingEmptyState.tsx:67
#: src/view/com/posts/FollowingEndOfFeed.tsx:68
@ -4789,7 +4789,7 @@ msgstr "També pots descobrir nous canals personalitzats per seguir."
#: src/screens/Onboarding/StepFollowingFeed.tsx:142
msgid "You can change these settings later."
msgstr ""
msgstr "Pots canviar aquests paràmetres més endavant."
#: src/view/com/auth/login/Login.tsx:158
#: src/view/com/auth/login/PasswordUpdatedForm.tsx:31
@ -4825,7 +4825,7 @@ msgstr "Has bloquejat aquest usuari. No pots veure el seu contingut."
#: src/view/com/modals/ChangePassword.tsx:87
#: src/view/com/modals/ChangePassword.tsx:121
msgid "You have entered an invalid code. It should look like XXXXX-XXXXX."
msgstr ""
msgstr "Has entrat un codi invàlid. Hauria de ser tipus XXXXX-XXXXX."
#: src/view/com/modals/ModerationDetails.tsx:87
msgid "You have muted this user."
@ -4862,7 +4862,7 @@ msgstr "Has de tenir 18 anys o més per habilitar el contingut per a adults."
#: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:103
msgid "You must be 18 years or older to enable adult content"
msgstr ""
msgstr "Has de tenir 18 anys o més per habilitar el contingut per a adults"
#: src/view/com/util/forms/PostDropdownBtn.tsx:147
msgid "You will no longer receive notifications for this thread"
@ -4878,17 +4878,17 @@ msgstr "Rebràs un correu amb un \"codi de restabliment\". Introdueix aquí el c
#: src/screens/Onboarding/StepModeration/index.tsx:72
msgid "You're in control"
msgstr ""
msgstr "Tu tens el control"
#: src/screens/Deactivated.tsx:87
#: src/screens/Deactivated.tsx:88
#: src/screens/Deactivated.tsx:103
msgid "You're in line"
msgstr ""
msgstr "Estàs a la cua"
#: src/screens/Onboarding/StepFinished.tsx:90
msgid "You're ready to go!"
msgstr ""
msgstr "Ja està tot llest!"
#: src/view/com/posts/FollowingEndOfFeed.tsx:48
msgid "You've reached the end of your feed! Find some more accounts to follow."
@ -4904,7 +4904,7 @@ msgstr "El teu compte s'ha eliminat"
#: src/view/screens/Settings/ExportCarDialog.tsx:47
msgid "Your account repository, containing all public data records, can be downloaded as a \"CAR\" file. This file does not include media embeds, such as images, or your private data, which must be fetched separately."
msgstr ""
msgstr "El repositori del teu compte, que conté tots els registres de dades públiques, es pot baixar com a fitxer \"CAR\". Aquest fitxer no inclou incrustacions multimèdia, com ara imatges, ni les teves dades privades, que s'han d'obtenir per separat."
#: src/view/com/auth/create/Step1.tsx:215
msgid "Your birth date"
@ -4916,7 +4916,7 @@ msgstr "La teva elecció es desarà, però es pot canviar més endavant a la con
#: src/screens/Onboarding/StepFollowingFeed.tsx:61
msgid "Your default feed is \"Following\""
msgstr ""
msgstr "El teu canal per defecte és \"Seguint\""
#: src/view/com/auth/create/state.ts:110
#: src/view/com/auth/login/ForgotPasswordForm.tsx:70
@ -4964,7 +4964,7 @@ msgstr ""
#: src/view/com/modals/ChangePassword.tsx:155
msgid "Your password has been changed successfully!"
msgstr ""
msgstr "S'ha canviat la teva contrasenya!"
#: src/view/com/composer/Composer.tsx:274
msgid "Your post has been published"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -8,8 +8,8 @@ msgstr ""
"Language: pt-BR\n"
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2024-02-08 19:59\n"
"Last-Translator: maisondasilva\n"
"PO-Revision-Date: 2024-03-12 11:36\n"
"Last-Translator: gildaswise\n"
"Language-Team: maisondasilva, MightyLoggor, gildaswise, gleydson, faeriarum\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@ -17,28 +17,10 @@ msgstr ""
msgid "(no email)"
msgstr "(sem email)"
#: src/view/shell/desktop/RightNav.tsx:168
#~ msgid "{0, plural, one {# invite code available} other {# invite codes available}}"
#~ msgstr "{0, plural, one {# convite disponível} other {# convites disponíveis}}"
#: src/view/com/profile/ProfileHeader.tsx:593
msgid "{following} following"
msgstr "{following} seguindo"
#: src/view/shell/desktop/RightNav.tsx:151
#~ msgid "{invitesAvailable, plural, one {Invite codes: # available} other {Invite codes: # available}}"
#~ msgstr "{invitesAvailable, plural, one {Convites: # disponível} other {Convites: # disponíveis}}"
#: src/view/screens/Settings.tsx:435
#: src/view/shell/Drawer.tsx:664
#~ msgid "{invitesAvailable} invite code available"
#~ msgstr "{invitesAvailable} convite disponível"
#: src/view/screens/Settings.tsx:437
#: src/view/shell/Drawer.tsx:666
#~ msgid "{invitesAvailable} invite codes available"
#~ msgstr "{invitesAvailable} convites disponíveis"
#: src/view/shell/Drawer.tsx:440
msgid "{numUnreadNotifications} unread"
msgstr "{numUnreadNotifications} não lidas"
@ -179,11 +161,11 @@ msgstr "Adicionar prévia de link:"
#: src/components/dialogs/MutedWords.tsx:158
msgid "Add mute word for configured settings"
msgstr ""
msgstr "Adicionar palavra silenciada para as configurações selecionadas"
#: src/components/dialogs/MutedWords.tsx:87
msgid "Add muted words and tags"
msgstr ""
msgstr "Adicionar palavras/tags silenciadas"
#: src/view/com/modals/ChangeHandle.tsx:417
msgid "Add the following DNS record to your domain:"
@ -223,17 +205,13 @@ msgstr "Conteúdo Adulto"
msgid "Adult content can only be enabled via the Web at <0/>."
msgstr "Conteúdo adulto só pode ser habilitado no site: <0/>."
#: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:78
#~ msgid "Adult content can only be enabled via the Web at <0>bsky.app</0>."
#~ msgstr "Conteúdo adulto só pode ser habilitado no site: <0>bsky.app</0>."
#: src/view/screens/Settings/index.tsx:664
msgid "Advanced"
msgstr "Avançado"
#: src/view/screens/Feeds.tsx:666
msgid "All the feeds you've saved, right in one place."
msgstr ""
msgstr "Todos os feeds que você salvou, em um único lugar."
#: src/view/com/auth/login/ForgotPasswordForm.tsx:221
#: src/view/com/modals/ChangePassword.tsx:168
@ -458,7 +436,7 @@ msgstr "Bluesky"
#: src/view/com/auth/server-input/index.tsx:150
msgid "Bluesky is an open network where you can choose your hosting provider. Custom hosting is now available in beta for developers."
msgstr ""
msgstr "Bluesky é uma rede aberta que permite a escolha do seu provedor de hospedagem. Desenvolvedores já conseguem utilizar a versão beta de hospedagem própria."
#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:80
#: src/view/com/auth/onboarding/WelcomeMobile.tsx:80
@ -483,10 +461,6 @@ msgstr "Bluesky é público."
msgid "Bluesky will not show your profile and posts to logged-out users. Other apps may not honor this request. This does not make your account private."
msgstr "O Bluesky não mostrará seu perfil e publicações para usuários desconectados. Outros aplicativos podem não honrar esta solicitação. Isso não torna a sua conta privada."
#: src/view/com/modals/ServerInput.tsx:78
#~ msgid "Bluesky.Social"
#~ msgstr "Bluesky.Social"
#: src/screens/Onboarding/index.tsx:33
msgid "Books"
msgstr "Livros"
@ -500,10 +474,6 @@ msgstr "Versão {0} {1}"
msgid "Business"
msgstr "Empresarial"
#: src/view/com/modals/ServerInput.tsx:115
#~ msgid "Button disabled. Input custom domain to proceed."
#~ msgstr "Botão desabilitado. Utilize um domínio personalizado para continuar."
#: src/view/com/profile/ProfileSubpageHeader.tsx:157
msgid "by —"
msgstr "por -"
@ -665,10 +635,6 @@ msgstr "Escolha os algoritmos que geram seus feeds customizados."
msgid "Choose the algorithms that power your experience with custom feeds."
msgstr "Escolha os algoritmos que fazem sentido para você com os feeds personalizados."
#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:103
#~ msgid "Choose your algorithmic feeds"
#~ msgstr "Escolha seus feeds algoritmicos"
#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:103
msgid "Choose your main feeds"
msgstr "Escolha seus feeds principais"
@ -706,11 +672,11 @@ msgstr "clique aqui"
#: src/components/TagMenu/index.web.tsx:138
msgid "Click here to open tag menu for {tag}"
msgstr ""
msgstr "Clique aqui para abrir o menu da tag {tag}"
#: src/components/RichText.tsx:191
msgid "Click here to open tag menu for #{tag}"
msgstr ""
msgstr "Clique aqui para abrir o menu da tag #{tag}"
#: src/screens/Onboarding/index.tsx:35
msgid "Climate"
@ -748,7 +714,7 @@ msgstr "Fechar o painel de navegação"
#: src/components/TagMenu/index.tsx:262
msgid "Close this dialog"
msgstr ""
msgstr "Fechar esta janela"
#: src/view/shell/index.web.tsx:52
msgid "Closes bottom navigation bar"
@ -789,7 +755,7 @@ msgstr "Completar e começar a usar sua conta"
#: src/view/com/auth/create/Step3.tsx:73
msgid "Complete the challenge"
msgstr ""
msgstr "Complete o captcha"
#: src/view/com/composer/Composer.tsx:424
msgid "Compose posts up to {MAX_GRAPHEME_LENGTH} characters in length"
@ -964,10 +930,6 @@ msgstr "Não foi possível carregar o feed"
msgid "Could not load list"
msgstr "Não foi possível carregar a lista"
#: src/view/com/auth/create/Step2.tsx:91
#~ msgid "Country"
#~ msgstr "País"
#: src/view/com/auth/HomeLoggedOutCTA.tsx:62
#: src/view/com/auth/SplashScreen.tsx:71
#: src/view/com/auth/SplashScreen.web.tsx:81
@ -1014,7 +976,7 @@ msgstr "Cultura"
#: src/view/com/auth/server-input/index.tsx:95
#: src/view/com/auth/server-input/index.tsx:96
msgid "Custom"
msgstr ""
msgstr "Customizado"
#: src/view/com/modals/ChangeHandle.tsx:389
msgid "Custom domain"
@ -1029,10 +991,6 @@ msgstr "Feeds customizados feitos pela comunidade te proporcionam novas experiê
msgid "Customize media from external sites."
msgstr "Configurar mídia de sites externos."
#: src/view/screens/Settings.tsx:687
#~ msgid "Danger Zone"
#~ msgstr "Zona Perigosa"
#: src/view/screens/Settings/index.tsx:485
#: src/view/screens/Settings/index.tsx:511
msgid "Dark"
@ -1072,10 +1030,6 @@ msgstr "Excluir Lista"
msgid "Delete my account"
msgstr "Excluir minha conta"
#: src/view/screens/Settings.tsx:706
#~ msgid "Delete my account…"
#~ msgstr "Excluir minha conta…"
#: src/view/screens/Settings/index.tsx:784
msgid "Delete My Account…"
msgstr "Excluir minha conta…"
@ -1133,13 +1087,9 @@ msgstr "Desencorajar aplicativos a mostrar minha conta para usuários deslogados
msgid "Discover new custom feeds"
msgstr "Descubra novos feeds"
#: src/view/screens/Feeds.tsx:473
#~ msgid "Discover new feeds"
#~ msgstr "Descubra novos feeds"
#: src/view/screens/Feeds.tsx:689
msgid "Discover New Feeds"
msgstr ""
msgstr "Descubra Novos Feeds"
#: src/view/com/modals/EditProfile.tsx:192
msgid "Display name"
@ -1196,12 +1146,12 @@ msgstr "Toque duas vezes para logar"
#: src/view/screens/Settings/index.tsx:755
msgid "Download Bluesky account data (repository)"
msgstr ""
msgstr "Baixar os dados da minha conta Bluesky (repositório)"
#: src/view/screens/Settings/ExportCarDialog.tsx:59
#: src/view/screens/Settings/ExportCarDialog.tsx:63
msgid "Download CAR file"
msgstr ""
msgstr "Baixar arquivo CAR"
#: src/view/com/composer/text-input/TextInput.web.tsx:249
msgid "Drop to add images"
@ -1360,7 +1310,7 @@ msgstr "Insira um nome para esta Senha de Aplicativo"
#: src/components/dialogs/MutedWords.tsx:100
#: src/components/dialogs/MutedWords.tsx:101
msgid "Enter a word or tag"
msgstr ""
msgstr "Digite uma palavra ou tag"
#: src/view/com/modals/VerifyEmail.tsx:105
msgid "Enter Confirmation Code"
@ -1399,17 +1349,13 @@ msgstr "Digite o novo e-mail acima"
msgid "Enter your new email address below."
msgstr "Digite seu novo endereço de e-mail abaixo."
#: src/view/com/auth/create/Step2.tsx:188
#~ msgid "Enter your phone number"
#~ msgstr "Digite seu número de telefone"
#: src/view/com/auth/login/Login.tsx:99
msgid "Enter your username and password"
msgstr "Digite seu nome de usuário e senha"
#: src/view/com/auth/create/Step3.tsx:67
msgid "Error receiving captcha response."
msgstr ""
msgstr "Não foi possível processar o captcha."
#: src/view/screens/Search/Search.tsx:110
msgid "Error:"
@ -1447,12 +1393,12 @@ msgstr "Mostrar ou esconder o post a que você está respondendo"
#: src/view/screens/Settings/index.tsx:753
msgid "Export my data"
msgstr ""
msgstr "Exportar meus dados"
#: src/view/screens/Settings/ExportCarDialog.tsx:44
#: src/view/screens/Settings/index.tsx:764
msgid "Export My Data"
msgstr ""
msgstr "Exportar Meus Dados"
#: src/view/com/modals/EmbedConsent.tsx:64
msgid "External Media"
@ -1523,14 +1469,6 @@ msgstr "Comentários"
msgid "Feeds"
msgstr "Feeds"
#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:106
#~ msgid "Feeds are created by users and can give you entirely new experiences."
#~ msgstr "Feeds são criados por usuários e podem te dar experiências completamente únicas."
#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:106
#~ msgid "Feeds are created by users and organizations. They offer you varied experiences and suggest content you may like using algorithms."
#~ msgstr "Feeds são criados por usuários ou organizações. Eles oferecem experiências únicas e podem te sugerir conteúdo usando algoritmos próprios."
#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:57
msgid "Feeds are created by users to curate content. Choose some feeds that you find interesting."
msgstr "Os feeds são criados por usuários para curadoria de conteúdo. Escolha alguns feeds que você acha interessantes."
@ -1567,11 +1505,7 @@ msgstr "Procurando contas semelhantes..."
#: src/view/screens/PreferencesFollowingFeed.tsx:111
msgid "Fine-tune the content you see on your Following feed."
msgstr ""
#: src/view/screens/PreferencesHomeFeed.tsx:111
#~ msgid "Fine-tune the content you see on your home screen."
#~ msgstr "Ajuste o conteúdo que você vê na sua tela inicial."
msgstr "Ajuste o conteúdo que você vê na sua tela inicial."
#: src/view/screens/PreferencesThreads.tsx:60
msgid "Fine-tune the discussion threads."
@ -1659,7 +1593,7 @@ msgstr "Seguindo {0}"
#: src/view/screens/PreferencesFollowingFeed.tsx:104
#: src/view/screens/Settings/index.tsx:543
msgid "Following Feed Preferences"
msgstr ""
msgstr "Configurações do feed principal"
#: src/view/com/profile/ProfileHeader.tsx:546
msgid "Follows you"
@ -1697,7 +1631,7 @@ msgstr "Esqueci a Senha"
#: src/screens/Hashtag.tsx:108
#: src/screens/Hashtag.tsx:148
msgid "From @{sanitizedAuthor}"
msgstr ""
msgstr "De @{sanitizedAuthor}"
#: src/view/com/posts/FeedItem.tsx:189
msgctxt "from-feed"
@ -1751,15 +1685,15 @@ msgstr "Usuário"
#: src/Navigation.tsx:270
msgid "Hashtag"
msgstr ""
msgstr "Hashtag"
#: src/components/RichText.tsx:188
#~ msgid "Hashtag: {tag}"
#~ msgstr ""
#~ msgstr "Hashtag: {tag}"
#: src/components/RichText.tsx:190
msgid "Hashtag: #{tag}"
msgstr ""
msgstr "Hashtag: #{tag}"
#: src/view/com/auth/create/CreateAccount.tsx:208
msgid "Having trouble?"
@ -1930,10 +1864,6 @@ msgstr "Insira a nova senha"
msgid "Input password for account deletion"
msgstr "Insira a senha para excluir a conta"
#: src/view/com/auth/create/Step2.tsx:196
#~ msgid "Input phone number for SMS verification"
#~ msgstr "Insira o número de telefone para verificação via SMS"
#: src/view/com/auth/login/LoginForm.tsx:230
msgid "Input the password tied to {identifier}"
msgstr "Insira a senha da conta {identifier}"
@ -1942,10 +1872,6 @@ msgstr "Insira a senha da conta {identifier}"
msgid "Input the username or email address you used at signup"
msgstr "Insira o usuário ou e-mail que você cadastrou"
#: src/view/com/auth/create/Step2.tsx:271
#~ msgid "Input the verification code we have texted to you"
#~ msgstr "Insira o código de verificação que enviamos para você"
#: src/view/com/modals/Waitlist.tsx:90
#~ msgid "Input your email to get on the Bluesky waitlist"
#~ msgstr "Insira seu e-mail para entrar na lista de espera do Bluesky"
@ -1966,10 +1892,6 @@ msgstr "Post inválido"
msgid "Invalid username or password"
msgstr "Credenciais inválidas"
#: src/view/screens/Settings.tsx:411
#~ msgid "Invite"
#~ msgstr "Convidar"
#: src/view/com/modals/InviteCodes.tsx:93
msgid "Invite a Friend"
msgstr "Convide um Amigo"
@ -1987,10 +1909,6 @@ msgstr "Convite inválido. Verifique se você o inseriu corretamente e tente nov
msgid "Invite codes: {0} available"
msgstr "Convites: {0} disponíveis"
#: src/view/shell/Drawer.tsx:645
#~ msgid "Invite codes: {invitesAvailable} available"
#~ msgstr "Convites: {invitesAvailable} disponível"
#: src/view/com/modals/InviteCodes.tsx:169
msgid "Invite codes: 1 available"
msgstr "Convites: 1 disponível"
@ -2232,15 +2150,15 @@ msgstr "Certifique-se de onde está indo!"
#: src/components/dialogs/MutedWords.tsx:83
msgid "Manage your muted words and tags"
msgstr ""
msgstr "Gerencie suas palavras/tags silenciadas"
#: src/view/com/auth/create/Step2.tsx:118
msgid "May not be longer than 253 characters"
msgstr ""
msgstr "Não pode ter mais que 253 caracteres"
#: src/view/com/auth/create/Step2.tsx:109
msgid "May only contain letters and numbers"
msgstr ""
msgstr "Só pode conter letras e números"
#: src/view/screens/Profile.tsx:182
msgid "Media"
@ -2332,15 +2250,15 @@ msgstr "Respostas mais curtidas primeiro"
#: src/view/com/auth/create/Step2.tsx:122
msgid "Must be at least 3 characters"
msgstr ""
msgstr "Deve ter no mínimo 3 caracteres"
#: src/components/TagMenu/index.tsx:249
msgid "Mute"
msgstr ""
msgstr "Silenciar"
#: src/components/TagMenu/index.web.tsx:105
msgid "Mute {truncatedTag}"
msgstr ""
msgstr "Silenciar {truncatedTag}"
#: src/view/com/profile/ProfileHeader.tsx:327
msgid "Mute Account"
@ -2352,19 +2270,19 @@ msgstr "Silenciar contas"
#: src/components/TagMenu/index.tsx:209
msgid "Mute all {displayTag} posts"
msgstr ""
msgstr "Silenciar posts com {displayTag}"
#: src/components/TagMenu/index.tsx:211
#~ msgid "Mute all {tag} posts"
#~ msgstr ""
#~ msgstr "Silenciar posts com {tag}"
#: src/components/dialogs/MutedWords.tsx:149
msgid "Mute in tags only"
msgstr ""
msgstr "Silenciar apenas as tags"
#: src/components/dialogs/MutedWords.tsx:134
msgid "Mute in text & tags"
msgstr ""
msgstr "Silenciar texto e tags"
#: src/view/screens/ProfileList.tsx:491
msgid "Mute list"
@ -2380,11 +2298,11 @@ msgstr "Silenciar esta lista"
#: src/components/dialogs/MutedWords.tsx:127
msgid "Mute this word in post text and tags"
msgstr ""
msgstr "Silenciar esta palavra no conteúdo de um post e tags"
#: src/components/dialogs/MutedWords.tsx:142
msgid "Mute this word in tags only"
msgstr ""
msgstr "Silenciar esta palavra apenas nas tags de um post"
#: src/view/com/util/forms/PostDropdownBtn.tsx:251
#: src/view/com/util/forms/PostDropdownBtn.tsx:257
@ -2394,7 +2312,7 @@ msgstr "Silenciar thread"
#: src/view/com/util/forms/PostDropdownBtn.tsx:267
#: src/view/com/util/forms/PostDropdownBtn.tsx:269
msgid "Mute words & tags"
msgstr ""
msgstr "Silenciar palavras/tags"
#: src/view/com/lists/ListCard.tsx:102
msgid "Muted"
@ -2415,7 +2333,7 @@ msgstr "Contas silenciadas não aparecem no seu feed ou nas suas notificações.
#: src/view/screens/Moderation.tsx:100
msgid "Muted words & tags"
msgstr ""
msgstr "Palavras/tags silenciadas"
#: src/view/screens/ProfileList.tsx:277
msgid "Muting is private. Muted accounts can interact with you, but you will not see their posts or receive notifications from them."
@ -2439,7 +2357,7 @@ msgstr "Meus Feeds Salvos"
#: src/view/com/auth/server-input/index.tsx:118
msgid "my-server.com"
msgstr ""
msgstr "meu-servidor.com.br"
#: src/view/com/modals/AddAppPasswords.tsx:179
#: src/view/com/modals/CreateOrEditList.tsx:290
@ -2482,7 +2400,7 @@ msgstr "Nunca perca o acesso aos seus seguidores ou dados."
#: src/components/dialogs/MutedWords.tsx:293
msgid "Nevermind"
msgstr ""
msgstr "Deixa pra lá"
#: src/view/screens/Lists.tsx:76
msgctxt "action"
@ -2587,7 +2505,7 @@ msgstr "Nenhum resultado"
#: src/components/Lists.tsx:192
msgid "No results found"
msgstr ""
msgstr "Nenhum resultado encontrado"
#: src/view/screens/Feeds.tsx:495
msgid "No results found for \"{query}\""
@ -2669,7 +2587,7 @@ msgstr "Apenas {0} pode responder."
#: src/components/Lists.tsx:82
msgid "Oops, something went wrong!"
msgstr ""
msgstr "Opa, algo deu errado!"
#: src/components/Lists.tsx:188
#: src/view/screens/AppPasswords.tsx:65
@ -2683,7 +2601,7 @@ msgstr "Abrir"
#: src/view/screens/Moderation.tsx:75
msgid "Open content filtering settings"
msgstr ""
msgstr "Abrir configurações de filtro"
#: src/view/com/composer/Composer.tsx:477
#: src/view/com/composer/Composer.tsx:478
@ -2696,7 +2614,7 @@ msgstr "Abrir links no navegador interno"
#: src/view/screens/Moderation.tsx:92
msgid "Open muted words settings"
msgstr ""
msgstr "Abrir configurações das palavras silenciadas"
#: src/view/com/home/HomeHeaderLayoutMobile.tsx:50
msgid "Open navigation"
@ -2704,7 +2622,7 @@ msgstr "Abrir navegação"
#: src/view/com/util/forms/PostDropdownBtn.tsx:175
msgid "Open post options menu"
msgstr ""
msgstr "Abrir opções do post"
#: src/view/screens/Settings/index.tsx:804
msgid "Open storybook page"
@ -2754,10 +2672,6 @@ msgstr "Abre lista de seguidores"
msgid "Opens following list"
msgstr "Abre lista de seguidos"
#: src/view/screens/Settings.tsx:412
#~ msgid "Opens invite code list"
#~ msgstr "Abre lista de convites"
#: src/view/com/modals/InviteCodes.tsx:172
msgid "Opens list of invite codes"
msgstr "Abre a lista de códigos de convite"
@ -2819,10 +2733,6 @@ msgstr "Ou combine estas opções:"
msgid "Other account"
msgstr "Outra conta"
#: src/view/com/modals/ServerInput.tsx:88
#~ msgid "Other service"
#~ msgstr "Outro serviço"
#: src/view/com/composer/select-language/SelectLangBtn.tsx:91
msgid "Other..."
msgstr "Outro..."
@ -2872,10 +2782,6 @@ msgstr "A permissão de galeria foi recusada. Por favor, habilite-a nas configur
msgid "Pets"
msgstr "Pets"
#: src/view/com/auth/create/Step2.tsx:183
#~ msgid "Phone number"
#~ msgstr "Número de telefone"
#: src/view/com/modals/SelfLabel.tsx:121
msgid "Pictures meant for adults."
msgstr "Imagens destinadas a adultos."
@ -2912,7 +2818,7 @@ msgstr "Por favor, escolha sua senha."
#: src/view/com/auth/create/state.ts:131
msgid "Please complete the verification captcha."
msgstr ""
msgstr "Por favor, complete o captcha de verificação."
#: src/view/com/modals/ChangeEmail.tsx:67
msgid "Please confirm your email before changing it. This is a temporary requirement while email-updating tools are added, and it will soon be removed."
@ -2922,17 +2828,13 @@ msgstr "Por favor, confirme seu e-mail antes de alterá-lo. Este é um requisito
msgid "Please enter a name for your app password. All spaces is not allowed."
msgstr "Por favor, insira um nome para a sua Senha de Aplicativo."
#: src/view/com/auth/create/Step2.tsx:206
#~ msgid "Please enter a phone number that can receive SMS text messages."
#~ msgstr "Por favor, insira um número de telefone que possa receber mensagens SMS."
#: src/view/com/modals/AddAppPasswords.tsx:145
msgid "Please enter a unique name for this App Password or use our randomly generated one."
msgstr "Por favor, insira um nome único para esta Senha de Aplicativo ou use nosso nome gerado automaticamente."
#: src/components/dialogs/MutedWords.tsx:68
msgid "Please enter a valid word, tag, or phrase to mute"
msgstr ""
msgstr "Por favor, insira uma palavra, tag ou frase para silenciar"
#: src/view/com/auth/create/state.ts:170
#~ msgid "Please enter the code you received by SMS."
@ -2955,11 +2857,6 @@ msgstr "Por favor, digite sua senha também:"
msgid "Please tell us why you think this content warning was incorrectly applied!"
msgstr "Por favor, diga-nos por que você acha que este aviso de conteúdo foi aplicado incorretamente!"
#: src/view/com/modals/AppealLabel.tsx:72
#: src/view/com/modals/AppealLabel.tsx:75
#~ msgid "Please tell us why you think this decision was incorrect."
#~ msgstr "Por favor, conte-nos por que achou que esta decisão está incorreta."
#: src/view/com/modals/VerifyEmail.tsx:101
msgid "Please Verify Your Email"
msgstr "Por favor, verifique seu e-mail"
@ -3019,7 +2916,7 @@ msgstr "Post não encontrado"
#: src/components/TagMenu/index.tsx:253
msgid "posts"
msgstr ""
msgstr "posts"
#: src/view/screens/Profile.tsx:180
msgid "Posts"
@ -3027,7 +2924,7 @@ msgstr "Posts"
#: src/components/dialogs/MutedWords.tsx:90
msgid "Posts can be muted based on their text, their tags, or both."
msgstr ""
msgstr "Posts podem ser silenciados baseados no seu conteúdo, tags ou ambos."
#: src/view/com/posts/FeedErrorMessage.tsx:64
msgid "Posts hidden"
@ -3171,7 +3068,7 @@ msgstr "Remover visualização da imagem"
#: src/components/dialogs/MutedWords.tsx:343
msgid "Remove mute word from your list"
msgstr ""
msgstr "Remover palavra silenciada da lista"
#: src/view/com/modals/Repost.tsx:47
msgid "Remove repost"
@ -3286,10 +3183,6 @@ msgstr "Reposts"
msgid "Request Change"
msgstr "Solicitar Alteração"
#: src/view/com/auth/create/Step2.tsx:219
#~ msgid "Request code"
#~ msgstr "Solicitar código"
#: src/view/com/modals/ChangePassword.tsx:239
#: src/view/com/modals/ChangePassword.tsx:241
msgid "Request Code"
@ -3368,10 +3261,6 @@ msgstr "Tente novamente"
msgid "Return to previous page"
msgstr "Voltar para página anterior"
#: src/view/shell/desktop/RightNav.tsx:55
#~ msgid "SANDBOX. Posts and accounts are not permanent."
#~ msgstr "SANDBOX. Posts e contas não são permanentes."
#: src/view/com/lightbox/Lightbox.tsx:132
#: src/view/com/modals/CreateOrEditList.tsx:345
msgctxt "action"
@ -3447,19 +3336,19 @@ msgstr "Pesquisar por \"{query}\""
#: src/components/TagMenu/index.tsx:145
msgid "Search for all posts by @{authorHandle} with tag {displayTag}"
msgstr ""
msgstr "Pesquisar por posts de @{authorHandle} com a tag {displayTag}"
#: src/components/TagMenu/index.tsx:145
#~ msgid "Search for all posts by @{authorHandle} with tag {tag}"
#~ msgstr ""
#~ msgstr "Pesquisar por posts de @{authorHandle} com a tag {tag}"
#: src/components/TagMenu/index.tsx:94
msgid "Search for all posts with tag {displayTag}"
msgstr ""
msgstr "Pesquisar por posts com a tag {displayTag}"
#: src/components/TagMenu/index.tsx:90
#~ msgid "Search for all posts with tag {tag}"
#~ msgstr ""
#~ msgstr "Pesquisar por posts com a tag {tag}"
#: src/view/com/auth/LoggedOut.tsx:104
#: src/view/com/auth/LoggedOut.tsx:105
@ -3473,27 +3362,27 @@ msgstr "Passo de Segurança Necessário"
#: src/components/TagMenu/index.web.tsx:66
msgid "See {truncatedTag} posts"
msgstr ""
msgstr "Ver posts com {truncatedTag}"
#: src/components/TagMenu/index.web.tsx:83
msgid "See {truncatedTag} posts by user"
msgstr ""
msgstr "Ver posts com {truncatedTag} deste usuário"
#: src/components/TagMenu/index.tsx:128
msgid "See <0>{displayTag}</0> posts"
msgstr ""
msgstr "Ver posts com <0>{displayTag}</0>"
#: src/components/TagMenu/index.tsx:187
msgid "See <0>{displayTag}</0> posts by this user"
msgstr ""
msgstr "Ver posts com <0>{displayTag}</0> deste usuário"
#: src/components/TagMenu/index.tsx:128
#~ msgid "See <0>{tag}</0> posts"
#~ msgstr ""
#~ msgstr "Ver posts com <0>{tag}</0>"
#: src/components/TagMenu/index.tsx:189
#~ msgid "See <0>{tag}</0> posts by this user"
#~ msgstr ""
#~ msgstr "Ver posts com <0>{tag}</0> deste usuário"
#: src/view/screens/SavedFeeds.tsx:163
msgid "See this guide"
@ -3507,10 +3396,6 @@ msgstr "Veja o que vem por aí"
msgid "Select {item}"
msgstr "Selecionar {item}"
#: src/view/com/modals/ServerInput.tsx:75
#~ msgid "Select Bluesky Social"
#~ msgstr "Selecionar Bluesky Social"
#: src/view/com/auth/login/Login.tsx:117
msgid "Select from an existing account"
msgstr "Selecionar de uma conta existente"
@ -3530,11 +3415,7 @@ msgstr "Selecione algumas contas para seguir"
#: src/view/com/auth/server-input/index.tsx:82
msgid "Select the service that hosts your data."
msgstr ""
#: src/screens/Onboarding/StepModeration/index.tsx:49
#~ msgid "Select the types of content that you want to see (or not see), and we'll handle the rest."
#~ msgstr "Selecione os tipos de conteúdo que você quer (ou não) ver, e cuidaremos do resto."
msgstr "Selecione o serviço que hospeda seus dados."
#: src/screens/Onboarding/StepTopicalFeeds.tsx:96
msgid "Select topical feeds to follow from the list below"
@ -3556,10 +3437,6 @@ msgstr "Selecione o idioma do seu aplicativo"
msgid "Select your interests from the options below"
msgstr "Selecione seus interesses"
#: src/view/com/auth/create/Step2.tsx:155
#~ msgid "Select your phone's country"
#~ msgstr "Selecione o país do número de telefone"
#: src/view/screens/LanguageSettings.tsx:190
msgid "Select your preferred language for translations in your feed."
msgstr "Selecione seu idioma preferido para as traduções no seu feed."
@ -3601,7 +3478,7 @@ msgstr "Envia o e-mail com o código de confirmação para excluir a conta"
#: src/view/com/auth/server-input/index.tsx:110
msgid "Server address"
msgstr ""
msgstr "URL do servidor"
#: src/view/com/modals/ContentFilteringSettings.tsx:311
msgid "Set {value} for {labelGroup} content moderation policy"
@ -3657,13 +3534,9 @@ msgstr "Defina esta configuração como \"Não\" para ocultar todos os reposts d
msgid "Set this setting to \"Yes\" to show replies in a threaded view. This is an experimental feature."
msgstr "Defina esta configuração como \"Sim\" para mostrar respostas em uma visualização de thread. Este é um recurso experimental."
#: src/view/screens/PreferencesHomeFeed.tsx:261
#~ msgid "Set this setting to \"Yes\" to show samples of your saved feeds in your following feed. This is an experimental feature."
#~ msgstr "Defina esta configuração como \"Sim\" para mostrar amostras de seus feeds salvos na sua página inicial. Este é um recurso experimental."
#: src/view/screens/PreferencesFollowingFeed.tsx:261
msgid "Set this setting to \"Yes\" to show samples of your saved feeds in your Following feed. This is an experimental feature."
msgstr ""
msgstr "Defina esta configuração como \"Sim\" para exibir amostras de seus feeds salvos no seu feed inicial. Este é um recurso experimental."
#: src/screens/Onboarding/Layout.tsx:50
msgid "Set up your account"
@ -3893,10 +3766,6 @@ msgstr "Pular"
msgid "Skip this flow"
msgstr "Pular"
#: src/view/com/auth/create/Step2.tsx:82
#~ msgid "SMS verification"
#~ msgstr "Verificação por SMS"
#: src/screens/Onboarding/index.tsx:40
msgid "Software Dev"
msgstr "Desenvolvimento de software"
@ -3907,7 +3776,7 @@ msgstr "Desenvolvimento de software"
#: src/components/Lists.tsx:203
msgid "Something went wrong!"
msgstr ""
msgstr "Algo deu errado!"
#: src/view/com/modals/Waitlist.tsx:51
#~ msgid "Something went wrong. Check your email and try again."
@ -3933,10 +3802,6 @@ msgstr "Esportes"
msgid "Square"
msgstr "Quadrado"
#: src/view/com/modals/ServerInput.tsx:62
#~ msgid "Staging"
#~ msgstr "Staging"
#: src/view/screens/Settings/index.tsx:871
msgid "Status page"
msgstr "Página de status"
@ -3989,10 +3854,6 @@ msgstr "Sugestivo"
msgid "Support"
msgstr "Suporte"
#: src/view/com/modals/ProfilePreview.tsx:110
#~ msgid "Swipe up to see more"
#~ msgstr "Deslize para cima para ver mais"
#: src/view/com/modals/SwitchAccount.tsx:117
msgid "Switch Account"
msgstr "Alterar Conta"
@ -4017,15 +3878,15 @@ msgstr "Log do sistema"
#: src/components/dialogs/MutedWords.tsx:337
msgid "tag"
msgstr ""
msgstr "tag"
#: src/components/TagMenu/index.tsx:78
msgid "Tag menu: {displayTag}"
msgstr ""
msgstr "Menu da tag: {displayTag}"
#: src/components/TagMenu/index.tsx:74
#~ msgid "Tag menu: {tag}"
#~ msgstr ""
#~ msgstr "Menu da tag: {tag}"
#: src/view/com/modals/crop-image/CropImage.web.tsx:112
msgid "Tall"
@ -4052,7 +3913,7 @@ msgstr "Termos de Serviço"
#: src/components/dialogs/MutedWords.tsx:337
msgid "text"
msgstr ""
msgstr "texto"
#: src/view/com/modals/AppealLabel.tsx:70
#: src/view/com/modals/report/InputIssueDetails.tsx:51
@ -4061,7 +3922,7 @@ msgstr "Campo de entrada de texto"
#: src/view/com/auth/create/CreateAccount.tsx:94
msgid "That handle is already taken."
msgstr ""
msgstr "Este identificador de usuário já está sendo usado."
#: src/view/com/profile/ProfileHeader.tsx:263
msgid "The account will be able to interact with you after unblocking."
@ -4179,10 +4040,6 @@ msgstr "Houve um problema inesperado no aplicativo. Por favor, deixe-nos saber s
msgid "There's been a rush of new users to Bluesky! We'll activate your account as soon as we can."
msgstr "Muitos usuários estão tentando acessar o Bluesky! Ativaremos sua conta assim que possível."
#: src/view/com/auth/create/Step2.tsx:55
#~ msgid "There's something wrong with this number. Please choose your country and enter your full phone number!"
#~ msgstr "Houve um problema com este número. Por favor, escolha um país e digite seu número de telefone completo!"
#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:138
msgid "These are popular accounts you might like:"
msgstr "Estas são contas populares que talvez você goste:"
@ -4209,7 +4066,7 @@ msgstr "Este conteúdo não é visível sem uma conta do Bluesky."
#: src/view/screens/Settings/ExportCarDialog.tsx:75
msgid "This feature is in beta. You can read more about repository exports in <0>this blogpost.</0>"
msgstr ""
msgstr "Esta funcionalidade está em beta. Você pode ler mais sobre exportação de repositórios <0>neste post</0> do nosso blog."
#: src/view/com/posts/FeedErrorMessage.tsx:114
msgid "This feed is currently receiving high traffic and is temporarily unavailable. Please try again later."
@ -4261,17 +4118,13 @@ msgstr "Este usuário está incluído na lista <0/>, que você bloqueou."
msgid "This user is included in the <0/> list which you have muted."
msgstr "Este usuário está incluído na lista <0/>, que você silenciou."
#: src/view/com/modals/ModerationDetails.tsx:74
#~ msgid "This user is included the <0/> list which you have muted."
#~ msgstr "Este usuário está incluído na lista <0/>, que você silenciou."
#: src/view/com/modals/SelfLabel.tsx:137
msgid "This warning is only available for posts with media attached."
msgstr "Este aviso só está disponível para publicações com mídia anexada."
#: src/components/dialogs/MutedWords.tsx:285
msgid "This will delete {0} from your muted words. You can always add it back later."
msgstr ""
msgstr "Isso removerá {0} das suas palavras silenciadas. Você pode adicioná-la novamente depois."
#: src/view/com/util/forms/PostDropdownBtn.tsx:282
msgid "This will hide this post from your feeds."
@ -4292,7 +4145,7 @@ msgstr "Preferências das Threads"
#: src/components/dialogs/MutedWords.tsx:113
msgid "Toggle between muted word options."
msgstr ""
msgstr "Alternar entre opções de uma palavra silenciada"
#: src/view/com/util/forms/DropdownButton.tsx:246
msgid "Toggle dropdown"
@ -4376,7 +4229,7 @@ msgstr "Dessilenciar"
#: src/components/TagMenu/index.web.tsx:104
msgid "Unmute {truncatedTag}"
msgstr ""
msgstr "Dessilenciar {truncatedTag}"
#: src/view/com/profile/ProfileHeader.tsx:326
msgid "Unmute Account"
@ -4384,11 +4237,11 @@ msgstr "Dessilenciar conta"
#: src/components/TagMenu/index.tsx:208
msgid "Unmute all {displayTag} posts"
msgstr ""
msgstr "Dessilenciar posts com {displayTag}"
#: src/components/TagMenu/index.tsx:210
#~ msgid "Unmute all {tag} posts"
#~ msgstr ""
#~ msgstr "Dessilenciar posts com {tag}"
#: src/view/com/util/forms/PostDropdownBtn.tsx:251
#: src/view/com/util/forms/PostDropdownBtn.tsx:256
@ -4446,10 +4299,6 @@ msgstr "Usar o meu navegador padrão"
msgid "Use this to sign into the other app along with your handle."
msgstr "Use esta senha para entrar no outro aplicativo juntamente com seu identificador."
#: src/view/com/modals/ServerInput.tsx:105
#~ msgid "Use your domain as your Bluesky client service provider"
#~ msgstr "Use seu domínio como o provedor de serviço do Bluesky"
#: src/view/com/modals/InviteCodes.tsx:200
msgid "Used by:"
msgstr "Usado por:"
@ -4514,10 +4363,6 @@ msgstr "usuários seguidos por <0/>"
msgid "Users in \"{0}\""
msgstr "Usuários em \"{0}\""
#: src/view/com/auth/create/Step2.tsx:243
#~ msgid "Verification code"
#~ msgstr "Código de verificação"
#: src/view/screens/Settings/index.tsx:910
msgid "Verify email"
msgstr "Verificar e-mail"
@ -4578,7 +4423,7 @@ msgstr "Também recomendamos o \"For You\", do Skygaze:"
#: src/screens/Hashtag.tsx:132
msgid "We couldn't find any results for that hashtag."
msgstr ""
msgstr "Não encontramos nenhum post com esta hashtag."
#: src/screens/Deactivated.tsx:133
msgid "We estimate {estimatedTime} until your account is ready."
@ -4598,7 +4443,7 @@ msgstr "Não temos mais posts de quem você segue. Aqui estão os mais novos de
#: src/components/dialogs/MutedWords.tsx:204
msgid "We recommend avoiding common words that appear in many posts, since it can result in no posts being shown."
msgstr ""
msgstr "Não recomendamos utilizar palavras comuns que aparecem em muitos posts, já que isso pode resultar em filtrar todos eles."
#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:124
msgid "We recommend our \"Discover\" feed:"
@ -4630,7 +4475,7 @@ msgstr "Tivemos um problema ao exibir esta lista. Se continuar acontecendo, cont
#: src/components/dialogs/MutedWords.tsx:230
msgid "We're sorry, but we weren't able to load your muted words at this time. Please try again."
msgstr ""
msgstr "Não foi possível carregar sua lista de palavras silenciadas. Por favor, tente novamente."
#: src/view/screens/Search/Search.tsx:254
msgid "We're sorry, but your search could not be completed. Please try again in a few minutes."
@ -4688,10 +4533,6 @@ msgstr "Escreva sua resposta"
msgid "Writers"
msgstr "Escritores"
#: src/view/com/auth/create/Step2.tsx:263
#~ msgid "XXXXXX"
#~ msgstr "XXXXXX"
#: src/view/com/composer/select-language/SuggestedLanguage.tsx:77
#: src/view/screens/PreferencesFollowingFeed.tsx:129
#: src/view/screens/PreferencesFollowingFeed.tsx:201
@ -4702,10 +4543,6 @@ msgstr "Escritores"
msgid "Yes"
msgstr "Sim"
#: src/screens/Onboarding/StepModeration/index.tsx:46
#~ msgid "You are in control"
#~ msgstr "Você está no controle"
#: src/screens/Deactivated.tsx:130
msgid "You are in line."
msgstr "Você está na fila."
@ -4715,10 +4552,6 @@ msgstr "Você está na fila."
msgid "You can also discover new Custom Feeds to follow."
msgstr "Você também pode descobrir novos feeds para seguir."
#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:123
#~ msgid "You can also try our \"Discover\" algorithm:"
#~ msgstr "Você também pode tentar nosso algoritmo \"Discover\":"
#: src/screens/Onboarding/StepFollowingFeed.tsx:142
msgid "You can change these settings later."
msgstr "Você pode mudar estas configurações depois."
@ -4786,7 +4619,7 @@ msgstr "Você ainda não silenciou nenhuma conta. Para silenciar uma conta, aces
#: src/components/dialogs/MutedWords.tsx:250
msgid "You haven't muted any words or tags yet"
msgstr ""
msgstr "Você não silenciou nenhuma palavra ou tag ainda"
#: src/view/com/modals/ContentFilteringSettings.tsx:175
msgid "You must be 18 or older to enable adult content."
@ -4836,7 +4669,7 @@ msgstr "Sua conta foi excluída"
#: src/view/screens/Settings/ExportCarDialog.tsx:47
msgid "Your account repository, containing all public data records, can be downloaded as a \"CAR\" file. This file does not include media embeds, such as images, or your private data, which must be fetched separately."
msgstr ""
msgstr "O repositório da sua conta, contendo todos os seus dados públicos, pode ser baixado como um arquivo \"CAR\". Este arquivo não inclui imagens ou dados privados, estes devem ser exportados separadamente."
#: src/view/com/auth/create/Step1.tsx:215
msgid "Your birth date"
@ -4888,7 +4721,7 @@ msgstr "Seu usuário completo será <0>@{0}</0>"
#: src/components/dialogs/MutedWords.tsx:221
msgid "Your muted words"
msgstr ""
msgstr "Suas palavras silenciadas"
#: src/view/com/modals/ChangePassword.tsx:155
msgid "Your password has been changed successfully!"

File diff suppressed because it is too large Load Diff

View File

@ -128,8 +128,8 @@ export default function HashtagScreen({
isError={isError}
isEmpty={posts.length < 1}
onRetry={refetch}
notFoundType="results"
empty={_(msg`We couldn't find any results for that hashtag.`)}
emptyTitle="results"
emptyMessage={_(msg`We couldn't find any results for that hashtag.`)}
/>
{!isLoading && posts.length > 0 && (
<List<PostView>

View File

@ -205,7 +205,6 @@ export function ModerationScreenInner({
)
return (
<View>
<ScrollView
contentContainerStyle={[
a.border_0,
@ -308,10 +307,7 @@ export function ModerationScreenInner({
</ButtonText>
</Button>
<BirthDateSettingsDialog
control={birthdateDialogControl}
preferences={preferences}
/>
<BirthDateSettingsDialog control={birthdateDialogControl} />
</>
)}
<View
@ -358,9 +354,7 @@ export function ModerationScreenInner({
<>
<GlobalModerationLabelPref labelValueDefinition={LABELS.porn} />
<Divider />
<GlobalModerationLabelPref
labelValueDefinition={LABELS.sexual}
/>
<GlobalModerationLabelPref labelValueDefinition={LABELS.sexual} />
<Divider />
<GlobalModerationLabelPref
labelValueDefinition={LABELS['graphic-media']}
@ -384,7 +378,9 @@ export function ModerationScreenInner({
</Text>
{isLabelersLoading ? (
<Loader />
<View style={[a.w_full, a.align_center, a.p_lg]}>
<Loader size="xl" />
</View>
) : labelersError || !labelers ? (
<View style={[a.p_lg, a.rounded_sm, t.atoms.bg_contrast_25]}>
<Text>
@ -415,7 +411,7 @@ export function ModerationScreenInner({
t.atoms.bg_contrast_50,
],
]}>
<LabelingService.Avatar />
<LabelingService.Avatar avatar={labeler.creator.avatar} />
<LabelingService.Content>
<LabelingService.Title
value={getLabelingServiceTitle({
@ -452,7 +448,6 @@ export function ModerationScreenInner({
<View style={{height: 200}} />
</ScrollView>
</View>
)
}
@ -520,11 +515,12 @@ function PwiOptOut() {
value={isOptedOut}
onChange={onToggleOptOut}
name="logged_out_visibility"
style={a.flex_1}
label={_(
msg`Discourage apps from showing my account to logged-out users`,
)}>
<Toggle.Switch />
<Toggle.Label style={[a.text_md]}>
<Toggle.Label style={[a.text_md, a.flex_1]}>
<Trans>
Discourage apps from showing my account to logged-out users
</Trans>

View File

@ -90,7 +90,9 @@ export function AdultContentEnabledPref({
a.align_center,
a.py_md,
]}>
<Text style={[a.font_bold]}>Enable Adult Content</Text>
<Text style={[a.font_bold]}>
<Trans>Enable Adult Content</Trans>
</Text>
<Toggle.Switch />
</View>
</Toggle.Item>
@ -111,7 +113,9 @@ export function AdultContentEnabledPref({
)}
<Prompt.Outer control={prompt}>
<Prompt.Title>Adult Content</Prompt.Title>
<Prompt.Title>
<Trans>Adult Content</Trans>
</Prompt.Title>
<Prompt.Description>
<Trans>
Due to Apple policies, adult content can only be enabled on the web
@ -119,7 +123,9 @@ export function AdultContentEnabledPref({
</Trans>
</Prompt.Description>
<Prompt.Actions>
<Prompt.Action onPress={() => prompt.close()}>OK</Prompt.Action>
<Prompt.Action onPress={() => prompt.close()}>
<Trans>OK</Trans>
</Prompt.Action>
</Prompt.Actions>
</Prompt.Outer>
</>

View File

@ -48,7 +48,6 @@ export const ProfileLabelsSection = React.forwardRef<
},
ref,
) {
const t = useTheme()
const {_} = useLingui()
const {height: minHeight} = useSafeAreaFrame()
@ -66,17 +65,7 @@ export const ProfileLabelsSection = React.forwardRef<
}))
return (
<CenteredView>
<View
style={[
a.border_l,
a.border_r,
a.border_t,
t.atoms.border_contrast_low,
{
minHeight,
},
]}>
<CenteredView style={{flex: 1, minHeight}} sideBorders>
{isLabelerLoading ? (
<View style={[a.w_full, a.align_center]}>
<Loader size="xl" />
@ -96,7 +85,6 @@ export const ProfileLabelsSection = React.forwardRef<
headerHeight={headerHeight}
/>
)}
</View>
</CenteredView>
)
})
@ -149,13 +137,7 @@ export function ProfileLabelsSectionInner({
}}
contentOffset={{x: 0, y: headerHeight * -1}}
onScroll={scrollHandler}>
<View
style={[
a.pt_xl,
a.px_lg,
isNative && a.border_t,
t.atoms.border_contrast_low,
]}>
<View style={[a.pt_xl, a.px_lg, a.border_t, t.atoms.border_contrast_low]}>
<View>
<Text style={[t.atoms.text_contrast_high, a.leading_snug, a.text_sm]}>
<Trans>

View File

@ -101,13 +101,7 @@ function computeSuggestions(
}
for (const item of searched) {
if (!items.find(item2 => item2.handle === item.handle)) {
items.push({
did: item.did,
handle: item.handle,
displayName: item.displayName,
avatar: item.avatar,
labels: item.labels,
})
items.push(item)
}
}
return items.filter(profile => {

View File

@ -5,6 +5,7 @@ import {
BskyFeedViewPreference,
ModerationOpts,
AppBskyActorDefs,
BSKY_LABELER_DID,
} from '@atproto/api'
import {track} from '#/lib/analytics/analytics'
@ -19,6 +20,7 @@ import {
DEFAULT_THREAD_VIEW_PREFS,
DEFAULT_LOGGED_OUT_PREFERENCES,
} from '#/state/queries/preferences/const'
import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/moderation'
import {STALE} from '#/state/queries'
import {useHiddenPosts, useLabelDefinitions} from '#/state/preferences'
import {saveLabelers} from '#/state/session/agent-config'
@ -95,7 +97,18 @@ export function useModerationOpts() {
}
return {
userDid: currentAccount?.did,
prefs: {...prefs.data.moderationPrefs, hiddenPosts: hiddenPosts || []},
prefs: {
...prefs.data.moderationPrefs,
labelers: prefs.data.moderationPrefs.labelers.length
? prefs.data.moderationPrefs.labelers
: [
{
did: BSKY_LABELER_DID,
labels: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES,
},
],
hiddenPosts: hiddenPosts || [],
},
labelDefs,
}
}, [override, currentAccount, labelDefs, prefs.data, hiddenPosts])

View File

@ -3,6 +3,7 @@ import {
AppBskyEmbedRecord,
AppBskyRichtextFacet,
ModerationDecision,
AppBskyActorDefs,
} from '@atproto/api'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
@ -10,11 +11,7 @@ export interface ComposerOptsPostRef {
uri: string
cid: string
text: string
author: {
handle: string
displayName?: string
avatar?: string
}
author: AppBskyActorDefs.ProfileViewBasic
embed?: AppBskyEmbedRecord.ViewRecord['embed']
moderation?: ModerationDecision
}

View File

@ -52,7 +52,9 @@ export function HomeLoggedOutCTA() {
onPress={showCreateAccount}
accessibilityRole="button"
accessibilityLabel={_(msg`Create new account`)}
accessibilityHint="Opens flow to create a new Bluesky account">
accessibilityHint={_(
msg`Opens flow to create a new Bluesky account`,
)}>
<Text
style={[
s.white,
@ -68,7 +70,9 @@ export function HomeLoggedOutCTA() {
onPress={showSignIn}
accessibilityRole="button"
accessibilityLabel={_(msg`Sign in`)}
accessibilityHint="Opens flow to sign into your existing Bluesky account">
accessibilityHint={_(
msg`Opens flow to sign into your existing Bluesky account`,
)}>
<Text
style={[
pal.text,

View File

@ -66,7 +66,9 @@ export const SplashScreen = ({
onPress={onPressCreateAccount}
accessibilityRole="button"
accessibilityLabel={_(msg`Create new account`)}
accessibilityHint="Opens flow to create a new Bluesky account">
accessibilityHint={_(
msg`Opens flow to create a new Bluesky account`,
)}>
<Text style={[s.white, styles.btnLabel]}>
<Trans>Create a new account</Trans>
</Text>
@ -77,7 +79,9 @@ export const SplashScreen = ({
onPress={onPressSignin}
accessibilityRole="button"
accessibilityLabel={_(msg`Sign in`)}
accessibilityHint="Opens flow to sign into your existing Bluesky account">
accessibilityHint={_(
msg`Opens flow to sign into your existing Bluesky account`,
)}>
<Text style={[pal.text, styles.btnLabel]}>
<Trans>Sign In</Trans>
</Text>

View File

@ -9,6 +9,8 @@ import {TextLink} from '../../util/Link'
import {Text} from '../../util/text/Text'
import {s, colors} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
@ -22,6 +24,7 @@ export const Policies = ({
under13: boolean
}) => {
const pal = usePalette('default')
const {_} = useLingui()
if (!serviceDescription) {
return <View />
}
@ -42,7 +45,9 @@ export const Policies = ({
/>
</View>
<Text style={[pal.textLight, s.pl5, s.flex1]}>
<Trans>
This service has not provided terms of service or a privacy policy.
</Trans>
</Text>
</View>
)
@ -53,7 +58,7 @@ export const Policies = ({
<TextLink
key="tos"
href={tos}
text="Terms of Service"
text={_(msg`Terms of Service`)}
style={[pal.link, s.underline]}
onPress={() => Linking.openURL(tos)}
/>,
@ -64,7 +69,7 @@ export const Policies = ({
<TextLink
key="pp"
href={pp}
text="Privacy Policy"
text={_(msg`Privacy Policy`)}
style={[pal.link, s.underline]}
onPress={() => Linking.openURL(pp)}
/>,
@ -83,7 +88,7 @@ export const Policies = ({
return (
<View style={styles.policies}>
<Text style={pal.textLight}>
By creating an account you agree to the {els}.
<Trans>By creating an account you agree to the {els}.</Trans>
</Text>
{under13 ? (
<Text style={[pal.textLight, s.bold]}>
@ -91,8 +96,10 @@ export const Policies = ({
</Text>
) : needsGuardian ? (
<Text style={[pal.textLight, s.bold]}>
<Trans>
If you are not yet an adult according to the laws of your country,
your parent or legal guardian must read these Terms on your behalf.
</Trans>
</Text>
) : undefined}
</View>

View File

@ -11,7 +11,8 @@ import {Text} from 'view/com/util/text/Text'
import Animated, {FadeInRight} from 'react-native-reanimated'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {useAnalytics} from 'lib/analytics/analytics'
import {Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro'
import {Shadow, useProfileShadow} from '#/state/cache/profile-shadow'
import {useProfileFollowMutationQueue} from '#/state/queries/profile'
import {logger} from '#/logger'
@ -70,6 +71,7 @@ function ProfileCard({
}) {
const {track} = useAnalytics()
const pal = usePalette('default')
const {_} = useLingui()
const [addingMoreSuggestions, setAddingMoreSuggestions] =
React.useState(false)
const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
@ -136,7 +138,7 @@ function ProfileCard({
type={profile.viewer?.following ? 'default' : 'inverted'}
labelStyle={styles.followButton}
onPress={onToggleFollow}
label={profile.viewer?.following ? 'Unfollow' : 'Follow'}
label={profile.viewer?.following ? _(msg`Unfollow`) : _(msg`Follow`)}
/>
</View>
{profile.description ? (

View File

@ -6,7 +6,8 @@ import {usePalette} from 'lib/hooks/usePalette'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Button} from 'view/com/util/forms/Button'
import {ViewHeader} from 'view/com/util/ViewHeader'
import {Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro'
type Props = {
next: () => void
@ -15,6 +16,7 @@ type Props = {
export function WelcomeMobile({next, skip}: Props) {
const pal = usePalette('default')
const {_} = useLingui()
return (
<View style={[styles.container]} testID="welcomeOnboarding">
@ -91,7 +93,7 @@ export function WelcomeMobile({next, skip}: Props) {
<Button
onPress={next}
label="Continue"
label={_(msg`Continue`)}
testID="continueBtn"
style={[styles.buttonContainer]}
labelStyle={styles.buttonText}

View File

@ -115,7 +115,7 @@ export function ServerInputDialog({
testID="customServerTextInput"
value={customAddress}
onChangeText={setCustomAddress}
label={_(msg`my-server.com`)}
label="my-server.com"
accessibilityLabelledBy="address-input-label"
autoCapitalize="none"
keyboardType="url"

View File

@ -415,7 +415,11 @@ export const ComposePost = observer(function ComposePost({
styles.textInputLayout,
isNative && styles.textInputLayoutMobile,
]}>
<UserAvatar avatar={currentProfile?.avatar} size={50} />
<UserAvatar
avatar={currentProfile?.avatar}
size={50}
type={currentProfile?.associated?.labeler ? 'labeler' : 'user'}
/>
<TextInput
ref={textInput}
richtext={richtext}

View File

@ -87,6 +87,7 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
avatar={replyTo.author.avatar}
size={50}
moderation={replyTo.moderation?.ui('avatar')}
type={replyTo.author.associated?.labeler ? 'labeler' : 'user'}
/>
<View style={styles.replyToPost}>
<Text type="xl-medium" style={[pal.text]}>

View File

@ -23,7 +23,11 @@ export function ComposePrompt({onPressCompose}: {onPressCompose: () => void}) {
accessibilityRole="button"
accessibilityLabel={_(msg`Compose reply`)}
accessibilityHint={_(msg`Opens composer`)}>
<UserAvatar avatar={profile?.avatar} size={38} />
<UserAvatar
avatar={profile?.avatar}
size={38}
type={profile?.associated?.labeler ? 'labeler' : 'user'}
/>
<Text
type="xl"
style={[

View File

@ -78,7 +78,11 @@ export function Autocomplete({
accessibilityLabel={`Select ${item.handle}`}
accessibilityHint="">
<View style={styles.avatarAndHandle}>
<UserAvatar avatar={item.avatar ?? null} size={24} />
<UserAvatar
avatar={item.avatar ?? null}
size={24}
type={item.associated?.labeler ? 'labeler' : 'user'}
/>
<Text type="md-medium" style={pal.text}>
{displayName}
</Text>

View File

@ -175,7 +175,11 @@ const MentionList = forwardRef<MentionListRef, SuggestionProps>(
}}
accessibilityRole="button">
<View style={styles.avatarAndDisplayName}>
<UserAvatar avatar={item.avatar ?? null} size={26} />
<UserAvatar
avatar={item.avatar ?? null}
size={26}
type={item.associated?.labeler ? 'labeler' : 'user'}
/>
<Text style={pal.text} numberOfLines={1}>
{displayName}
</Text>

View File

@ -78,9 +78,9 @@ function LightboxFooter({imageIndex}: {imageIndex: number}) {
try {
await saveImageToMediaLibrary({uri})
Toast.show('Saved to your camera roll.')
Toast.show(_(msg`Saved to your camera roll.`))
} catch (e: any) {
Toast.show(`Failed to save image: ${String(e)}`)
Toast.show(_(msg`Failed to save image: ${String(e)}`))
}
},
[permissionResponse, requestPermission, _],

View File

@ -150,7 +150,7 @@ export function Inner({
accessibilityHint={_(msg`Exits handle change process`)}
onAccessibilityEscape={onPressCancel}>
<Text type="lg" style={pal.textLight}>
Cancel
<Trans>Cancel</Trans>
</Text>
</TouchableOpacity>
</View>
@ -254,7 +254,7 @@ function ProvidedHandleForm({
<TextInput
testID="setHandleInput"
style={[pal.text, styles.textInput]}
placeholder="e.g. alice"
placeholder={_(msg`e.g. alice`)}
placeholderTextColor={pal.colors.textLight}
autoCapitalize="none"
keyboardAppearance={theme.colorScheme}
@ -277,8 +277,8 @@ function ProvidedHandleForm({
<TouchableOpacity
onPress={onToggleCustom}
accessibilityRole="button"
accessibilityHint="Hosting provider"
accessibilityLabel={_(msg`Opens modal for using custom domain`)}>
accessibilityLabel={_(msg`Hosting provider`)}
accessibilityHint={_(msg`Opens modal for using custom domain`)}>
<Text type="md-medium" style={[pal.link, s.pl10, s.pt5]}>
<Trans>I have my own domain</Trans>
</Text>
@ -324,8 +324,8 @@ function CustomHandleForm({
Clipboard.setString(
isDNSForm ? `did=${currentAccount.did}` : currentAccount.did,
)
Toast.show('Copied to clipboard')
}, [currentAccount, isDNSForm])
Toast.show(_(msg`Copied to clipboard`))
}, [currentAccount, isDNSForm, _])
const onChangeHandle = React.useCallback(
(v: string) => {
setHandle(v)
@ -378,7 +378,7 @@ function CustomHandleForm({
<TextInput
testID="setHandleInput"
style={[pal.text, styles.textInput]}
placeholder="e.g. alice.com"
placeholder={_(msg`e.g. alice.com`)}
placeholderTextColor={pal.colors.textLight}
autoCapitalize="none"
keyboardAppearance={theme.colorScheme}
@ -387,7 +387,7 @@ function CustomHandleForm({
editable={!isProcessing}
accessibilityLabelledBy="customDomain"
accessibilityLabel={_(msg`Custom domain`)}
accessibilityHint="Input your preferred hosting provider"
accessibilityHint={_(msg`Input your preferred hosting provider`)}
/>
</View>
<View style={styles.spacer} />
@ -395,18 +395,18 @@ function CustomHandleForm({
<View style={[styles.selectableBtns]}>
<SelectableBtn
selected={isDNSForm}
label="DNS Panel"
label={_(msg`DNS Panel`)}
left
onSelect={() => setDNSForm(true)}
accessibilityHint="Use the DNS panel"
accessibilityHint={_(msg`Use the DNS panel`)}
style={s.flex1}
/>
<SelectableBtn
selected={!isDNSForm}
label="No DNS Panel"
label={_(msg`No DNS Panel`)}
right
onSelect={() => setDNSForm(false)}
accessibilityHint="Use a file on your server"
accessibilityHint={_(msg`Use a file on your server`)}
style={s.flex1}
/>
</View>
@ -418,7 +418,7 @@ function CustomHandleForm({
</Text>
<View style={[styles.dnsTable, pal.btn]}>
<Text type="md-medium" style={[styles.dnsLabel, pal.text]}>
Host:
<Trans>Host:</Trans>
</Text>
<View style={[styles.dnsValue]}>
<Text type="mono" style={[styles.monoText, pal.text]}>
@ -426,7 +426,7 @@ function CustomHandleForm({
</Text>
</View>
<Text type="md-medium" style={[styles.dnsLabel, pal.text]}>
Type:
<Trans>Type:</Trans>
</Text>
<View style={[styles.dnsValue]}>
<Text type="mono" style={[styles.monoText, pal.text]}>
@ -434,7 +434,7 @@ function CustomHandleForm({
</Text>
</View>
<Text type="md-medium" style={[styles.dnsLabel, pal.text]}>
Value:
<Trans>Value:</Trans>
</Text>
<View style={[styles.dnsValue]}>
<Text type="mono" style={[styles.monoText, pal.text]}>
@ -443,7 +443,7 @@ function CustomHandleForm({
</View>
</View>
<Text type="md" style={[pal.text, s.pt20, s.pl5]}>
This should create a domain record at:{' '}
<Trans>This should create a domain record at:</Trans>
</Text>
<Text type="mono" style={[styles.monoText, pal.text, s.pt5, s.pl5]}>
_atproto.{handle}
@ -463,7 +463,7 @@ function CustomHandleForm({
</View>
<View style={styles.spacer} />
<Text type="md" style={[pal.text, s.pb5, s.pl5]}>
That contains the following:
<Trans>That contains the following:</Trans>
</Text>
<View style={[styles.valueContainer, pal.btn]}>
<View style={[styles.dnsValue]}>
@ -478,7 +478,9 @@ function CustomHandleForm({
<View style={styles.spacer} />
<Button type="default" style={[s.p20, s.mb10]} onPress={onPressCopy}>
<Text type="xl" style={[pal.link, s.textCenter]}>
Copy {isDNSForm ? 'Domain Value' : 'File Contents'}
<Trans>
Copy {isDNSForm ? _(msg`Domain Value`) : _(msg`File Contents`)}
</Trans>
</Text>
</Button>
{canSave === true && (
@ -504,8 +506,8 @@ function CustomHandleForm({
) : (
<Text type="xl-medium" style={[s.white, s.textCenter]}>
{canSave
? `Update to ${handle}`
: `Verify ${isDNSForm ? 'DNS Record' : 'Text File'}`}
? _(msg`Update to ${handle}`)
: _(msg`Verify ${isDNSForm ? 'DNS Record' : 'Text File'}`)}
</Text>
)}
</Button>
@ -513,9 +515,9 @@ function CustomHandleForm({
<TouchableOpacity
onPress={onToggleCustom}
accessibilityLabel={_(msg`Use default provider`)}
accessibilityHint="Use bsky.social as hosting provider">
accessibilityHint={_(msg`Use bsky.social as hosting provider`)}>
<Text type="md-medium" style={[pal.link, s.pl10, s.pt5]}>
Nevermind, create a handle for me
<Trans>Nevermind, create a handle for me</Trans>
</Text>
</TouchableOpacity>
</>

View File

@ -137,7 +137,9 @@ export function Component() {
<View>
<View style={styles.titleSection}>
<Text type="title-lg" style={[pal.text, styles.title]}>
{stage !== Stages.Done ? 'Change Password' : 'Password Changed'}
{stage !== Stages.Done
? _(msg`Change Password`)
: _(msg`Password Changed`)}
</Text>
</View>
@ -180,7 +182,7 @@ export function Component() {
<TextInput
testID="codeInput"
style={[pal.text, styles.textInput]}
placeholder="Reset code"
placeholder={_(msg`Reset code`)}
placeholderTextColor={pal.colors.textLight}
value={resetCode}
onChangeText={setResetCode}
@ -207,7 +209,7 @@ export function Component() {
<TextInput
testID="codeInput"
style={[pal.text, styles.textInput]}
placeholder="New password"
placeholder={_(msg`New password`)}
placeholderTextColor={pal.colors.textLight}
onChangeText={setNewPassword}
secureTextEntry

View File

@ -173,7 +173,7 @@ export function Component({}: {}) {
</Text>
<TextInput
style={[styles.textInput, pal.borderDark, pal.text, styles.mb20]}
placeholder="Confirmation code"
placeholder={_(msg`Confirmation code`)}
placeholderTextColor={pal.textLight.color}
keyboardAppearance={theme.colorScheme}
value={confirmCode}
@ -192,7 +192,7 @@ export function Component({}: {}) {
</Text>
<TextInput
style={[styles.textInput, pal.borderDark, pal.text]}
placeholder="Password"
placeholder={_(msg`Password`)}
placeholderTextColor={pal.textLight.color}
keyboardAppearance={theme.colorScheme}
secureTextEntry
@ -228,7 +228,7 @@ export function Component({}: {}) {
onPress={onCancel}
accessibilityRole="button"
accessibilityLabel={_(msg`Cancel account deletion`)}
accessibilityHint="Exits account deletion process"
accessibilityHint={_(msg`Exits account deletion process`)}
onAccessibilityEscape={onCancel}>
<Text type="button-lg" style={pal.textLight}>
<Trans context="action">Cancel</Trans>

View File

@ -77,7 +77,7 @@ export function Component({href}: {href: string}) {
}}
accessibilityLabel={_(msg`Cancel`)}
accessibilityHint=""
label="Cancel"
label={_(msg`Cancel`)}
labelContainerStyle={{justifyContent: 'center', padding: 8}}
labelStyle={[s.f18]}
/>

View File

@ -73,8 +73,8 @@ export function Component({text, href}: {text: string; href: string}) {
type="primary"
onPress={onPressVisit}
accessibilityLabel={_(msg`Visit Site`)}
accessibilityHint=""
label="Visit Site"
accessibilityHint={_(msg`Opens the linked website`)}
label={_(msg`Visit Site`)}
labelContainerStyle={{justifyContent: 'center', padding: 4}}
labelStyle={[s.f18]}
/>
@ -85,8 +85,8 @@ export function Component({text, href}: {text: string; href: string}) {
closeModal()
}}
accessibilityLabel={_(msg`Cancel`)}
accessibilityHint=""
label="Cancel"
accessibilityHint={_(msg`Cancels opening the linked website`)}
label={_(msg`Cancel`)}
labelContainerStyle={{justifyContent: 'center', padding: 4}}
labelStyle={[s.f18]}
/>

View File

@ -231,7 +231,11 @@ function UserResult({
width: 54,
paddingLeft: 4,
}}>
<UserAvatar size={40} avatar={profile.avatar} />
<UserAvatar
size={40}
avatar={profile.avatar}
type={profile.associated?.labeler ? 'labeler' : 'user'}
/>
</View>
<View
style={{

View File

@ -45,7 +45,11 @@ function SwitchAccountCard({account}: {account: SessionAccount}) {
const contents = (
<View style={[pal.view, styles.linkCard]}>
<View style={styles.avi}>
<UserAvatar size={40} avatar={profile?.avatar} />
<UserAvatar
size={40}
avatar={profile?.avatar}
type={profile?.associated?.labeler ? 'labeler' : 'user'}
/>
</View>
<View style={[s.flex1]}>
<Text type="md-bold" style={pal.text} numberOfLines={1}>

View File

@ -180,7 +180,7 @@ function ListItem({
},
]}>
<View style={styles.listItemAvi}>
<UserAvatar size={40} avatar={list.avatar} />
<UserAvatar size={40} avatar={list.avatar} type="list" />
</View>
<View style={styles.listItemContent}>
<Text

View File

@ -149,7 +149,7 @@ export function Component({showReminder}: {showReminder?: boolean}) {
onPress={onEmailIncorrect}
style={styles.changeEmailLink}>
<Text type="lg" style={pal.link}>
Change
<Trans>Change</Trans>
</Text>
</Pressable>
</>

View File

@ -100,7 +100,7 @@ export function Component({
onPress={doSetAs(AspectRatio.Wide)}
accessibilityRole="button"
accessibilityLabel={_(msg`Wide`)}
accessibilityHint="Sets image aspect ratio to wide">
accessibilityHint={_(msg`Sets image aspect ratio to wide`)}>
<RectWideIcon
size={24}
style={as === AspectRatio.Wide ? s.blue3 : pal.text}
@ -110,7 +110,7 @@ export function Component({
onPress={doSetAs(AspectRatio.Tall)}
accessibilityRole="button"
accessibilityLabel={_(msg`Tall`)}
accessibilityHint="Sets image aspect ratio to tall">
accessibilityHint={_(msg`Sets image aspect ratio to tall`)}>
<RectTallIcon
size={24}
style={as === AspectRatio.Tall ? s.blue3 : pal.text}
@ -120,7 +120,7 @@ export function Component({
onPress={doSetAs(AspectRatio.Square)}
accessibilityRole="button"
accessibilityLabel={_(msg`Square`)}
accessibilityHint="Sets image aspect ratio to square">
accessibilityHint={_(msg`Sets image aspect ratio to square`)}>
<SquareIcon
size={24}
style={as === AspectRatio.Square ? s.blue3 : pal.text}
@ -132,9 +132,9 @@ export function Component({
onPress={onPressCancel}
accessibilityRole="button"
accessibilityLabel={_(msg`Cancel image crop`)}
accessibilityHint="Exits image cropping process">
accessibilityHint={_(msg`Exits image cropping process`)}>
<Text type="xl" style={pal.link}>
Cancel
<Trans>Cancel</Trans>
</Text>
</TouchableOpacity>
<View style={s.flex1} />
@ -142,7 +142,7 @@ export function Component({
onPress={onPressDone}
accessibilityRole="button"
accessibilityLabel={_(msg`Save image crop`)}
accessibilityHint="Saves image crop settings">
accessibilityHint={_(msg`Saves image crop settings`)}>
<LinearGradient
colors={[gradients.blueLight.start, gradients.blueLight.end]}
start={{x: 0, y: 0}}

View File

@ -14,6 +14,7 @@ import {
ModerationDecision,
moderateProfile,
AppBskyEmbedRecordWithMedia,
AppBskyActorDefs,
} from '@atproto/api'
import {AtUri} from '@atproto/api'
import {
@ -55,6 +56,7 @@ interface Author {
displayName?: string
avatar?: string
moderation: ModerationDecision
associated?: AppBskyActorDefs.ProfileAssociated
}
let FeedItem = ({
@ -100,6 +102,7 @@ let FeedItem = ({
displayName: item.notification.author.displayName,
avatar: item.notification.author.avatar,
moderation: moderateProfile(item.notification.author, moderationOpts),
associated: item.notification.author.associated,
},
...(item.additional?.map(({author}) => {
return {
@ -109,6 +112,7 @@ let FeedItem = ({
displayName: author.displayName,
avatar: author.avatar,
moderation: moderateProfile(author, moderationOpts),
associated: author.associated,
}
}) || []),
]
@ -337,6 +341,7 @@ function CondensedAuthorsList({
handle={authors[0].handle}
avatar={authors[0].avatar}
moderation={authors[0].moderation.ui('avatar')}
type={authors[0].associated?.labeler ? 'labeler' : 'user'}
/>
</View>
)
@ -355,6 +360,7 @@ function CondensedAuthorsList({
size={35}
avatar={author.avatar}
moderation={author.moderation.ui('avatar')}
type={author.associated?.labeler ? 'labeler' : 'user'}
/>
</View>
))}
@ -413,6 +419,7 @@ function ExpandedAuthorsList({
size={35}
avatar={author.avatar}
moderation={author.moderation.ui('avatar')}
type={author.associated?.labeler ? 'labeler' : 'user'}
/>
</View>
<View style={s.flex1}>

View File

@ -1,25 +1,14 @@
import React, {useEffect, useRef} from 'react'
import {
ActivityIndicator,
Pressable,
StyleSheet,
TouchableOpacity,
View,
} from 'react-native'
import {StyleSheet, useWindowDimensions, View} from 'react-native'
import {AppBskyFeedDefs} from '@atproto/api'
import {CenteredView} from '../util/Views'
import {LoadingScreen} from '../util/LoadingScreen'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {List, ListMethods} from '../util/List'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {PostThreadItem} from './PostThreadItem'
import {ComposePrompt} from '../composer/Prompt'
import {ViewHeader} from '../util/ViewHeader'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {Text} from '../util/text/Text'
import {s} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {useSetTitle} from 'lib/hooks/useSetTitle'
import {
@ -30,21 +19,18 @@ import {
usePostThreadQuery,
sortThread,
} from '#/state/queries/post-thread'
import {useNavigation} from '@react-navigation/native'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {NavigationProp} from 'lib/routes/types'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {cleanError} from '#/lib/strings/errors'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {
UsePreferencesQueryResponse,
useModerationOpts,
usePreferencesQuery,
} from '#/state/queries/preferences'
import {useSession} from '#/state/session'
import {isAndroid, isNative, isWeb} from '#/platform/detection'
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
import {cleanError} from 'lib/strings/errors'
// FlatList maintainVisibleContentPosition breaks if too many items
// are prepended. This seems to be an optimal number based on *shrug*.
@ -58,9 +44,7 @@ const MAINTAIN_VISIBLE_CONTENT_POSITION = {
const TOP_COMPONENT = {_reactKey: '__top_component__'}
const REPLY_PROMPT = {_reactKey: '__reply__'}
const CHILD_SPINNER = {_reactKey: '__child_spinner__'}
const LOAD_MORE = {_reactKey: '__load_more__'}
const BOTTOM_COMPONENT = {_reactKey: '__bottom_component__'}
type YieldedItem = ThreadPost | ThreadBlocked | ThreadNotFound
type RowItem =
@ -68,9 +52,7 @@ type RowItem =
// TODO: TS doesn't actually enforce it's one of these, it only enforces matching shape.
| typeof TOP_COMPONENT
| typeof REPLY_PROMPT
| typeof CHILD_SPINNER
| typeof LOAD_MORE
| typeof BOTTOM_COMPONENT
type ThreadSkeletonParts = {
parents: YieldedItem[]
@ -78,6 +60,10 @@ type ThreadSkeletonParts = {
replies: YieldedItem[]
}
const keyExtractor = (item: RowItem) => {
return item._reactKey
}
export function PostThread({
uri,
onCanReply,
@ -85,17 +71,30 @@ export function PostThread({
}: {
uri: string | undefined
onCanReply: (canReply: boolean) => void
onPressReply: () => void
onPressReply: () => unknown
}) {
const {hasSession} = useSession()
const {_} = useLingui()
const pal = usePalette('default')
const {isMobile, isTabletOrMobile} = useWebMediaQueries()
const initialNumToRender = useInitialNumToRender()
const {height: windowHeight} = useWindowDimensions()
const {data: preferences} = usePreferencesQuery()
const {
isLoading,
isError,
error,
isFetching,
isError: isThreadError,
error: threadError,
refetch,
data: thread,
} = usePostThreadQuery(uri)
const {data: preferences} = usePreferencesQuery()
const treeView = React.useMemo(
() =>
!!preferences?.threadViewPrefs?.lab_treeViewEnabled &&
hasBranchingReplies(thread),
[preferences?.threadViewPrefs, thread],
)
const rootPost = thread?.type === 'post' ? thread.post : undefined
const rootPostRecord = thread?.type === 'post' ? thread.record : undefined
@ -105,7 +104,6 @@ export function PostThread({
rootPost && moderationOpts
? moderatePost(rootPost, moderationOpts)
: undefined
return !!mod
?.ui('contentList')
.blurs.find(
@ -114,6 +112,14 @@ export function PostThread({
)
}, [rootPost, moderationOpts])
// Values used for proper rendering of parents
const ref = useRef<ListMethods>(null)
const highlightedPostRef = useRef<View | null>(null)
const [maxParents, setMaxParents] = React.useState(
isWeb ? Infinity : PARENTS_CHUNK_SIZE,
)
const [maxReplies, setMaxReplies] = React.useState(50)
useSetTitle(
rootPost && !isNoPwi
? `${sanitizeDisplayName(
@ -121,62 +127,6 @@ export function PostThread({
)}: "${rootPostRecord!.text}"`
: '',
)
useEffect(() => {
if (rootPost) {
onCanReply(!rootPost.viewer?.replyDisabled)
}
}, [rootPost, onCanReply])
if (isError || AppBskyFeedDefs.isNotFoundPost(thread)) {
return (
<PostThreadError
error={error}
notFound={AppBskyFeedDefs.isNotFoundPost(thread)}
onRefresh={refetch}
/>
)
}
if (AppBskyFeedDefs.isBlockedPost(thread)) {
return <PostThreadBlocked />
}
if (!thread || isLoading || !preferences) {
return <LoadingScreen />
}
return (
<PostThreadLoaded
thread={thread}
threadViewPrefs={preferences.threadViewPrefs}
onRefresh={refetch}
onPressReply={onPressReply}
/>
)
}
function PostThreadLoaded({
thread,
threadViewPrefs,
onRefresh,
onPressReply,
}: {
thread: ThreadNode
threadViewPrefs: UsePreferencesQueryResponse['threadViewPrefs']
onRefresh: () => void
onPressReply: () => void
}) {
const {hasSession} = useSession()
const {_} = useLingui()
const pal = usePalette('default')
const {isMobile, isTabletOrMobile} = useWebMediaQueries()
const ref = useRef<ListMethods>(null)
const highlightedPostRef = useRef<View | null>(null)
const [maxParents, setMaxParents] = React.useState(
isWeb ? Infinity : PARENTS_CHUNK_SIZE,
)
const [maxReplies, setMaxReplies] = React.useState(100)
const treeView = React.useMemo(
() => !!threadViewPrefs.lab_treeViewEnabled && hasBranchingReplies(thread),
[threadViewPrefs, thread],
)
// On native, this is going to start out `true`. We'll toggle it to `false` after the initial render if flushed.
// This ensures that the first render contains no parents--even if they are already available in the cache.
@ -184,18 +134,56 @@ function PostThreadLoaded({
// On the web this is not necessary because we can synchronously adjust the scroll in onContentSizeChange instead.
const [deferParents, setDeferParents] = React.useState(isNative)
const skeleton = React.useMemo(
() =>
createThreadSkeleton(
const skeleton = React.useMemo(() => {
const threadViewPrefs = preferences?.threadViewPrefs
if (!threadViewPrefs || !thread) return null
return createThreadSkeleton(
sortThread(thread, threadViewPrefs),
hasSession,
treeView,
),
[thread, threadViewPrefs, hasSession, treeView],
)
}, [thread, preferences?.threadViewPrefs, hasSession, treeView])
const error = React.useMemo(() => {
if (AppBskyFeedDefs.isNotFoundPost(thread)) {
return {
title: _(msg`Post not found`),
message: _(msg`The post may have been deleted.`),
}
} else if (skeleton?.highlightedPost.type === 'blocked') {
return {
title: _(msg`Post hidden`),
message: _(
msg`You have blocked the author or you have been blocked by the author.`,
),
}
} else if (threadError?.message.startsWith('Post not found')) {
return {
title: _(msg`Post not found`),
message: _(msg`The post may have been deleted.`),
}
} else if (isThreadError) {
return {
message: threadError ? cleanError(threadError) : undefined,
}
}
return null
}, [thread, skeleton?.highlightedPost, isThreadError, _, threadError])
useEffect(() => {
if (error) {
onCanReply(false)
} else if (rootPost) {
onCanReply(!rootPost.viewer?.replyDisabled)
}
}, [rootPost, onCanReply, error])
// construct content
const posts = React.useMemo(() => {
if (!skeleton) return []
const {parents, highlightedPost, replies} = skeleton
let arr: RowItem[] = []
if (highlightedPost.type === 'post') {
@ -231,18 +219,12 @@ function PostThreadLoaded({
if (!highlightedPost.post.viewer?.replyDisabled) {
arr.push(REPLY_PROMPT)
}
if (highlightedPost.ctx.isChildLoading) {
arr.push(CHILD_SPINNER)
} else {
for (let i = 0; i < replies.length; i++) {
arr.push(replies[i])
if (i === maxReplies) {
arr.push(LOAD_MORE)
break
}
}
arr.push(BOTTOM_COMPONENT)
}
}
return arr
}, [skeleton, deferParents, maxParents, maxReplies])
@ -256,7 +238,7 @@ function PostThreadLoaded({
return
}
// wait for loading to finish
if (thread.type === 'post' && !!thread.parent) {
if (thread?.type === 'post' && !!thread.parent) {
function onMeasure(pageY: number) {
ref.current?.scrollToOffset({
animated: false,
@ -280,10 +262,10 @@ function PostThreadLoaded({
// To work around this, we prepend rows after scroll bumps against the top and rests.
const needsBumpMaxParents = React.useRef(false)
const onStartReached = React.useCallback(() => {
if (maxParents < skeleton.parents.length) {
if (skeleton?.parents && maxParents < skeleton.parents.length) {
needsBumpMaxParents.current = true
}
}, [maxParents, skeleton.parents.length])
}, [maxParents, skeleton?.parents])
const bumpMaxParentsIfNeeded = React.useCallback(() => {
if (!isNative) {
return
@ -296,6 +278,11 @@ function PostThreadLoaded({
const onMomentumScrollEnd = bumpMaxParentsIfNeeded
const onScrollToTop = bumpMaxParentsIfNeeded
const onEndReached = React.useCallback(() => {
if (isFetching || posts.length < maxReplies) return
setMaxReplies(prev => prev + 50)
}, [isFetching, maxReplies, posts.length])
const renderItem = React.useCallback(
({item, index}: {item: RowItem; index: number}) => {
if (item === TOP_COMPONENT) {
@ -326,46 +313,6 @@ function PostThreadLoaded({
</Text>
</View>
)
} else if (item === LOAD_MORE) {
return (
<Pressable
onPress={() => setMaxReplies(n => n + 50)}
style={[pal.border, pal.view, styles.itemContainer]}
accessibilityLabel={_(msg`Load more posts`)}
accessibilityHint="">
<View
style={[
pal.viewLight,
{paddingHorizontal: 18, paddingVertical: 14, borderRadius: 6},
]}>
<Text type="lg-medium" style={pal.text}>
<Trans>Load more posts</Trans>
</Text>
</View>
</Pressable>
)
} else if (item === BOTTOM_COMPONENT) {
// HACK
// due to some complexities with how flatlist works, this is the easiest way
// I could find to get a border positioned directly under the last item
// -prf
return (
<View
// @ts-ignore web-only
style={{
// Leave enough space below that the scroll doesn't jump
height: isNative ? 600 : '100vh',
borderTopWidth: 1,
borderColor: pal.colors.border,
}}
/>
)
} else if (item === CHILD_SPINNER) {
return (
<View style={[pal.border, styles.childSpinner]}>
<ActivityIndicator />
</View>
)
} else if (isThreadPost(item)) {
const prev = isThreadPost(posts[index - 1])
? (posts[index - 1] as ThreadPost)
@ -374,7 +321,9 @@ function PostThreadLoaded({
? (posts[index - 1] as ThreadPost)
: undefined
const hasUnrevealedParents =
index === 0 && maxParents < skeleton.parents.length
index === 0 &&
skeleton?.parents &&
maxParents < skeleton.parents.length
return (
<View
ref={item.ctx.isHighlightedPost ? highlightedPostRef : undefined}
@ -391,9 +340,9 @@ function PostThreadLoaded({
showChildReplyLine={item.ctx.showChildReplyLine}
showParentReplyLine={item.ctx.showParentReplyLine}
hasPrecedingItem={
!!prev?.ctx.showChildReplyLine || hasUnrevealedParents
!!prev?.ctx.showChildReplyLine || !!hasUnrevealedParents
}
onPostReply={onRefresh}
onPostReply={refetch}
/>
</View>
)
@ -403,142 +352,62 @@ function PostThreadLoaded({
[
hasSession,
isTabletOrMobile,
_,
isMobile,
onPressReply,
pal.border,
pal.viewLight,
pal.textLight,
pal.view,
pal.text,
pal.colors.border,
posts,
onRefresh,
skeleton?.parents,
maxParents,
deferParents,
treeView,
skeleton.parents.length,
maxParents,
_,
refetch,
],
)
return (
<>
<ListMaybePlaceholder
isLoading={!preferences || !thread}
isError={!!error}
onRetry={refetch}
errorTitle={error?.title}
errorMessage={error?.message}
/>
{!error && thread && (
<List
ref={ref}
data={posts}
keyExtractor={item => item._reactKey}
renderItem={renderItem}
keyExtractor={keyExtractor}
onContentSizeChange={isNative ? undefined : onContentSizeChangeWeb}
onStartReached={onStartReached}
onEndReached={onEndReached}
onEndReachedThreshold={2}
onMomentumScrollEnd={onMomentumScrollEnd}
onScrollToTop={onScrollToTop}
maintainVisibleContentPosition={
isNative ? MAINTAIN_VISIBLE_CONTENT_POSITION : undefined
}
style={s.hContentRegion}
// @ts-ignore our .web version only -prf
desktopFixedHeight
removeClippedSubviews={isAndroid ? false : undefined}
ListFooterComponent={
<ListFooter
isFetching={isFetching}
onRetry={refetch}
// 300 is based on the minimum height of a post. This is enough extra height for the `maintainVisPos` to
// work without causing weird jumps on web or glitches on native
height={windowHeight - 200}
/>
}
initialNumToRender={initialNumToRender}
windowSize={11}
/>
)
}
function PostThreadBlocked() {
const {_} = useLingui()
const pal = usePalette('default')
const navigation = useNavigation<NavigationProp>()
const onPressBack = React.useCallback(() => {
if (navigation.canGoBack()) {
navigation.goBack()
} else {
navigation.navigate('Home')
}
}, [navigation])
return (
<CenteredView>
<View style={[pal.view, pal.border, styles.notFoundContainer]}>
<Text type="title-lg" style={[pal.text, s.mb5]}>
<Trans>Post hidden</Trans>
</Text>
<Text type="md" style={[pal.text, s.mb10]}>
<Trans>
You have blocked the author or you have been blocked by the author.
</Trans>
</Text>
<TouchableOpacity
onPress={onPressBack}
accessibilityRole="button"
accessibilityLabel={_(msg`Back`)}
accessibilityHint="">
<Text type="2xl" style={pal.link}>
<FontAwesomeIcon
icon="angle-left"
style={[pal.link as FontAwesomeIconStyle, s.mr5]}
size={14}
/>
<Trans context="action">Back</Trans>
</Text>
</TouchableOpacity>
</View>
</CenteredView>
)
}
function PostThreadError({
onRefresh,
notFound,
error,
}: {
onRefresh: () => void
notFound: boolean
error: Error | null
}) {
const {_} = useLingui()
const pal = usePalette('default')
const navigation = useNavigation<NavigationProp>()
const onPressBack = React.useCallback(() => {
if (navigation.canGoBack()) {
navigation.goBack()
} else {
navigation.navigate('Home')
}
}, [navigation])
if (notFound) {
return (
<CenteredView>
<View style={[pal.view, pal.border, styles.notFoundContainer]}>
<Text type="title-lg" style={[pal.text, s.mb5]}>
<Trans>Post not found</Trans>
</Text>
<Text type="md" style={[pal.text, s.mb10]}>
<Trans>The post may have been deleted.</Trans>
</Text>
<TouchableOpacity
onPress={onPressBack}
accessibilityRole="button"
accessibilityLabel={_(msg`Back`)}
accessibilityHint="">
<Text type="2xl" style={pal.link}>
<FontAwesomeIcon
icon="angle-left"
style={[pal.link as FontAwesomeIconStyle, s.mr5]}
size={14}
/>
<Trans>Back</Trans>
</Text>
</TouchableOpacity>
</View>
</CenteredView>
)
}
return (
<CenteredView>
<ErrorMessage message={cleanError(error)} onPressTryAgain={onRefresh} />
</CenteredView>
)}
</>
)
}
@ -558,7 +427,9 @@ function createThreadSkeleton(
node: ThreadNode,
hasSession: boolean,
treeView: boolean,
): ThreadSkeletonParts {
): ThreadSkeletonParts | null {
if (!node) return null
return {
parents: Array.from(flattenThreadParents(node, hasSession)),
highlightedPost: node,
@ -615,7 +486,10 @@ function hasPwiOptOut(node: ThreadPost) {
return !!node.post.author.labels?.find(l => l.val === '!no-unauthenticated')
}
function hasBranchingReplies(node: ThreadNode) {
function hasBranchingReplies(node?: ThreadNode) {
if (!node) {
return false
}
if (node.type !== 'post') {
return false
}
@ -629,20 +503,9 @@ function hasBranchingReplies(node: ThreadNode) {
}
const styles = StyleSheet.create({
notFoundContainer: {
margin: 10,
paddingHorizontal: 18,
paddingVertical: 14,
borderRadius: 6,
},
itemContainer: {
borderTopWidth: 1,
paddingHorizontal: 18,
paddingVertical: 18,
},
childSpinner: {
borderTopWidth: 1,
paddingTop: 40,
paddingBottom: 200,
},
})

View File

@ -205,11 +205,7 @@ let PostThreadItemLoaded = ({
uri: post.uri,
cid: post.cid,
text: record.text,
author: {
handle: post.author.handle,
displayName: post.author.displayName,
avatar: post.author.avatar,
},
author: post.author,
embed: post.embed,
moderation,
},
@ -256,6 +252,7 @@ let PostThreadItemLoaded = ({
handle={post.author.handle}
avatar={post.author.avatar}
moderation={moderation.ui('avatar')}
type={post.author.associated?.labeler ? 'labeler' : 'user'}
/>
</View>
<View style={styles.layoutContent}>
@ -452,6 +449,7 @@ let PostThreadItemLoaded = ({
handle={post.author.handle}
avatar={post.author.avatar}
moderation={moderation.ui('avatar')}
type={post.author.associated?.labeler ? 'labeler' : 'user'}
/>
{showChildReplyLine && (
@ -540,7 +538,7 @@ let PostThreadItemLoaded = ({
title={itemTitle}
noFeedback>
<Text type="sm-medium" style={pal.textLight}>
More
<Trans>More</Trans>
</Text>
<FontAwesomeIcon
icon="angle-right"

View File

@ -118,11 +118,7 @@ function PostInner({
uri: post.uri,
cid: post.cid,
text: record.text,
author: {
handle: post.author.handle,
displayName: post.author.displayName,
avatar: post.author.avatar,
},
author: post.author,
embed: post.embed,
moderation,
},
@ -144,6 +140,7 @@ function PostInner({
handle={post.author.handle}
avatar={post.author.avatar}
moderation={moderation.ui('avatar')}
type={post.author.associated?.labeler ? 'labeler' : 'user'}
/>
</View>
<View style={styles.layoutContent}>

View File

@ -126,11 +126,7 @@ let FeedItemInner = ({
uri: post.uri,
cid: post.cid,
text: record.text || '',
author: {
handle: post.author.handle,
displayName: post.author.displayName,
avatar: post.author.avatar,
},
author: post.author,
embed: post.embed,
moderation,
},
@ -243,6 +239,7 @@ let FeedItemInner = ({
handle={post.author.handle}
avatar={post.author.avatar}
moderation={moderation.ui('avatar')}
type={post.author.associated?.labeler ? 'labeler' : 'user'}
/>
{isThreadParent && (
<View

View File

@ -49,6 +49,7 @@ export function ProfileCard({
const pal = usePalette('default')
const profile = useProfileShadow(profileUnshadowed)
const moderationOpts = useModerationOpts()
const isLabeler = profile?.associated?.labeler
if (!moderationOpts) {
return null
}
@ -79,6 +80,7 @@ export function ProfileCard({
size={40}
avatar={profile.avatar}
moderation={moderation.ui('avatar')}
type={isLabeler ? 'labeler' : 'user'}
/>
</View>
<View style={styles.layoutContent}>
@ -101,7 +103,7 @@ export function ProfileCard({
/>
{!!profile.viewer?.followedBy && <View style={s.flexRow} />}
</View>
{renderButton ? (
{renderButton && !isLabeler ? (
<View style={styles.layoutButton}>{renderButton(profile)}</View>
) : undefined}
</View>
@ -223,6 +225,7 @@ function FollowersList({
avatar={f.avatar}
size={32}
moderation={mod.ui('avatar')}
type={f.associated?.labeler ? 'labeler' : 'user'}
/>
</View>
</View>

View File

@ -1,39 +1,66 @@
import React from 'react'
import {ActivityIndicator, StyleSheet, View} from 'react-native'
import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
import {CenteredView} from '../util/Views'
import {LoadingScreen} from '../util/LoadingScreen'
import {List} from '../util/List'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {ProfileCardWithFollowBtn} from './ProfileCard'
import {useProfileFollowersQuery} from '#/state/queries/profile-followers'
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {logger} from '#/logger'
import {cleanError} from '#/lib/strings/errors'
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
import {
ListFooter,
ListHeaderDesktop,
ListMaybePlaceholder,
} from '#/components/Lists'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useSession} from 'state/session'
import {View} from 'react-native'
function renderItem({item}: {item: ActorDefs.ProfileViewBasic}) {
return <ProfileCardWithFollowBtn key={item.did} profile={item} />
}
function keyExtractor(item: ActorDefs.ProfileViewBasic) {
return item.did
}
export function ProfileFollowers({name}: {name: string}) {
const {_} = useLingui()
const initialNumToRender = useInitialNumToRender()
const {currentAccount} = useSession()
const [isPTRing, setIsPTRing] = React.useState(false)
const {
data: resolvedDid,
isLoading: isDidLoading,
error: resolveError,
isFetching: isFetchingDid,
} = useResolveDidQuery(name)
const {
data,
isLoading: isFollowersLoading,
isFetching,
isFetched,
isFetchingNextPage,
hasNextPage,
fetchNextPage,
isError,
error,
refetch,
} = useProfileFollowersQuery(resolvedDid)
const isError = React.useMemo(
() => !!resolveError || !!error,
[resolveError, error],
)
const isMe = React.useMemo(() => {
return resolvedDid === currentAccount?.did
}, [resolvedDid, currentAccount?.did])
const followers = React.useMemo(() => {
if (data?.pages) {
return data.pages.flatMap(page => page.followers)
}
return []
}, [data])
const onRefresh = React.useCallback(async () => {
@ -47,7 +74,7 @@ export function ProfileFollowers({name}: {name: string}) {
}, [refetch, setIsPTRing])
const onEndReached = async () => {
if (isFetching || !hasNextPage || isError) return
if (isFetching || !hasNextPage || !!error) return
try {
await fetchNextPage()
} catch (err) {
@ -55,57 +82,38 @@ export function ProfileFollowers({name}: {name: string}) {
}
}
const renderItem = React.useCallback(
({item}: {item: ActorDefs.ProfileViewBasic}) => (
<ProfileCardWithFollowBtn key={item.did} profile={item} />
),
[],
)
if (isFetchingDid || !isFetched) {
return <LoadingScreen />
}
// error
// =
if (resolveError || isError) {
return (
<CenteredView>
<ErrorMessage
message={cleanError(resolveError || error)}
onPressTryAgain={onRefresh}
<View style={{flex: 1}}>
<ListMaybePlaceholder
isLoading={isDidLoading || isFollowersLoading}
isEmpty={followers.length < 1}
isError={isError}
emptyType="results"
emptyMessage={
isMe
? _(msg`You do not have any followers.`)
: _(msg`This user doesn't have any followers.`)
}
errorMessage={cleanError(resolveError || error)}
onRetry={isError ? refetch : undefined}
/>
</CenteredView>
)
}
// loaded
// =
return (
{followers.length > 0 && (
<List
data={followers}
keyExtractor={item => item.did}
renderItem={renderItem}
keyExtractor={keyExtractor}
refreshing={isPTRing}
onRefresh={onRefresh}
onEndReached={onEndReached}
renderItem={renderItem}
initialNumToRender={15}
// FIXME(dan)
// eslint-disable-next-line react/no-unstable-nested-components
ListFooterComponent={() => (
<View style={styles.footer}>
{(isFetching || isFetchingNextPage) && <ActivityIndicator />}
</View>
)}
onEndReachedThreshold={4}
ListHeaderComponent={<ListHeaderDesktop title={_(msg`Followers`)} />}
ListFooterComponent={<ListFooter isFetching={isFetchingNextPage} />}
// @ts-ignore our .web version only -prf
desktopFixedHeight
initialNumToRender={initialNumToRender}
windowSize={11}
/>
)}
</View>
)
}
const styles = StyleSheet.create({
footer: {
height: 200,
paddingTop: 20,
},
})

View File

@ -1,39 +1,65 @@
import React from 'react'
import {ActivityIndicator, StyleSheet, View} from 'react-native'
import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
import {CenteredView} from '../util/Views'
import {LoadingScreen} from '../util/LoadingScreen'
import {List} from '../util/List'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {ProfileCardWithFollowBtn} from './ProfileCard'
import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {logger} from '#/logger'
import {cleanError} from '#/lib/strings/errors'
import {
ListFooter,
ListHeaderDesktop,
ListMaybePlaceholder,
} from '#/components/Lists'
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
import {useSession} from 'state/session'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
function renderItem({item}: {item: ActorDefs.ProfileViewBasic}) {
return <ProfileCardWithFollowBtn key={item.did} profile={item} />
}
function keyExtractor(item: ActorDefs.ProfileViewBasic) {
return item.did
}
export function ProfileFollows({name}: {name: string}) {
const {_} = useLingui()
const initialNumToRender = useInitialNumToRender()
const {currentAccount} = useSession()
const [isPTRing, setIsPTRing] = React.useState(false)
const {
data: resolvedDid,
isLoading: isDidLoading,
error: resolveError,
isFetching: isFetchingDid,
} = useResolveDidQuery(name)
const {
data,
isLoading: isFollowsLoading,
isFetching,
isFetched,
isFetchingNextPage,
hasNextPage,
fetchNextPage,
isError,
error,
refetch,
} = useProfileFollowsQuery(resolvedDid)
const isError = React.useMemo(
() => !!resolveError || !!error,
[resolveError, error],
)
const isMe = React.useMemo(() => {
return resolvedDid === currentAccount?.did
}, [resolvedDid, currentAccount?.did])
const follows = React.useMemo(() => {
if (data?.pages) {
return data.pages.flatMap(page => page.follows)
}
return []
}, [data])
const onRefresh = React.useCallback(async () => {
@ -47,7 +73,7 @@ export function ProfileFollows({name}: {name: string}) {
}, [refetch, setIsPTRing])
const onEndReached = async () => {
if (isFetching || !hasNextPage || isError) return
if (isFetching || !hasNextPage || !!error) return
try {
await fetchNextPage()
} catch (err) {
@ -55,57 +81,38 @@ export function ProfileFollows({name}: {name: string}) {
}
}
const renderItem = React.useCallback(
({item}: {item: ActorDefs.ProfileViewBasic}) => (
<ProfileCardWithFollowBtn key={item.did} profile={item} />
),
[],
)
if (isFetchingDid || !isFetched) {
return <LoadingScreen />
}
// error
// =
if (resolveError || isError) {
return (
<CenteredView>
<ErrorMessage
message={cleanError(resolveError || error)}
onPressTryAgain={onRefresh}
<>
<ListMaybePlaceholder
isLoading={isDidLoading || isFollowsLoading}
isEmpty={follows.length < 1}
isError={isError}
emptyType="results"
emptyMessage={
isMe
? _(msg`You are not following anyone.`)
: _(msg`This user isn't following anyone.`)
}
errorMessage={cleanError(resolveError || error)}
onRetry={isError ? refetch : undefined}
/>
</CenteredView>
)
}
// loaded
// =
return (
{follows.length > 0 && (
<List
data={follows}
keyExtractor={item => item.did}
renderItem={renderItem}
keyExtractor={keyExtractor}
refreshing={isPTRing}
onRefresh={onRefresh}
onEndReached={onEndReached}
renderItem={renderItem}
initialNumToRender={15}
// FIXME(dan)
// eslint-disable-next-line react/no-unstable-nested-components
ListFooterComponent={() => (
<View style={styles.footer}>
{(isFetching || isFetchingNextPage) && <ActivityIndicator />}
</View>
)}
onEndReachedThreshold={4}
ListHeaderComponent={<ListHeaderDesktop title={_(msg`Following`)} />}
ListFooterComponent={<ListFooter isFetching={isFetchingNextPage} />}
// @ts-ignore our .web version only -prf
desktopFixedHeight
initialNumToRender={initialNumToRender}
windowSize={11}
/>
)}
</>
)
}
const styles = StyleSheet.create({
footer: {
height: 200,
paddingTop: 20,
},
})

View File

@ -21,7 +21,8 @@ import {useModerationOpts} from '#/state/queries/preferences'
import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {useProfileFollowMutationQueue} from '#/state/queries/profile'
import {Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro'
const OUTER_PADDING = 10
const INNER_PADDING = 14
@ -98,7 +99,9 @@ export function ProfileHeaderSuggestedFollows({
<SuggestedFollowSkeleton />
</>
) : data ? (
data.suggestions.map(profile => (
data.suggestions
.filter(s => (s.associated?.labeler ? false : true))
.map(profile => (
<SuggestedFollow key={profile.did} profile={profile} />
))
) : (
@ -168,6 +171,7 @@ function SuggestedFollow({
}) {
const {track} = useAnalytics()
const pal = usePalette('default')
const {_} = useLingui()
const moderationOpts = useModerationOpts()
const profile = useProfileShadow(profileUnshadowed)
const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
@ -181,20 +185,20 @@ function SuggestedFollow({
await queueFollow()
} catch (e: any) {
if (e?.name !== 'AbortError') {
Toast.show('An issue occurred, please try again.')
Toast.show(_(msg`An issue occurred, please try again.`))
}
}
}, [queueFollow, track])
}, [queueFollow, track, _])
const onPressUnfollow = React.useCallback(async () => {
try {
await queueUnfollow()
} catch (e: any) {
if (e?.name !== 'AbortError') {
Toast.show('An issue occurred, please try again.')
Toast.show(_(msg`An issue occurred, please try again.`))
}
}
}, [queueUnfollow])
}, [queueUnfollow, _])
if (!moderationOpts) {
return null
@ -239,7 +243,7 @@ function SuggestedFollow({
</View>
<Button
label={following ? 'Unfollow' : 'Follow'}
label={following ? _(msg`Unfollow`) : _(msg`Follow`)}
type="inverted"
labelStyle={{textAlign: 'center'}}
onPress={following ? onPressUnfollow : onPressFollow}

View File

@ -11,16 +11,11 @@ import {sanitizeHandle} from 'lib/strings/handles'
import {isAndroid, isWeb} from 'platform/detection'
import {TimeElapsed} from './TimeElapsed'
import {makeProfileLink} from 'lib/routes/links'
import {ModerationDecision, ModerationUI} from '@atproto/api'
import {AppBskyActorDefs, ModerationDecision, ModerationUI} from '@atproto/api'
import {usePrefetchProfileQuery} from '#/state/queries/profile'
interface PostMetaOpts {
author: {
avatar?: string
did: string
handle: string
displayName?: string | undefined
}
author: AppBskyActorDefs.ProfileViewBasic
moderation: ModerationDecision | undefined
authorHasWarning: boolean
postHref: string
@ -47,6 +42,7 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
avatar={opts.author.avatar}
size={opts.avatarSize || 16}
moderation={opts.avatarModeration}
type={opts.author.associated?.labeler ? 'labeler' : 'user'}
/>
</View>
)}

View File

@ -97,7 +97,7 @@ export function LanguageSettingsScreen(_props: Props) {
<Text style={[pal.text, s.pb10]}>
<Trans>
Select your app language for the default text to display in the
app
app.
</Trans>
</Text>
@ -296,7 +296,7 @@ export function LanguageSettingsScreen(_props: Props) {
type="button"
style={[pal.text, {flexShrink: 1, overflow: 'hidden'}]}
numberOfLines={1}>
{myLanguages.length ? myLanguages : 'Select languages'}
{myLanguages.length ? myLanguages : _(msg`Select languages`)}
</Text>
</Button>
</View>

View File

@ -51,7 +51,13 @@ export const NotFoundScreen = () => {
</Text>
<Button
type="primary"
label={canGoBack ? 'Go back' : 'Go home'}
label={canGoBack ? _(msg`Go Back`) : _(msg`Go Home`)}
accessibilityLabel={canGoBack ? _(msg`Go back`) : _(msg`Go home`)}
accessibilityHint={
canGoBack
? _(msg`Returns to previous page`)
: _(msg`Returns to home page`)
}
onPress={onPressHome}
/>
</View>

View File

@ -59,11 +59,7 @@ export function PostThreadScreen({route}: Props) {
uri: thread.post.uri,
cid: thread.post.cid,
text: thread.record.text,
author: {
handle: thread.post.author.handle,
displayName: thread.post.author.displayName,
avatar: thread.post.author.avatar,
},
author: thread.post.author,
embed: thread.post.embed,
},
onPost: () =>

View File

@ -108,8 +108,8 @@ export function ProfileFeedScreen(props: Props) {
<View style={{flexDirection: 'row'}}>
<Button
type="default"
accessibilityLabel={_(msg`Go Back`)}
accessibilityHint="Return to previous page"
accessibilityLabel={_(msg`Go back`)}
accessibilityHint={_(msg`Returns to previous page`)}
onPress={onPressBack}
style={{flexShrink: 1}}>
<Text type="button" style={pal.text}>

View File

@ -21,7 +21,7 @@ export const ProfileFollowersScreen = ({route}: Props) => {
)
return (
<View>
<View style={{flex: 1}}>
<ViewHeader title={_(msg`Followers`)} />
<ProfileFollowersComponent name={name} />
</View>

View File

@ -21,7 +21,7 @@ export const ProfileFollowsScreen = ({route}: Props) => {
)
return (
<View>
<View style={{flex: 1}}>
<ViewHeader title={_(msg`Following`)} />
<ProfileFollowsComponent name={name} />
</View>

View File

@ -913,7 +913,7 @@ function ErrorScreen({error}: {error: string}) {
<View style={{flexDirection: 'row'}}>
<Button
type="default"
accessibilityLabel={_(msg`Go Back`)}
accessibilityLabel={_(msg`Go back`)}
accessibilityHint={_(msg`Return to previous page`)}
onPress={onPressBack}
style={{flexShrink: 1}}>

View File

@ -141,6 +141,7 @@ function SearchScreenSuggestedFollows() {
friends.slice(0, 4).map(friend =>
getSuggestedFollowsByActor(friend.did).then(foafsRes => {
for (const user of foafsRes.suggestions) {
if (user.associated?.labeler) continue
friendsOfFriends.set(user.did, user)
}
}),
@ -772,7 +773,7 @@ export function SearchScreen(
{searchHistory.length > 0 && (
<View style={styles.searchHistoryContent}>
<Text style={[pal.text, styles.searchHistoryTitle]}>
Recent Searches
<Trans>Recent Searches</Trans>
</Text>
{searchHistory.map((historyItem, index) => (
<View key={index} style={styles.historyItemContainer}>

View File

@ -78,8 +78,9 @@ export function ExportCarDialog({
<InlineLink
to="https://docs.bsky.app/blog/repo-export"
style={[a.text_sm]}>
this blogpost.
this blogpost
</InlineLink>
.
</Trans>
</P>

View File

@ -40,10 +40,7 @@ import {
} from '#/state/preferences'
import {useSession, useSessionApi, SessionAccount} from '#/state/session'
import {useProfileQuery} from '#/state/queries/profile'
import {
useClearPreferencesMutation,
usePreferencesQuery,
} from '#/state/queries/preferences'
import {useClearPreferencesMutation} from '#/state/queries/preferences'
// TODO import {useInviteCodesQuery} from '#/state/queries/invites'
import {clear as clearStorage} from '#/state/persisted/store'
import {clearLegacyStorage} from '#/state/persisted/legacy'
@ -85,7 +82,11 @@ function SettingsAccountCard({account}: {account: SessionAccount}) {
const contents = (
<View style={[pal.view, styles.linkCard]}>
<View style={styles.avi}>
<UserAvatar size={40} avatar={profile?.avatar} />
<UserAvatar
size={40}
avatar={profile?.avatar}
type={profile?.associated?.labeler ? 'labeler' : 'user'}
/>
</View>
<View style={[s.flex1]}>
<Text type="md-bold" style={pal.text}>
@ -156,7 +157,6 @@ export function SettingsScreen({}: Props) {
const {screen, track} = useAnalytics()
const {openModal} = useModalControls()
const {isSwitchingAccounts, accounts, currentAccount} = useSession()
const {data: preferences} = usePreferencesQuery()
const {mutate: clearPreferences} = useClearPreferencesMutation()
// TODO
// const {data: invites} = useInviteCodesQuery()
@ -295,10 +295,7 @@ export function SettingsScreen({}: Props) {
return (
<View style={s.hContentRegion} testID="settingsScreen">
<ExportCarDialog control={exportCarControl} />
<BirthDateSettingsDialog
control={birthdayControl}
preferences={preferences}
/>
<BirthDateSettingsDialog control={birthdayControl} />
<SimpleViewHeader
showBackButton={isMobile}
@ -490,20 +487,20 @@ export function SettingsScreen({}: Props) {
label={_(msg`System`)}
left
onSelect={() => setColorMode('system')}
accessibilityHint={_(msg`Set color theme to system setting`)}
accessibilityHint={_(msg`Sets color theme to system setting`)}
/>
<SelectableBtn
selected={colorMode === 'light'}
label={_(msg`Light`)}
onSelect={() => setColorMode('light')}
accessibilityHint={_(msg`Set color theme to light`)}
accessibilityHint={_(msg`Sets color theme to light`)}
/>
<SelectableBtn
selected={colorMode === 'dark'}
label={_(msg`Dark`)}
right
onSelect={() => setColorMode('dark')}
accessibilityHint={_(msg`Set color theme to dark`)}
accessibilityHint={_(msg`Sets color theme to dark`)}
/>
</View>
</View>
@ -522,14 +519,14 @@ export function SettingsScreen({}: Props) {
label={_(msg`Dim`)}
left
onSelect={() => setDarkTheme('dim')}
accessibilityHint={_(msg`Set dark theme to the dim theme`)}
accessibilityHint={_(msg`Sets dark theme to the dim theme`)}
/>
<SelectableBtn
selected={darkTheme === 'dark'}
label={_(msg`Dark`)}
right
onSelect={() => setDarkTheme('dark')}
accessibilityHint={_(msg`Set dark theme to the dark theme`)}
accessibilityHint={_(msg`Sets dark theme to the dark theme`)}
/>
</View>
</View>
@ -549,8 +546,8 @@ export function SettingsScreen({}: Props) {
]}
onPress={openFollowingFeedPreferences}
accessibilityRole="button"
accessibilityHint=""
accessibilityLabel={_(msg`Opens the home feed preferences`)}>
accessibilityLabel={_(msg`Following feed preferences`)}
accessibilityHint={_(msg`Opens the Following feed preferences`)}>
<View style={[styles.iconContainer, pal.btn]}>
<FontAwesomeIcon
icon="sliders"
@ -570,8 +567,8 @@ export function SettingsScreen({}: Props) {
]}
onPress={openThreadsPreferences}
accessibilityRole="button"
accessibilityHint=""
accessibilityLabel={_(msg`Opens the threads preferences`)}>
accessibilityLabel={_(msg`Thread preferences`)}
accessibilityHint={_(msg`Opens the threads preferences`)}>
<View style={[styles.iconContainer, pal.btn]}>
<FontAwesomeIcon
icon={['far', 'comments']}
@ -590,9 +587,10 @@ export function SettingsScreen({}: Props) {
pal.view,
isSwitchingAccounts && styles.dimmed,
]}
accessibilityHint="My Saved Feeds"
accessibilityLabel={_(msg`Opens screen with all saved feeds`)}
onPress={onPressSavedFeeds}>
onPress={onPressSavedFeeds}
accessibilityRole="button"
accessibilityLabel={_(msg`My saved feeds`)}
accessibilityHint={_(msg`Opens screen with all saved feeds`)}>
<View style={[styles.iconContainer, pal.btn]}>
<HashtagIcon style={pal.text} size={18} strokeWidth={3} />
</View>
@ -691,7 +689,7 @@ export function SettingsScreen({}: Props) {
onPress={onPressAppPasswords}
accessibilityRole="button"
accessibilityLabel={_(msg`App password settings`)}
accessibilityHint={_(msg`Opens the app password settings page`)}>
accessibilityHint={_(msg`Opens the app password settings`)}>
<View style={[styles.iconContainer, pal.btn]}>
<FontAwesomeIcon
icon="lock"
@ -712,7 +710,9 @@ export function SettingsScreen({}: Props) {
onPress={isSwitchingAccounts ? undefined : onPressChangeHandle}
accessibilityRole="button"
accessibilityLabel={_(msg`Change handle`)}
accessibilityHint={_(msg`Choose a new Bluesky username or create`)}>
accessibilityHint={_(
msg`Opens modal for choosing or creating a new Bluesky username`,
)}>
<View style={[styles.iconContainer, pal.btn]}>
<FontAwesomeIcon
icon="at"
@ -748,7 +748,9 @@ export function SettingsScreen({}: Props) {
onPress={() => openModal({name: 'change-password'})}
accessibilityRole="button"
accessibilityLabel={_(msg`Change password`)}
accessibilityHint={_(msg`Change your Bluesky password`)}>
accessibilityHint={_(
msg`Opens modal for changing your Bluesky password`,
)}>
<View style={[styles.iconContainer, pal.btn]}>
<FontAwesomeIcon
icon="lock"
@ -770,7 +772,7 @@ export function SettingsScreen({}: Props) {
accessibilityRole="button"
accessibilityLabel={_(msg`Export my data`)}
accessibilityHint={_(
msg`Download Bluesky account data (repository)`,
msg`Opens modal for downloading Bluesky account data (repository)`,
)}>
<View style={[styles.iconContainer, pal.btn]}>
<FontAwesomeIcon
@ -789,7 +791,7 @@ export function SettingsScreen({}: Props) {
accessibilityRole="button"
accessibilityLabel={_(msg`Delete account`)}
accessibilityHint={_(
msg`Opens modal for account deletion confirmation. Requires email code.`,
msg`Opens modal for account deletion confirmation. Requires email code`,
)}>
<View style={[styles.iconContainer, dangerBg]}>
<FontAwesomeIcon
@ -807,8 +809,8 @@ export function SettingsScreen({}: Props) {
style={[pal.view, styles.linkCardNoIcon]}
onPress={onPressSystemLog}
accessibilityRole="button"
accessibilityHint="Open system log"
accessibilityLabel={_(msg`Opens the system log page`)}>
accessibilityLabel={_(msg`Open system log`)}
accessibilityHint={_(msg`Opens the system log page`)}>
<Text type="lg" style={pal.text}>
<Trans>System log</Trans>
</Text>
@ -839,7 +841,7 @@ export function SettingsScreen({}: Props) {
style={[pal.view, styles.linkCardNoIcon]}
onPress={onPressResetPreferences}
accessibilityRole="button"
accessibilityLabel={_(msg`Reset preferences`)}
accessibilityLabel={_(msg`Reset preferences state`)}
accessibilityHint={_(msg`Resets the preferences state`)}>
<Text type="lg" style={pal.text}>
<Trans>Reset preferences state</Trans>
@ -849,7 +851,7 @@ export function SettingsScreen({}: Props) {
style={[pal.view, styles.linkCardNoIcon]}
onPress={onPressResetOnboarding}
accessibilityRole="button"
accessibilityLabel={_(msg`Reset onboarding`)}
accessibilityLabel={_(msg`Reset onboarding state`)}
accessibilityHint={_(msg`Resets the onboarding state`)}>
<Text type="lg" style={pal.text}>
<Trans>Reset onboarding state</Trans>
@ -860,7 +862,7 @@ export function SettingsScreen({}: Props) {
onPress={clearAllLegacyStorage}
accessibilityRole="button"
accessibilityLabel={_(msg`Clear all legacy storage data`)}
accessibilityHint={_(msg`Clear all legacy storage data`)}>
accessibilityHint={_(msg`Clears all legacy storage data`)}>
<Text type="lg" style={pal.text}>
<Trans>
Clear all legacy storage data (restart after this)
@ -872,7 +874,7 @@ export function SettingsScreen({}: Props) {
onPress={clearAllStorage}
accessibilityRole="button"
accessibilityLabel={_(msg`Clear all storage data`)}
accessibilityHint={_(msg`Clear all storage data`)}>
accessibilityHint={_(msg`Clears all storage data`)}>
<Text type="lg" style={pal.text}>
<Trans>Clear all storage data (restart after this)</Trans>
</Text>
@ -961,7 +963,7 @@ function EmailConfirmationNotice() {
]}
accessibilityRole="button"
accessibilityLabel={_(msg`Verify my email`)}
accessibilityHint=""
accessibilityHint={_(msg`Opens modal for email verification`)}
onPress={() => openModal({name: 'verify-email'})}>
<FontAwesomeIcon
icon="envelope"

View File

@ -75,6 +75,7 @@ let DrawerProfileCard = ({
avatar={profile?.avatar}
// See https://github.com/bluesky-social/social-app/pull/1801:
usePlainRNImage={true}
type={profile?.associated?.labeler ? 'labeler' : 'user'}
/>
<Text
type="title-lg"
@ -93,10 +94,12 @@ let DrawerProfileCard = ({
{formatCountShortOnly(profile?.followersCount ?? 0)}
</Text>{' '}
{pluralize(profile?.followersCount || 0, 'follower')} &middot;{' '}
<Trans>
<Text type="xl-medium" style={pal.text}>
{formatCountShortOnly(profile?.followsCount ?? 0)}
</Text>{' '}
following
</Trans>
</Text>
</TouchableOpacity>
)

View File

@ -58,7 +58,7 @@ let NavSignupCard = ({}: {}): React.ReactNode => {
accessibilityHint={_(msg`Sign in`)}
accessibilityLabel={_(msg`Sign in`)}>
<Text type="md" style={[pal.text, s.bold]}>
Sign in
<Trans>Sign in</Trans>
</Text>
</Button>
</View>

View File

@ -229,6 +229,7 @@ export function BottomBar({navigation}: BottomTabBarProps) {
size={27}
// See https://github.com/bluesky-social/social-app/pull/1801:
usePlainRNImage={true}
type={profile?.associated?.labeler ? 'labeler' : 'user'}
/>
</View>
) : (
@ -238,6 +239,7 @@ export function BottomBar({navigation}: BottomTabBarProps) {
size={28}
// See https://github.com/bluesky-social/social-app/pull/1801:
usePlainRNImage={true}
type={profile?.associated?.labeler ? 'labeler' : 'user'}
/>
</View>
)}

View File

@ -64,7 +64,11 @@ function ProfileCard() {
style={[styles.profileCard, !isDesktop && styles.profileCardTablet]}
title={_(msg`My Profile`)}
asAnchor>
<UserAvatar avatar={profile.avatar} size={size} />
<UserAvatar
avatar={profile.avatar}
size={size}
type={profile?.associated?.labeler ? 'labeler' : 'user'}
/>
</Link>
) : (
<View style={[styles.profileCard, !isDesktop && styles.profileCardTablet]}>

View File

@ -112,6 +112,7 @@ export function SearchProfileCard({
size={40}
avatar={profile.avatar}
moderation={moderation.ui('avatar')}
type={profile.associated?.labeler ? 'labeler' : 'user'}
/>
<View style={{flex: 1}}>
<Text

View File

@ -34,10 +34,10 @@
jsonpointer "^5.0.0"
leven "^3.1.0"
"@atproto/api@^0.12.0":
version "0.12.0"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.0.tgz#69e52f8761dc7d76c675fa7284bd49240bb0df64"
integrity sha512-nSWiad1Z6IC/oVFSVxD5gZLhkD+J4EW2CFqAqIhklJNc0cjFKdmf8D56Pac6Ktm1sJoM6TVZ8GEeuEG6bJS/aQ==
"@atproto/api@^0.12.1":
version "0.12.1"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.1.tgz#3340cbbd6a51a8c2f3248dae55a01415ab71084e"
integrity sha512-Grigs9neuQxytXr2yHq/IfNlgXQVptWDO9KTQr5FDmgMY4Zly2X7Sa99u9c1CW9auwUTbcd+yRFBNEtbA3n3qg==
dependencies:
"@atproto/common-web" "^0.3.0"
"@atproto/lexicon" "^0.4.0"