Merge remote-tracking branch 'origin/main' into 3p-moderators

* origin/main: (33 commits)
  Improved server selector during account creation and signin (#2840)
  remove line height from text input for composer on ios (#2844)
  Add follow button to highlighted post (#2828)
  Cleaning up old codes pt_BR (#2809)
  Fix wrong translations in zh-CN localization  (#2812)
  Adjust Japanese translation (#2814)
  Update ja messages.po (#2821)
  change follow to follow all when !== 20 (#2831)
  ios adult content link fix 🤦 (#2845)
  improves build.md completeness (#2835)
  increase stroke width for active hashtag icon (#2829)
  Update `blueskyweb.xyz` links to `bsky.social` (#2830)
  eas nightlies (#2826)
  Always show post dropdown button at the bottom of the post, add share button to highlighted post (#2646)
  Measure header layout reliably (#2817)
  Round line height (#2824)
  Design system tweaks (#2822)
  Fix layout calculations (#2816)
  Fix flashes and jumps when opening profile (#2815)
  Show text cursor on web bio (#2813)
  ...
This commit is contained in:
Eric Bailey
2024-02-12 15:43:33 -06:00
90 changed files with 1781 additions and 1707 deletions
@@ -0,0 +1,52 @@
name: Deploy Nightly Testflight Release
on:
schedule:
- cron: '0 5 * * *'
jobs:
build:
name: Deploy Nightly Testflight Release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Check for EXPO_TOKEN
run: |
if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then
echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions"
exit 1
fi
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 18.x
cache: yarn
- name: Setup EAS
uses: expo/expo-github-action@v8
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- name: Install dependencies
run: yarn install
- name: Bump build number
run: yarn bump:ios
- name: EAS build and submit
run: eas build -p ios --profile production --auto-submit --non-interactive
- name: Commit
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: Nightly iOS Build Bump
branch: main
commit_user_name: github-actions[bot]
commit_user_email: github-actions[bot]@users.noreply.github.com
+2 -2
View File
@@ -6,7 +6,7 @@ Get the app itself:
- **Web: [bsky.app](https://bsky.app)**
- **iOS: [App Store](https://apps.apple.com/us/app/bluesky-social/id6444370199)**
- **Android: [Play Store](https://play.google.com/store/apps/details?id=xyz.blueskyweb.app&hl=en_US&gl=US)**
- **Android: [Play Store](https://play.google.com/store/apps/details?id=xyz.blueskyweb.app)**
## Development Resources
@@ -21,7 +21,7 @@ The Authenticated Transfer Protocol ("AT Protocol" or "atproto") is a decentrali
- [Overview and Guides](https://atproto.com/guides/overview)
- [Github Discussions](https://github.com/bluesky-social/atproto/discussions) 👈 Great place to ask questions
- [Protocol Specifications](https://atproto.com/specs/atp)
- [Blogpost on self-authenticating data structures](https://blueskyweb.xyz/blog/3-6-2022-a-self-authenticating-social-protocol)
- [Blogpost on self-authenticating data structures](https://bsky.social/about/blog/3-6-2022-a-self-authenticating-social-protocol)
The Bluesky Social application encompasses a set of schemas and APIs built in the overall AT Protocol framework. The namespace for these "Lexicons" is `app.bsky.*`.
+3 -1
View File
@@ -19,11 +19,13 @@ module.exports = function () {
/**
* iOS build number. Must be incremented for each TestFlight version.
* WARNING: Always leave this variable on line 24! If it is moved, you need to update ./scripts/bumpIosBuildNumber.sh
*/
const IOS_BUILD_NUMBER = '2'
const IOS_BUILD_NUMBER = '3'
/**
* Android build number. Must be incremented for each release.
* WARNING: Always leave this variable on line 30! If it is moved, you need to update ./scripts/bumpAndroidBuildNumber.sh
*/
const ANDROID_VERSION_CODE = 61
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12.707 3.293a1 1 0 0 0-1.414 0l-4.5 4.5a1 1 0 0 0 1.414 1.414L11 6.414v8.836a1 1 0 1 0 2 0V6.414l2.793 2.793a1 1 0 1 0 1.414-1.414l-4.5-4.5ZM5 12.75a1 1 0 1 0-2 0V20a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1v-7.25a1 1 0 1 0-2 0V19H5v-6.25Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 372 B

+4 -4
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, viewport-fit=cover">
@@ -217,9 +217,9 @@
{%- block body_all %}
<div id="root"></div>
<noscript>
<h1>JavaScript Required</h1>
<p>This is a heavily interactive web application, and JavaScript is required. Simple HTML interfaces are possible, but that is not what this is.
<p>Learn more about Bluesky at <a href="https://blueskyweb.xyz">blueskyweb.xyz</a> and <a href="https://atproto.com">atproto.com</a>.
<h1 lang="en">JavaScript Required</h1>
<p lang="en">This is a heavily interactive web application, and JavaScript is required. Simple HTML interfaces are possible, but that is not what this is.
<p lang="en">Learn more about Bluesky at <a href="https://bsky.social">bsky.social</a> and <a href="https://atproto.com">atproto.com</a>.
{% block noscript_extra %}{% endblock %}
</noscript>
{% endblock -%}
+11
View File
@@ -26,11 +26,22 @@
- Start the dev servers
- `git clone git@github.com:bluesky-social/atproto.git`
- `cd atproto`
- `brew install pnpm`
- `brew install jq`
- `pnpm i`
- `pnpm build`
- Start the docker daemon (on MacOS this entails starting the Docker Desktop app)
- Launch a Postgres database on port 5432
- `cd packages/dev-env && pnpm start`
- Run the dev app
- iOS: `yarn ios`
- Xcode must be installed for this to run.
- A simulator must be preconfigured in Xcode settings.
- if no iOS versions are available, install the iOS runtime at `Xcode > Settings > Platforms`.
- In addition, ensure Xcode Command Line Tools are installed using `xcode-select --install`.
- Pods must be installed:
- From the project directory root: `cd ios && pod install`.
- Expo will require you to configure Xcode Signing. Follow the linked instructions. Error messages in Xcode related to the signing process can be safely ignored when installing on the iOS Simulator; Expo merely requires the profile to exist in order to install the app on the Simulator.
- Android: `yarn android`
- Web: `yarn web`
- If you are cloning or forking this repo as an open-source developer, please check the tips below as well
+5 -1
View File
@@ -34,6 +34,10 @@
}
},
"submit": {
"production": {}
"production": {
"ios": {
"ascAppId": "6444370199"
}
}
}
}
+4 -1
View File
@@ -36,7 +36,10 @@
"intl:check": "yarn intl:extract && git diff-index -G'(^[^\\*# /])|(^#\\w)|(^\\s+[^\\*#/])' HEAD || (echo '\n⚠️ i18n detected un-extracted translations\n' && exit 1)",
"intl:extract": "lingui extract",
"intl:compile": "lingui compile",
"nuke": "rm -rf ./node_modules && rm -rf ./ios && rm -rf ./android"
"nuke": "rm -rf ./node_modules && rm -rf ./ios && rm -rf ./android",
"bump": "./scripts/bumpIosBuildNumber.sh && ./scripts/bumpAndroidBuildNumber.sh",
"bump:ios": "./scripts/bumpIosBuildNumber.sh",
"bump:android": "./scripts/bumpAndroidBuildNumber.sh"
},
"dependencies": {
"@atproto/api": "^0.9.5",
+10
View File
@@ -0,0 +1,10 @@
#!/bin/sh
# The number here should always be the line number the iOS build variable is on
line=$(sed "30q;d" ./app.config.js)
currentBuildNumber=$(echo "$line" | grep -oE '[0-9]+([.][0-9]+)?')
newBuildNumber=$((currentBuildNumber+1))
newBuildVariable="const ANDROID_VERSION_CODE = '$newBuildNumber'"
sed -i.bak "30s/.*/ $newBuildVariable/" ./app.config.js
rm -rf ./app.config.js.bak
echo "Android build number bumped to $newBuildNumber"
+10
View File
@@ -0,0 +1,10 @@
#!/bin/sh
# The number here should always be the line number the iOS build variable is on
line=$(sed "24q;d" ./app.config.js)
currentBuildNumber=$(echo "$line" | grep -oE '[0-9]+([.][0-9]+)?')
newBuildNumber=$((currentBuildNumber+1))
newBuildVariable="const IOS_BUILD_NUMBER = '$newBuildNumber'"
sed -i.bak "24s/.*/ $newBuildVariable/" ./app.config.js
rm -rf ./app.config.js.bak
echo "iOS build number bumped to $newBuildNumber"
+14 -11
View File
@@ -32,6 +32,7 @@ import {Provider as MutedThreadsProvider} from 'state/muted-threads'
import {Provider as InvitesStateProvider} from 'state/invites'
import {Provider as PrefsStateProvider} from 'state/preferences'
import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out'
import {Provider as SelectedFeedProvider} from 'state/shell/selected-feed'
import I18nProvider from './locale/i18nProvider'
import {
Provider as SessionProvider,
@@ -72,17 +73,19 @@ function InnerApp() {
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<LoggedOutViewProvider>
<UnreadNotifsProvider>
<ThemeProvider theme={theme}>
{/* All components should be within this provider */}
<RootSiblingParent>
<GestureHandlerRootView style={s.h100pct}>
<TestCtrls />
<Shell />
</GestureHandlerRootView>
</RootSiblingParent>
</ThemeProvider>
</UnreadNotifsProvider>
<SelectedFeedProvider>
<UnreadNotifsProvider>
<ThemeProvider theme={theme}>
{/* All components should be within this provider */}
<RootSiblingParent>
<GestureHandlerRootView style={s.h100pct}>
<TestCtrls />
<Shell />
</GestureHandlerRootView>
</RootSiblingParent>
</ThemeProvider>
</UnreadNotifsProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</React.Fragment>
</Splash>
+14 -11
View File
@@ -22,6 +22,7 @@ import {Provider as MutedThreadsProvider} from 'state/muted-threads'
import {Provider as InvitesStateProvider} from 'state/invites'
import {Provider as PrefsStateProvider} from 'state/preferences'
import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out'
import {Provider as SelectedFeedProvider} from 'state/shell/selected-feed'
import I18nProvider from './locale/i18nProvider'
import {
Provider as SessionProvider,
@@ -52,17 +53,19 @@ function InnerApp() {
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<LoggedOutViewProvider>
<UnreadNotifsProvider>
<ThemeProvider theme={theme}>
{/* All components should be within this provider */}
<RootSiblingParent>
<SafeAreaProvider>
<Shell />
</SafeAreaProvider>
</RootSiblingParent>
<ToastContainer />
</ThemeProvider>
</UnreadNotifsProvider>
<SelectedFeedProvider>
<UnreadNotifsProvider>
<ThemeProvider theme={theme}>
{/* All components should be within this provider */}
<RootSiblingParent>
<SafeAreaProvider>
<Shell />
</SafeAreaProvider>
</RootSiblingParent>
<ToastContainer />
</ThemeProvider>
</UnreadNotifsProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</React.Fragment>
</Alf>
-1
View File
@@ -146,7 +146,6 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
getComponent={() => ProfileScreen}
options={({route}) => ({
title: bskyTitle(`@${route.params.name}`, unreadCountLabel),
animation: 'none',
})}
/>
<Stack.Screen
+162 -40
View File
@@ -73,19 +73,19 @@ export const darkPalette: Palette = {
white: tokens.color.gray_0,
black: tokens.color.trueBlack,
contrast_25: tokens.color.gray_975,
contrast_50: tokens.color.gray_950,
contrast_100: tokens.color.gray_900,
contrast_200: tokens.color.gray_800,
contrast_300: tokens.color.gray_700,
contrast_400: tokens.color.gray_600,
contrast_500: tokens.color.gray_500,
contrast_600: tokens.color.gray_400,
contrast_700: tokens.color.gray_300,
contrast_800: tokens.color.gray_200,
contrast_900: tokens.color.gray_100,
contrast_950: tokens.color.gray_50,
contrast_975: tokens.color.gray_25,
contrast_25: tokens.color.gray_1000,
contrast_50: tokens.color.gray_975,
contrast_100: tokens.color.gray_950,
contrast_200: tokens.color.gray_900,
contrast_300: tokens.color.gray_800,
contrast_400: tokens.color.gray_700,
contrast_500: tokens.color.gray_600,
contrast_600: tokens.color.gray_500,
contrast_700: tokens.color.gray_400,
contrast_800: tokens.color.gray_300,
contrast_900: tokens.color.gray_200,
contrast_950: tokens.color.gray_100,
contrast_975: tokens.color.gray_50,
primary_25: tokens.color.blue_25,
primary_50: tokens.color.blue_50,
@@ -133,6 +133,20 @@ export const darkPalette: Palette = {
export const dimPalette: Palette = {
...darkPalette,
black: tokens.color.gray_1000,
contrast_25: tokens.color.gray_975,
contrast_50: tokens.color.gray_950,
contrast_100: tokens.color.gray_900,
contrast_200: tokens.color.gray_800,
contrast_300: tokens.color.gray_700,
contrast_400: tokens.color.gray_600,
contrast_500: tokens.color.gray_500,
contrast_600: tokens.color.gray_400,
contrast_700: tokens.color.gray_300,
contrast_800: tokens.color.gray_200,
contrast_900: tokens.color.gray_100,
contrast_950: tokens.color.gray_50,
contrast_975: tokens.color.gray_25,
} as const
export const light = {
@@ -142,17 +156,14 @@ export const light = {
text: {
color: lightPalette.black,
},
text_contrast_700: {
text_contrast_low: {
color: lightPalette.contrast_400,
},
text_contrast_medium: {
color: lightPalette.contrast_700,
},
text_contrast_600: {
color: lightPalette.contrast_600,
},
text_contrast_500: {
color: lightPalette.contrast_500,
},
text_contrast_400: {
color: lightPalette.contrast_400,
text_contrast_high: {
color: lightPalette.contrast_900,
},
text_inverted: {
color: lightPalette.white,
@@ -175,11 +186,38 @@ export const light = {
bg_contrast_300: {
backgroundColor: lightPalette.contrast_300,
},
border: {
bg_contrast_400: {
backgroundColor: lightPalette.contrast_400,
},
bg_contrast_500: {
backgroundColor: lightPalette.contrast_500,
},
bg_contrast_600: {
backgroundColor: lightPalette.contrast_600,
},
bg_contrast_700: {
backgroundColor: lightPalette.contrast_700,
},
bg_contrast_800: {
backgroundColor: lightPalette.contrast_800,
},
bg_contrast_900: {
backgroundColor: lightPalette.contrast_900,
},
bg_contrast_950: {
backgroundColor: lightPalette.contrast_950,
},
bg_contrast_975: {
backgroundColor: lightPalette.contrast_975,
},
border_contrast_low: {
borderColor: lightPalette.contrast_100,
},
border_contrast: {
borderColor: lightPalette.contrast_400,
border_contrast_medium: {
borderColor: lightPalette.contrast_200,
},
border_contrast_high: {
borderColor: lightPalette.contrast_300,
},
shadow_sm: {
...atoms.shadow_sm,
@@ -203,17 +241,14 @@ export const dark: Theme = {
text: {
color: darkPalette.white,
},
text_contrast_700: {
text_contrast_low: {
color: darkPalette.contrast_400,
},
text_contrast_medium: {
color: darkPalette.contrast_700,
},
text_contrast_600: {
color: darkPalette.contrast_600,
},
text_contrast_500: {
color: darkPalette.contrast_500,
},
text_contrast_400: {
color: darkPalette.contrast_400,
text_contrast_high: {
color: darkPalette.contrast_900,
},
text_inverted: {
color: darkPalette.black,
@@ -222,24 +257,51 @@ export const dark: Theme = {
backgroundColor: darkPalette.black,
},
bg_contrast_25: {
backgroundColor: darkPalette.contrast_50,
backgroundColor: darkPalette.contrast_25,
},
bg_contrast_50: {
backgroundColor: darkPalette.contrast_100,
backgroundColor: darkPalette.contrast_50,
},
bg_contrast_100: {
backgroundColor: darkPalette.contrast_200,
backgroundColor: darkPalette.contrast_100,
},
bg_contrast_200: {
backgroundColor: darkPalette.contrast_300,
backgroundColor: darkPalette.contrast_200,
},
bg_contrast_300: {
backgroundColor: darkPalette.contrast_300,
},
bg_contrast_400: {
backgroundColor: darkPalette.contrast_400,
},
border: {
bg_contrast_500: {
backgroundColor: darkPalette.contrast_500,
},
bg_contrast_600: {
backgroundColor: darkPalette.contrast_600,
},
bg_contrast_700: {
backgroundColor: darkPalette.contrast_700,
},
bg_contrast_800: {
backgroundColor: darkPalette.contrast_800,
},
bg_contrast_900: {
backgroundColor: darkPalette.contrast_900,
},
bg_contrast_950: {
backgroundColor: darkPalette.contrast_950,
},
bg_contrast_975: {
backgroundColor: darkPalette.contrast_975,
},
border_contrast_low: {
borderColor: darkPalette.contrast_100,
},
border_contrast: {
border_contrast_medium: {
borderColor: darkPalette.contrast_200,
},
border_contrast_high: {
borderColor: darkPalette.contrast_300,
},
shadow_sm: {
@@ -265,11 +327,71 @@ export const dim: Theme = {
name: 'dim',
atoms: {
...dark.atoms,
text: {
color: dimPalette.white,
},
text_contrast_low: {
color: dimPalette.contrast_400,
},
text_contrast_medium: {
color: dimPalette.contrast_700,
},
text_contrast_high: {
color: dimPalette.contrast_900,
},
text_inverted: {
color: dimPalette.black,
},
bg: {
backgroundColor: dimPalette.black,
},
bg_contrast_25: {
backgroundColor: dimPalette.contrast_25,
},
bg_contrast_50: {
backgroundColor: dimPalette.contrast_50,
},
bg_contrast_100: {
backgroundColor: dimPalette.contrast_100,
},
bg_contrast_200: {
backgroundColor: dimPalette.contrast_200,
},
bg_contrast_300: {
backgroundColor: dimPalette.contrast_300,
},
bg_contrast_400: {
backgroundColor: dimPalette.contrast_400,
},
bg_contrast_500: {
backgroundColor: dimPalette.contrast_500,
},
bg_contrast_600: {
backgroundColor: dimPalette.contrast_600,
},
bg_contrast_700: {
backgroundColor: dimPalette.contrast_700,
},
bg_contrast_800: {
backgroundColor: dimPalette.contrast_800,
},
bg_contrast_900: {
backgroundColor: dimPalette.contrast_900,
},
bg_contrast_950: {
backgroundColor: dimPalette.contrast_950,
},
bg_contrast_975: {
backgroundColor: dimPalette.contrast_975,
},
border_contrast_low: {
borderColor: dimPalette.contrast_100,
},
border_contrast_medium: {
borderColor: dimPalette.contrast_200,
},
border_contrast_high: {
borderColor: dimPalette.contrast_300,
},
},
}
+7 -7
View File
@@ -13,13 +13,13 @@ export const color = {
gray_300: `hsl(${BLUE_HUE}, 20%, 70%)`,
gray_400: `hsl(${BLUE_HUE}, 20%, 60%)`,
gray_500: `hsl(${BLUE_HUE}, 20%, 50%)`,
gray_600: `hsl(${BLUE_HUE}, 20%, 42%)`,
gray_700: `hsl(${BLUE_HUE}, 20%, 34%)`,
gray_800: `hsl(${BLUE_HUE}, 20%, 26%)`,
gray_900: `hsl(${BLUE_HUE}, 20%, 18%)`,
gray_950: `hsl(${BLUE_HUE}, 20%, 10%)`,
gray_975: `hsl(${BLUE_HUE}, 20%, 7%)`,
gray_1000: `hsl(${BLUE_HUE}, 20%, 4%)`,
gray_600: `hsl(${BLUE_HUE}, 24%, 42%)`,
gray_700: `hsl(${BLUE_HUE}, 24%, 34%)`,
gray_800: `hsl(${BLUE_HUE}, 28%, 26%)`,
gray_900: `hsl(${BLUE_HUE}, 28%, 18%)`,
gray_950: `hsl(${BLUE_HUE}, 28%, 10%)`,
gray_975: `hsl(${BLUE_HUE}, 28%, 7%)`,
gray_1000: `hsl(${BLUE_HUE}, 28%, 4%)`,
blue_25: `hsl(${BLUE_HUE}, 99%, 97%)`,
blue_50: `hsl(${BLUE_HUE}, 99%, 95%)`,
+1
View File
@@ -52,6 +52,7 @@ export type ButtonProps = React.PropsWithChildren<
Pick<PressableProps, 'disabled' | 'onPress' | 'testID'> &
AccessibilityProps &
VariantProps & {
testID?: string
label: string
style?: StyleProp<ViewStyle>
}
+15 -3
View File
@@ -35,6 +35,7 @@ export function Outer({
const sheet = React.useRef<BottomSheet>(null)
const sheetOptions = nativeOptions?.sheet || {}
const hasSnapPoints = !!sheetOptions.snapPoints
const insets = useSafeAreaInsets()
const open = React.useCallback<DialogControlProps['open']>(({index} = {}) => {
sheet.current?.snapToIndex(index || 0)
@@ -42,8 +43,7 @@ export function Outer({
const close = React.useCallback(() => {
sheet.current?.close()
onClose?.()
}, [onClose])
}, [])
useImperativeHandle(
control.ref,
@@ -54,6 +54,15 @@ export function Outer({
[open, close],
)
const onChange = React.useCallback(
(index: number) => {
if (index === -1) {
onClose?.()
}
},
[onClose],
)
const context = React.useMemo(() => ({close}), [close])
return (
@@ -64,6 +73,7 @@ export function Outer({
keyboardBehavior="interactive"
android_keyboardInputMode="adjustResize"
keyboardBlurBehavior="restore"
topInset={insets.top}
{...sheetOptions}
ref={sheet}
index={defaultOpen ? 0 : -1}
@@ -78,7 +88,7 @@ export function Outer({
)}
handleIndicatorStyle={{backgroundColor: t.palette.primary_500}}
handleStyle={{display: 'none'}}
onClose={onClose}>
onChange={onChange}>
<Context.Provider value={context}>
<View
style={[
@@ -122,6 +132,8 @@ export function ScrollableInner(props: DialogInnerProps) {
const insets = useSafeAreaInsets()
return (
<BottomSheetScrollView
keyboardShouldPersistTaps="handled"
keyboardDismissMode="on-drag"
style={[
a.flex_1, // main diff is this
a.p_xl,
+11 -3
View File
@@ -1,10 +1,18 @@
import React from 'react'
import {View} from 'react-native'
import {atoms as a, useTheme} from '#/alf'
import {ViewStyleProp} from '#/alf'
import {atoms as a, useTheme, ViewStyleProp, flatten} from '#/alf'
export function Divider({style}: ViewStyleProp) {
const t = useTheme()
return <View style={[a.w_full, a.border_t, t.atoms.border, style]} />
return (
<View
style={[
a.w_full,
a.border_t,
t.atoms.border_contrast_low,
flatten(style),
]}
/>
)
}
+7 -8
View File
@@ -4,7 +4,7 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useTheme, atoms as a} from '#/alf'
import {H4, P} from '#/components/Typography'
import {Text} from '#/components/Typography'
import {Button} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
@@ -50,14 +50,11 @@ export function Outer({
}
export function Title({children}: React.PropsWithChildren<{}>) {
const t = useTheme()
const {titleId} = React.useContext(Context)
return (
<H4
nativeID={titleId}
style={[a.font_bold, t.atoms.text_contrast_700, a.pb_sm]}>
<Text nativeID={titleId} style={[a.text_2xl, a.font_bold, a.pb_sm]}>
{children}
</H4>
</Text>
)
}
@@ -65,9 +62,11 @@ export function Description({children}: React.PropsWithChildren<{}>) {
const t = useTheme()
const {descriptionId} = React.useContext(Context)
return (
<P nativeID={descriptionId} style={[t.atoms.text, a.pb_lg]}>
<Text
nativeID={descriptionId}
style={[a.text_md, a.leading_snug, t.atoms.text_contrast_high, a.pb_lg]}>
{children}
</P>
</Text>
)
}
+30 -126
View File
@@ -15,7 +15,7 @@ export function leading<
>(textSize: Size, leading: Leading) {
const size = textSize?.fontSize || atoms.text_md.fontSize
const lineHeight = leading?.lineHeight || atoms.leading_normal.lineHeight
return size * lineHeight
return Math.round(size * lineHeight)
}
/**
@@ -32,7 +32,7 @@ function normalizeTextStyles(styles: TextStyle[]) {
if (s?.lineHeight) {
if (s.lineHeight <= 2) {
s.lineHeight = fontSize * s.lineHeight
s.lineHeight = Math.round(fontSize * s.lineHeight)
}
} else {
s.lineHeight = fontSize
@@ -41,138 +41,42 @@ function normalizeTextStyles(styles: TextStyle[]) {
return s
}
/**
* Our main text component. Use this most of the time.
*/
export function Text({style, ...rest}: TextProps) {
const t = useTheme()
const s = normalizeTextStyles([atoms.text_sm, t.atoms.text, flatten(style)])
return <RNText style={s} {...rest} />
}
export function H1({style, ...rest}: TextProps) {
const t = useTheme()
const attr =
web({
role: 'heading',
'aria-level': 1,
}) || {}
return (
<RNText
{...attr}
{...rest}
style={normalizeTextStyles([
atoms.text_5xl,
atoms.font_bold,
t.atoms.text,
flatten(style),
])}
/>
)
}
export function H2({style, ...rest}: TextProps) {
const t = useTheme()
const attr =
web({
role: 'heading',
'aria-level': 2,
}) || {}
return (
<RNText
{...attr}
{...rest}
style={normalizeTextStyles([
atoms.text_4xl,
atoms.font_bold,
t.atoms.text,
flatten(style),
])}
/>
)
}
export function H3({style, ...rest}: TextProps) {
const t = useTheme()
const attr =
web({
role: 'heading',
'aria-level': 3,
}) || {}
return (
<RNText
{...attr}
{...rest}
style={normalizeTextStyles([
atoms.text_3xl,
atoms.font_bold,
t.atoms.text,
flatten(style),
])}
/>
)
}
export function H4({style, ...rest}: TextProps) {
const t = useTheme()
const attr =
web({
role: 'heading',
'aria-level': 4,
}) || {}
return (
<RNText
{...attr}
{...rest}
style={normalizeTextStyles([
atoms.text_2xl,
atoms.font_bold,
t.atoms.text,
flatten(style),
])}
/>
)
}
export function H5({style, ...rest}: TextProps) {
const t = useTheme()
const attr =
web({
role: 'heading',
'aria-level': 5,
}) || {}
return (
<RNText
{...attr}
{...rest}
style={normalizeTextStyles([
atoms.text_xl,
atoms.font_bold,
t.atoms.text,
flatten(style),
])}
/>
)
}
export function H6({style, ...rest}: TextProps) {
const t = useTheme()
const attr =
web({
role: 'heading',
'aria-level': 6,
}) || {}
return (
<RNText
{...attr}
{...rest}
style={normalizeTextStyles([
atoms.text_lg,
atoms.font_bold,
t.atoms.text,
flatten(style),
])}
/>
)
export function createHeadingElement({level}: {level: number}) {
return function HeadingElement({style, ...rest}: TextProps) {
const t = useTheme()
const attr =
web({
role: 'heading',
'aria-level': level,
}) || {}
return (
<RNText
{...attr}
{...rest}
style={normalizeTextStyles([t.atoms.text, flatten(style)])}
/>
)
}
}
/*
* Use semantic components when it's beneficial to the user or to a web scraper
*/
export const H1 = createHeadingElement({level: 1})
export const H2 = createHeadingElement({level: 2})
export const H3 = createHeadingElement({level: 3})
export const H4 = createHeadingElement({level: 4})
export const H5 = createHeadingElement({level: 5})
export const H6 = createHeadingElement({level: 6})
export function P({style, ...rest}: TextProps) {
const t = useTheme()
const attr =
+8 -3
View File
@@ -241,10 +241,15 @@ export function createInput(Component: typeof TextInput) {
export const Input = createInput(TextInput)
export function Label({children}: React.PropsWithChildren<{}>) {
export function Label({
nativeID,
children,
}: React.PropsWithChildren<{nativeID?: string}>) {
const t = useTheme()
return (
<Text style={[a.text_sm, a.font_bold, t.atoms.text_contrast_600, a.mb_sm]}>
<Text
nativeID={nativeID}
style={[a.text_sm, a.font_bold, t.atoms.text_contrast_medium, a.mb_sm]}>
{children}
</Text>
)
@@ -318,7 +323,7 @@ export function Suffix({
a.z_20,
a.pr_sm,
a.text_md,
t.atoms.text_contrast_400,
t.atoms.text_contrast_medium,
{
pointerEvents: 'none',
},
+3 -3
View File
@@ -347,7 +347,7 @@ export function Checkbox() {
a.align_center,
a.border,
a.rounded_xs,
t.atoms.border_contrast,
t.atoms.border_contrast_high,
{
height: 20,
width: 20,
@@ -393,7 +393,7 @@ export function Switch() {
a.border,
a.rounded_full,
t.atoms.bg,
t.atoms.border_contrast,
t.atoms.border_contrast_high,
{
height: 20,
width: 30,
@@ -445,7 +445,7 @@ export function Radio() {
a.align_center,
a.border,
a.rounded_full,
t.atoms.border_contrast,
t.atoms.border_contrast_high,
{
height: 20,
width: 20,
+4 -4
View File
@@ -8,7 +8,7 @@ import * as Toggle from '#/components/forms/Toggle'
export type ItemProps = Omit<Toggle.ItemProps, 'style' | 'role' | 'children'> &
AccessibilityProps &
React.PropsWithChildren<{}>
React.PropsWithChildren<{testID?: string}>
export type GroupProps = Omit<Toggle.GroupProps, 'style' | 'type'> & {
multiple?: boolean
@@ -25,7 +25,7 @@ export function Group({children, multiple, ...props}: GroupProps) {
a.border,
a.rounded_sm,
a.overflow_hidden,
t.atoms.border,
t.atoms.border_contrast_low,
]}>
{children}
</View>
@@ -103,7 +103,7 @@ function ButtonInner({children}: React.PropsWithChildren<{}>) {
}),
a.px_sm,
t.atoms.bg,
t.atoms.border,
t.atoms.border_contrast_low,
baseStyles,
activeStyles,
(state.hovered || state.focused || state.pressed) && hoverStyles,
@@ -113,7 +113,7 @@ function ButtonInner({children}: React.PropsWithChildren<{}>) {
style={[
a.text_center,
a.font_bold,
t.atoms.text_contrast_500,
t.atoms.text_contrast_medium,
textStyles,
]}>
{children}
+5
View File
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const ArrowOutOfBox_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M12.707 3.293a1 1 0 0 0-1.414 0l-4.5 4.5a1 1 0 0 0 1.414 1.414L11 6.414v8.836a1 1 0 1 0 2 0V6.414l2.793 2.793a1 1 0 1 0 1.414-1.414l-4.5-4.5ZM5 12.75a1 1 0 1 0-2 0V20a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1v-7.25a1 1 0 1 0-2 0V19H5v-6.25Z',
})
+1 -1
View File
@@ -219,7 +219,7 @@ export const COUNTRY_CODES: CountryCodeMap[] = [
{code2: 'SE', name: 'Sweden (+46)'},
{code2: 'CH', name: 'Switzerland (+41)'},
{code2: 'SY', name: 'Syrian Arab Republic (+963)'},
{code2: 'TW', name: 'Taiwan, Province of China (+886)'},
{code2: 'TW', name: 'Taiwan (+886)'},
{code2: 'TJ', name: 'Tajikistan (+992)'},
{code2: 'TZ', name: 'Tanzania, United Republic of (+255)'},
{code2: 'TH', name: 'Thailand (+66)'},
+1 -1
View File
@@ -236,7 +236,7 @@ export function lh(
height: number,
): TextStyle {
return {
lineHeight: (theme.typography[type].fontSize || 16) * height,
lineHeight: Math.round((theme.typography[type].fontSize || 16) * height),
}
}
+1 -1
View File
@@ -2445,7 +2445,7 @@ msgstr "Hinweis: Bluesky ist ein offenes und öffentliches Netzwerk. Diese Einst
#: src/view/shell/Drawer.tsx:438
#: src/view/shell/Drawer.tsx:439
msgid "Notifications"
msgstr "Benachrichtigungen"
msgstr "Mitteilungen"
#: src/view/com/modals/SelfLabel.tsx:103
msgid "Nudity"
+61 -61
View File
@@ -9,8 +9,8 @@ msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2024-01-30 19:00+0900\n"
"Last-Translator: dolciss\n"
"Language-Team: Hima-Zinn, tkusano, dolciss, oboenikui, noritada\n"
"Last-Translator: Hima-Zinn\n"
"Language-Team: Hima-Zinn, tkusano, dolciss, oboenikui, noritada, middlingphys\n"
"Plural-Forms: \n"
#: src/view/com/modals/VerifyEmail.tsx:142
@@ -242,7 +242,7 @@ msgstr "高度な設定"
#: src/view/com/auth/login/ForgotPasswordForm.tsx:217
#: src/view/com/modals/ChangePassword.tsx:168
msgid "Already have a code?"
msgstr ""
msgstr "コードをすでに持っていますか?"
#: src/view/com/auth/login/ChooseAccountForm.tsx:98
msgid "Already signed in as @{0}"
@@ -280,7 +280,7 @@ msgstr "および"
#: src/screens/Onboarding/index.tsx:32
msgid "Animals"
msgstr ""
msgstr "動物"
#: src/view/screens/LanguageSettings.tsx:95
msgid "App Language"
@@ -334,7 +334,7 @@ msgstr "この判断に異議を申し立てる"
#: src/view/screens/Settings.tsx:460
msgid "Appearance"
msgstr "外観"
msgstr "背景"
#: src/view/screens/AppPasswords.tsx:224
msgid "Are you sure you want to delete the app password \"{name}\"?"
@@ -358,7 +358,7 @@ msgstr "<0>{0}</0>で書かれた投稿ですか?"
#: src/screens/Onboarding/index.tsx:26
msgid "Art"
msgstr ""
msgstr "アート"
#: src/view/com/modals/SelfLabel.tsx:123
msgid "Artistic or non-erotic nudity."
@@ -476,7 +476,7 @@ msgstr "Blueskyはパブリックです。"
#: src/view/com/modals/Waitlist.tsx:70
msgid "Bluesky uses invites to build a healthier community. If you don't know anybody with an invite, you can sign up for the waitlist and we'll send one soon."
msgstr "Blueskyはより健全なコミュニティを構築するために招待状を使用します。招待状をお持ちでない場合、Waitlistにお申し込みいただくと招待状をお送りします。"
msgstr "Blueskyはより健全なコミュニティを構築するために招待状を使用します。招待状をお持ちでない場合、Waitlistにお申し込みいただくと招待状をお送りします。"
#: src/view/screens/Moderation.tsx:226
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."
@@ -488,7 +488,7 @@ msgstr "Bluesky.Social"
#: src/screens/Onboarding/index.tsx:33
msgid "Books"
msgstr ""
msgstr "書籍"
#: src/view/screens/Settings.tsx:841
msgid "Build version {0} {1}"
@@ -616,11 +616,11 @@ msgstr "メールアドレスを変更"
#: src/view/screens/Settings.tsx:726
msgid "Change password"
msgstr ""
msgstr "パスワードを変更"
#: src/view/screens/Settings.tsx:735
msgid "Change Password"
msgstr ""
msgstr "パスワードを変更"
#: src/view/com/composer/select-language/SuggestedLanguage.tsx:73
msgid "Change post language to {0}"
@@ -628,7 +628,7 @@ msgstr "投稿の言語を{0}に変更します"
#: src/view/screens/Settings.tsx:727
msgid "Change your Bluesky password"
msgstr ""
msgstr "Blueskyのパスワードを変更"
#: src/view/com/modals/ChangeEmail.tsx:109
msgid "Change Your Email"
@@ -682,7 +682,7 @@ msgstr "メインのフィードを選択"
#: src/view/com/auth/create/Step1.tsx:163
msgid "Choose your password"
msgstr "パスワードを選択"
msgstr "パスワードを入力"
#: src/view/screens/Settings.tsx:816
#: src/view/screens/Settings.tsx:817
@@ -713,12 +713,12 @@ msgstr "こちらをクリック"
#: src/screens/Onboarding/index.tsx:35
msgid "Climate"
msgstr ""
msgstr "気象"
#: src/view/com/modals/ChangePassword.tsx:265
#: src/view/com/modals/ChangePassword.tsx:268
msgid "Close"
msgstr ""
msgstr "閉じる"
#: src/components/Dialog/index.web.tsx:78
msgid "Close active dialog"
@@ -766,16 +766,16 @@ msgstr "指定した通知のユーザーリストを折りたたむ"
#: src/screens/Onboarding/index.tsx:41
msgid "Comedy"
msgstr ""
msgstr "コメディー"
#: src/screens/Onboarding/index.tsx:27
msgid "Comics"
msgstr ""
msgstr "漫画"
#: src/Navigation.tsx:228
#: src/view/screens/CommunityGuidelines.tsx:32
msgid "Community Guidelines"
msgstr "コミュニティガイドライン"
msgstr "コミュニティガイドライン"
#: src/screens/Onboarding/StepFinished.tsx:148
msgid "Complete onboarding and start using your account"
@@ -898,7 +898,7 @@ msgstr "アカウントをフォローせずに次のステップへ進む"
#: src/screens/Onboarding/index.tsx:44
msgid "Cooking"
msgstr ""
msgstr "料理"
#: src/view/com/modals/AddAppPasswords.tsx:195
#: src/view/com/modals/InviteCodes.tsx:182
@@ -997,7 +997,7 @@ msgstr "サムネイル付きのカードを作成します。そのカードは
#: src/screens/Onboarding/index.tsx:29
msgid "Culture"
msgstr ""
msgstr "文化"
#: src/view/com/modals/ChangeHandle.tsx:389
#: src/view/com/modals/ServerInput.tsx:102
@@ -1006,7 +1006,7 @@ msgstr "カスタムドメイン"
#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:106
msgid "Custom feeds built by the community bring you new experiences and help you find the content you love."
msgstr ""
msgstr "コミュニティーによって作成されたカスタムフィードは、あなたに新しい体験をもたらし、あなたが好きなコンテンツを見つけるのに役立ちます。"
#: src/view/screens/PreferencesExternalEmbeds.tsx:55
msgid "Customize media from external sites."
@@ -1027,7 +1027,7 @@ msgstr "ダークモード"
#: src/view/screens/Settings.tsx:492
msgid "Dark Theme"
msgstr ""
msgstr "ダークテーマ"
#: src/Navigation.tsx:204
#~ msgid "Debug"
@@ -1065,7 +1065,7 @@ msgstr "マイアカウントを削除"
#: src/view/screens/Settings.tsx:755
msgid "Delete My Account…"
msgstr ""
msgstr "マイアカウントを削除…"
#: src/view/com/util/forms/PostDropdownBtn.tsx:228
msgid "Delete post"
@@ -1104,7 +1104,7 @@ msgstr "なにか言いたいことはあった?"
#: src/view/screens/Settings.tsx:498
msgid "Dim"
msgstr ""
msgstr "グレー"
#: src/view/com/composer/Composer.tsx:144
msgid "Discard"
@@ -1210,7 +1210,7 @@ msgstr "例:返信として広告を繰り返し送ってくるユーザー。
#: src/view/com/modals/InviteCodes.tsx:96
msgid "Each code works once. You'll receive more invite codes periodically."
msgstr "それぞれのコードは一度ずつ動作します。定期的に招待コードをお送りします。"
msgstr "それぞれのコードは一回限り有効です。定期的に追加の招待コードをお送りします。"
#: src/view/com/lists/ListMembers.tsx:149
msgctxt "action"
@@ -1266,7 +1266,7 @@ msgstr "あなたのプロフィールの説明を編集します"
#: src/screens/Onboarding/index.tsx:34
msgid "Education"
msgstr ""
msgstr "教育"
#: src/view/com/auth/create/Step1.tsx:143
#: src/view/com/auth/create/Step2.tsx:194
@@ -1342,7 +1342,7 @@ msgstr "確認コードを入力してください"
#: src/view/com/modals/ChangePassword.tsx:151
msgid "Enter the code you received to change your password."
msgstr ""
msgstr "パスワードを変更するために受け取ったコードを入力してください。"
#: src/view/com/modals/ChangeHandle.tsx:371
msgid "Enter the domain you want to use"
@@ -1535,7 +1535,7 @@ msgstr "ディスカッションスレッドを微調整します。"
#: src/screens/Onboarding/index.tsx:38
msgid "Fitness"
msgstr ""
msgstr "フィットネス"
#: src/screens/Onboarding/StepFinished.tsx:131
msgid "Flexible"
@@ -1623,7 +1623,7 @@ msgstr "あなたをフォロー"
#: src/screens/Onboarding/index.tsx:43
msgid "Food"
msgstr ""
msgstr "食べ物"
#: src/view/com/modals/DeleteAccount.tsx:111
msgid "For security reasons, we'll need to send a confirmation code to your email address."
@@ -1658,7 +1658,7 @@ msgstr "ギャラリー"
#: src/view/com/modals/VerifyEmail.tsx:189
#: src/view/com/modals/VerifyEmail.tsx:191
msgid "Get Started"
msgstr "はじめに"
msgstr "開始"
#: src/view/com/auth/LoggedOut.tsx:81
#: src/view/com/auth/LoggedOut.tsx:82
@@ -1825,11 +1825,11 @@ msgstr "ALTテキストが長い場合、ALTテキストの展開状態を切り
#: src/view/com/modals/SelfLabel.tsx:127
msgid "If none are selected, suitable for all ages."
msgstr "選択されていない場合は、すべての年齢に適しています。"
msgstr "何も選択しない場合は、全年齢対象です。"
#: 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 "パスワードを変更する場合は、あなたのアカウントであることを確認するためのコードをお送りします。"
#: src/view/com/util/images/Gallery.tsx:38
msgid "Image"
@@ -1969,7 +1969,7 @@ msgstr "Waitlistに参加"
#: src/screens/Onboarding/index.tsx:24
msgid "Journalism"
msgstr ""
msgstr "報道"
#: src/view/com/composer/select-language/SelectLangBtn.tsx:104
msgid "Language selection"
@@ -2272,7 +2272,7 @@ msgstr "モデレーションの設定"
#: src/view/com/modals/ModerationDetails.tsx:35
msgid "Moderator has chosen to set a general warning on the content."
msgstr "モデレーターはその投稿に一般的な警告設定を選択しました。"
msgstr "モデレーターによりコンテンツに一般的な警告設定されました。"
#: src/view/shell/desktop/Feeds.tsx:53
msgid "More feeds"
@@ -2364,7 +2364,7 @@ msgstr "名前は必須です"
#: src/screens/Onboarding/index.tsx:25
msgid "Nature"
msgstr ""
msgstr "自然"
#: src/view/com/auth/login/ForgotPasswordForm.tsx:186
#: src/view/com/auth/login/ForgotPasswordForm.tsx:215
@@ -2411,7 +2411,7 @@ msgstr "新しいパスワード"
#: src/view/com/modals/ChangePassword.tsx:215
msgid "New Password"
msgstr ""
msgstr "新しいパスワード"
#: src/view/com/feeds/FeedPage.tsx:201
msgctxt "action"
@@ -2446,7 +2446,7 @@ msgstr "新しい順に返信を表示"
#: src/screens/Onboarding/index.tsx:23
msgid "News"
msgstr ""
msgstr "ニュース"
#: src/view/com/auth/create/CreateAccount.tsx:161
#: src/view/com/auth/login/ForgotPasswordForm.tsx:178
@@ -2525,7 +2525,7 @@ msgstr "見つかりません"
#: src/view/com/modals/VerifyEmail.tsx:246
#: src/view/com/modals/VerifyEmail.tsx:252
msgid "Not right now"
msgstr "今すぐにではない"
msgstr "今はない"
#: src/view/screens/Moderation.tsx:227
#~ msgid "Note: Bluesky is an open and public network, and enabling this will not make your profile private or limit the ability of logged in users to see your posts. This setting only limits the visibility of posts on the Bluesky app and website; third-party apps that display Bluesky content may not respect this setting, and could show your content to logged-out users."
@@ -2768,7 +2768,7 @@ msgstr "カメラへのアクセスが拒否されました。システムの設
#: src/screens/Onboarding/index.tsx:31
msgid "Pets"
msgstr ""
msgstr "ペット"
#: src/view/com/auth/create/Step2.tsx:183
msgid "Phone number"
@@ -2776,7 +2776,7 @@ msgstr "電話番号"
#: src/view/com/modals/SelfLabel.tsx:121
msgid "Pictures meant for adults."
msgstr "成人向けの写真です。"
msgstr "成人向けの画像です。"
#: src/view/screens/ProfileFeed.tsx:353
#: src/view/screens/ProfileList.tsx:580
@@ -2860,7 +2860,7 @@ msgstr "リンクカードがロードされるまでお待ちください"
#: src/screens/Onboarding/index.tsx:37
msgid "Politics"
msgstr ""
msgstr "政治"
#: src/view/com/modals/SelfLabel.tsx:111
msgid "Porn"
@@ -3187,7 +3187,7 @@ msgstr "コードをリクエスト"
#: src/view/com/modals/ChangePassword.tsx:239
#: src/view/com/modals/ChangePassword.tsx:241
msgid "Request Code"
msgstr ""
msgstr "コードをリクエスト"
#: src/view/screens/Settings.tsx:450
msgid "Require alt text before posting"
@@ -3204,7 +3204,7 @@ msgstr "コードをリセット"
#: src/view/com/modals/ChangePassword.tsx:190
msgid "Reset Code"
msgstr ""
msgstr "コードをリセット"
#: src/view/screens/Settings.tsx:806
msgid "Reset onboarding"
@@ -3312,7 +3312,7 @@ msgstr "{handle}へのハンドルの変更を保存"
#: src/screens/Onboarding/index.tsx:36
msgid "Science"
msgstr ""
msgstr "科学"
#: src/view/screens/ProfileList.tsx:854
msgid "Scroll to top"
@@ -3470,7 +3470,7 @@ msgstr "年齢を設定"
#: src/view/screens/Settings.tsx:482
msgid "Set color theme to dark"
msgstr "カラーテーマをダークに設定します"
msgstr "カラーテーマを暗いものに設定します"
#: src/view/screens/Settings.tsx:475
msgid "Set color theme to light"
@@ -3478,15 +3478,15 @@ msgstr "カラーテーマをライトに設定します"
#: src/view/screens/Settings.tsx:469
msgid "Set color theme to system setting"
msgstr "システム設定のカラーテーマを使用するように設定します"
msgstr "デバイスで設定したカラーテーマを使用するように設定します"
#: src/view/screens/Settings.tsx:508
msgid "Set dark theme to the dark theme"
msgstr ""
msgstr "ダークテーマをダークに設定します"
#: src/view/screens/Settings.tsx:501
msgid "Set dark theme to the dim theme"
msgstr ""
msgstr "ダークテーマを薄暗いものに設定します"
#: src/view/com/auth/login/SetNewPasswordForm.tsx:104
msgid "Set new password"
@@ -3717,7 +3717,7 @@ msgstr "サインアップ"
#: src/view/shell/NavSignupCard.tsx:42
msgid "Sign up or sign in to join the conversation"
msgstr "登録またはログインして会話に参加"
msgstr "サインアップまたはサインインして会話に参加"
#: src/view/com/util/moderation/ScreenHider.tsx:76
msgid "Sign-in Required"
@@ -3751,7 +3751,7 @@ msgstr "SMS認証"
#: src/screens/Onboarding/index.tsx:40
msgid "Software Dev"
msgstr ""
msgstr "ソフトウェア開発"
#: src/view/com/modals/ProfilePreview.tsx:62
msgid "Something went wrong and we're not sure what."
@@ -3775,7 +3775,7 @@ msgstr "次の方法で同じ投稿への返信を並び替えます。"
#: src/screens/Onboarding/index.tsx:30
msgid "Sports"
msgstr ""
msgstr "スポーツ"
#: src/view/com/modals/crop-image/CropImage.web.tsx:122
msgid "Square"
@@ -3837,7 +3837,7 @@ msgstr "あなたへのおすすめ"
#: src/view/com/modals/SelfLabel.tsx:95
msgid "Suggestive"
msgstr "提案"
msgstr "きわどい"
#: src/Navigation.tsx:213
#: src/view/screens/Support.tsx:30
@@ -3881,7 +3881,7 @@ msgstr "タップして全体を表示"
#: src/screens/Onboarding/index.tsx:39
msgid "Tech"
msgstr ""
msgstr "テクノロジー"
#: src/view/shell/desktop/RightNav.tsx:93
msgid "Terms"
@@ -3905,7 +3905,7 @@ msgstr "このアカウントは、ブロック解除後にあなたとやり取
#: src/view/screens/CommunityGuidelines.tsx:36
msgid "The Community Guidelines have been moved to <0/>"
msgstr "コミュニティガイドラインは<0/>に移動しました"
msgstr "コミュニティガイドラインは<0/>に移動しました"
#: src/view/screens/CopyrightPolicy.tsx:33
msgid "The Copyright Policy has been moved to <0/>"
@@ -4105,7 +4105,7 @@ msgstr "このユーザーは、あなたがブロックした<0/>リストに
#: src/view/com/modals/ModerationDetails.tsx:74
msgid "This user is included in the <0/> list which you have muted."
msgstr ""
msgstr "このユーザーは、あなたがミュートした<0/>リストに含まれています。"
#: src/view/com/modals/ModerationDetails.tsx:74
#~ msgid "This user is included the <0/> list which you have muted."
@@ -4113,7 +4113,7 @@ msgstr ""
#: src/view/com/modals/SelfLabel.tsx:137
msgid "This warning is only available for posts with media attached."
msgstr "この警告は、メディアが接続されている投稿にのみ使用できます。"
msgstr "この警告は、メディアが添付されている投稿にのみ使用できます。"
#: src/view/com/util/forms/PostDropdownBtn.tsx:192
msgid "This will hide this post from your feeds."
@@ -4368,7 +4368,7 @@ msgstr "メールアドレスを確認"
#: src/screens/Onboarding/index.tsx:42
msgid "Video Games"
msgstr ""
msgstr "ビデオゲーム"
#: src/view/com/profile/ProfileHeader.tsx:701
msgid "View {0}'s avatar"
@@ -4504,7 +4504,7 @@ msgstr "返信を書く"
#: src/screens/Onboarding/index.tsx:28
msgid "Writers"
msgstr ""
msgstr "ライター"
#: src/view/com/auth/create/Step2.tsx:263
msgid "XXXXXX"
@@ -4568,7 +4568,7 @@ msgstr "保存されたフィードがありません。"
#: src/view/com/post-thread/PostThread.tsx:406
msgid "You have blocked the author or you have been blocked by the author."
msgstr "あなたが者をブロックしているか、または者によってあなたはブロックされています。"
msgstr "あなたが投稿者をブロックしているか、または投稿者によってあなたはブロックされています。"
#: src/view/com/modals/ModerationDetails.tsx:56
msgid "You have blocked this user. You cannot view their content."
@@ -4579,7 +4579,7 @@ msgstr "あなたはこのユーザーをブロックしているため、コン
#: 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 "無効なコードが入力されました。それはXXXXX-XXXXXのようになっているはずです。"
#: src/view/com/modals/ModerationDetails.tsx:87
msgid "You have muted this user."
@@ -4676,7 +4676,7 @@ msgstr "メールアドレスが保存されました!すぐにご連絡いた
#: src/view/com/modals/ChangeEmail.tsx:125
msgid "Your email has been updated but not verified. As a next step, please verify your new email."
msgstr "メールアドレスは更新されましたが、確認されていません。次のステップとして、新しいEメールを確認してください。"
msgstr "メールアドレスは更新されましたが、確認されていません。次のステップとして、新しいメールアドレスを確認してください。"
#: src/view/com/modals/VerifyEmail.tsx:114
msgid "Your email has not yet been verified. This is an important security step which we recommend."
@@ -4706,7 +4706,7 @@ msgstr "アプリパスワードを使用してログインすると、招待コ
#: src/view/com/modals/ChangePassword.tsx:155
msgid "Your password has been changed successfully!"
msgstr ""
msgstr "パスワードの変更が完了しました!"
#: src/view/com/composer/Composer.tsx:267
msgid "Your post has been published"
+20 -305
View File
@@ -8,17 +8,11 @@ msgstr ""
"Language: pt-BR\n"
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2024-02-07 00:25\n"
"PO-Revision-Date: 2024-02-08 19:59\n"
"Last-Translator: maisondasilva\n"
"Language-Team: maisondasilva, MightyLoggor, gildaswise, gleydson, faeriarum\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#~ msgid "- end of feed -"
#~ msgstr "- fim do feed -"
#~ msgid ". This warning is only available for posts with media attached."
#~ msgstr ". Este aviso só aparece em posts com imagens."
#: src/view/com/modals/VerifyEmail.tsx:142
msgid "(no email)"
msgstr "(sem email)"
@@ -27,15 +21,6 @@ msgstr "(sem email)"
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/modals/CreateOrEditList.tsx:185
#: src/view/screens/Settings.tsx:294
#~ msgid "{0}"
#~ msgstr "{0}"
#: src/view/com/modals/CreateOrEditList.tsx:176
#~ msgid "{0} {purposeLabel} List"
#~ msgstr "Lista {0} {purposeLabel}"
#: src/view/com/profile/ProfileHeader.tsx:632
msgid "{following} following"
msgstr "{following} seguindo"
@@ -54,18 +39,10 @@ msgstr "{invitesAvailable} convite disponível"
msgid "{invitesAvailable} invite codes available"
msgstr "{invitesAvailable} convites disponíveis"
#: src/view/screens/Search/Search.tsx:87
#~ msgid "{message}"
#~ msgstr "{message}"
#: src/view/shell/Drawer.tsx:443
msgid "{numUnreadNotifications} unread"
msgstr "{numUnreadNotifications} não lidas"
#: src/Navigation.tsx:147
#~ msgid "@{0}"
#~ msgstr "@{0}"
#: src/view/com/threadgate/WhoCanReply.tsx:158
msgid "<0/> members"
msgstr "<0/> membros"
@@ -82,18 +59,6 @@ msgstr "<0>Escolha seus</0><2>Feeds</2><1>recomendados</1>"
msgid "<0>Follow some</0><1>Recommended</1><2>Users</2>"
msgstr "<0>Siga alguns</0><2>Usuários</2><1>recomendados</1>"
#: src/view/com/modals/AddAppPasswords.tsx:132
#~ msgid "<0>Here is your app password.</0> Use this to sign into the other app along with your handle."
#~ msgstr "<0>Aqui está sua senha de aplicativo.</0> Use-a com seu usuário para logar em outro aplicativo."
#: src/view/screens/Moderation.tsx:212
#~ msgid "<0>Note: This setting may not be respected by third-party apps that display Bluesky content.</0>"
#~ msgstr "<0>Nota: Esta opção pode não ser respeitada por aplicativos de terceiro que mostram conteúdo do Bluesky.</0>"
#: src/view/screens/Moderation.tsx:212
#~ msgid "<0>Note: Your profile and posts will remain publicly available. Third-party apps that display Bluesky content may not respect this setting.</0>"
#~ msgstr "<0>Nota: Seu perfil e posts continuarão públicos. Aplicativos de terceiros que mostram conteúdo do Bluesky podem não respeitar esta opção."
#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:21
msgid "<0>Welcome to</0><1>Bluesky</1>"
msgstr "<0>Bem-vindo ao</0><1>Bluesky</1>"
@@ -260,7 +225,7 @@ msgstr "Avançado"
#: src/view/com/auth/login/ForgotPasswordForm.tsx:217
#: src/view/com/modals/ChangePassword.tsx:168
msgid "Already have a code?"
msgstr ""
msgstr "Já tem um código?"
#: src/view/com/auth/login/ChooseAccountForm.tsx:98
msgid "Already signed in as @{0}"
@@ -338,10 +303,6 @@ msgstr "Contestar aviso de conteúdo"
msgid "Appeal Content Warning"
msgstr "Contestar aviso de conteúdo"
#: src/view/com/modals/AppealLabel.tsx:65
#~ msgid "Appeal Decision"
#~ msgstr "Contestar Decisão"
#: src/view/com/util/moderation/LabelInfo.tsx:52
msgid "Appeal this decision"
msgstr "Contestar esta decisão"
@@ -354,10 +315,6 @@ msgstr "Contestar esta decisão."
msgid "Appearance"
msgstr "Aparência"
#: src/view/screens/Moderation.tsx:206
#~ msgid "Apps that respect this setting, including the official Bluesky app and bsky.app website, won't show your content to logged out users."
#~ msgstr "Aplicativos que respeitam esta configuração, incluindo os aplicativos oficiais do Bluesky e o site, não mostrarão seu conteúdo para usuários deslogados."
#: src/view/screens/AppPasswords.tsx:224
msgid "Are you sure you want to delete the app password \"{name}\"?"
msgstr "Tem certeza de que deseja excluir a senha do aplicativo \"{name}\"?"
@@ -386,10 +343,6 @@ msgstr "Arte"
msgid "Artistic or non-erotic nudity."
msgstr "Nudez artística ou não erótica."
#: src/view/screens/Moderation.tsx:189
#~ msgid "Ask apps to limit the visibility of my account"
#~ msgstr "Exigir visibilidade limitada da minha conta"
#: src/view/com/auth/create/CreateAccount.tsx:147
#: src/view/com/auth/login/ChooseAccountForm.tsx:151
#: src/view/com/auth/login/ForgotPasswordForm.tsx:170
@@ -589,10 +542,6 @@ msgstr "Cancelar"
msgid "Cancel account deletion"
msgstr "Cancelar exclusão da conta"
#: src/view/com/modals/AltImage.tsx:123
#~ msgid "Cancel add image alt text"
#~ msgstr "Cancelar adição de texto alternativo da imagem"
#: src/view/com/modals/ChangeHandle.tsx:149
msgid "Cancel change handle"
msgstr "Cancelar alteração de usuário"
@@ -623,10 +572,6 @@ msgctxt "action"
msgid "Change"
msgstr "Alterar"
#: src/view/screens/Settings.tsx:306
#~ msgid "Change"
#~ msgstr "Alterar"
#: src/view/screens/Settings.tsx:690
msgid "Change handle"
msgstr "Alterar usuário"
@@ -642,11 +587,11 @@ msgstr "Alterar meu email"
#: src/view/screens/Settings.tsx:726
msgid "Change password"
msgstr ""
msgstr "Alterar senha"
#: src/view/screens/Settings.tsx:735
msgid "Change Password"
msgstr ""
msgstr "Alterar Senha"
#: src/view/com/composer/select-language/SuggestedLanguage.tsx:73
msgid "Change post language to {0}"
@@ -654,7 +599,7 @@ msgstr "Trocar idioma do post para {0}"
#: src/view/screens/Settings.tsx:727
msgid "Change your Bluesky password"
msgstr ""
msgstr "Alterar sua senha do Bluesky"
#: src/view/com/modals/ChangeEmail.tsx:109
msgid "Change Your Email"
@@ -698,10 +643,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/view/com/auth/onboarding/RecommendedFeeds.tsx:65
#~ msgid "Choose your"
#~ msgstr "Escolha seu"
#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:103
#~ msgid "Choose your algorithmic feeds"
#~ msgstr "Escolha seus feeds algoritmicos"
@@ -748,7 +689,7 @@ msgstr "Clima e tempo"
#: src/view/com/modals/ChangePassword.tsx:265
#: src/view/com/modals/ChangePassword.tsx:268
msgid "Close"
msgstr ""
msgstr "Fechar"
#: src/components/Dialog/index.web.tsx:78
msgid "Close active dialog"
@@ -1036,7 +977,7 @@ msgstr "Domínio personalizado"
#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:106
msgid "Custom feeds built by the community bring you new experiences and help you find the content you love."
msgstr ""
msgstr "Feeds customizados feitos pela comunidade te proporcionam novas experiências e te ajudam a encontrar o conteúdo que você mais ama."
#: src/view/screens/PreferencesExternalEmbeds.tsx:55
msgid "Customize media from external sites."
@@ -1057,11 +998,7 @@ msgstr "Modo escuro"
#: src/view/screens/Settings.tsx:492
msgid "Dark Theme"
msgstr ""
#: src/Navigation.tsx:204
#~ msgid "Debug"
#~ msgstr "Depuração"
msgstr "Modo Escuro"
#: src/view/screens/Debug.tsx:83
msgid "Debug panel"
@@ -1095,7 +1032,7 @@ msgstr "Excluir minha conta"
#: src/view/screens/Settings.tsx:755
msgid "Delete My Account…"
msgstr ""
msgstr "Excluir minha conta…"
#: src/view/com/util/forms/PostDropdownBtn.tsx:228
msgid "Delete post"
@@ -1120,10 +1057,6 @@ msgstr "Post excluído."
msgid "Description"
msgstr "Descrição"
#: src/view/com/auth/create/Step1.tsx:96
#~ msgid "Dev Server"
#~ msgstr "Servidor de Desenvolvimento"
#: src/view/screens/Settings.tsx:760
msgid "Developer Tools"
msgstr "Ferramentas de Desenvolvedor"
@@ -1134,7 +1067,7 @@ msgstr "Você gostaria de dizer alguma coisa?"
#: src/view/screens/Settings.tsx:498
msgid "Dim"
msgstr ""
msgstr "Menos escuro"
#: src/view/com/composer/Composer.tsx:144
msgid "Discard"
@@ -1366,13 +1299,9 @@ msgstr "Insira um nome para esta Senha de Aplicativo"
msgid "Enter Confirmation Code"
msgstr "Insira o código de confirmação"
#: src/view/com/auth/create/Step1.tsx:71
#~ msgid "Enter the address of your provider:"
#~ msgstr "Digite o endereço do seu provedor:"
#: src/view/com/modals/ChangePassword.tsx:151
msgid "Enter the code you received to change your password."
msgstr ""
msgstr "Digite o código recebido para alterar sua senha."
#: src/view/com/modals/ChangeHandle.tsx:371
msgid "Enter the domain you want to use"
@@ -1602,14 +1531,6 @@ msgstr "Seguir Todas"
msgid "Follow selected accounts and continue to the next step"
msgstr "Siga algumas contas e continue para o próximo passo"
#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:174
#~ msgid "Follow selected accounts and continue to then next step"
#~ msgstr "Siga algumas contas e continue para o próximo passo"
#: src/view/com/auth/onboarding/RecommendedFollows.tsx:42
#~ msgid "Follow some"
#~ msgstr "Siga alguns"
#: 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."
msgstr "Comece seguindo alguns usuários. Mais usuários podem ser recomendados com base em quem você acha interessante."
@@ -1634,10 +1555,6 @@ msgstr "seguiu você"
msgid "Followers"
msgstr "Seguidores"
#: src/view/com/profile/ProfileHeader.tsx:624
#~ msgid "following"
#~ msgstr "seguindo"
#: src/view/com/profile/ProfileHeader.tsx:534
#: src/view/screens/ProfileFollows.tsx:25
msgid "Following"
@@ -1743,10 +1660,6 @@ msgstr "Ajuda"
msgid "Here are some accounts for you to follow"
msgstr "Aqui estão algumas contas para você seguir"
#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:132
#~ msgid "Here are some accounts for your to follow"
#~ msgstr "Aqui estão algumas contas para você seguir"
#: src/screens/Onboarding/StepTopicalFeeds.tsx:79
msgid "Here are some popular topical feeds. You can choose to follow as many as you like."
msgstr "Aqui estão alguns feeds de assuntos. Você pode seguir quantos quiser."
@@ -1793,10 +1706,6 @@ msgstr "Ocultar lista de usuários"
msgid "Hides posts from {0} in your feed"
msgstr "Esconder posts de {0} no seu feed"
#: src/view/com/posts/FeedErrorMessage.tsx:102
#~ msgid "Hmm, some kind of issue occured when contacting the feed server. Please let the feed owner know about this issue."
#~ msgstr "Hmm, ocorreu algum problema ao entrar em contato com o servidor deste feed. Por favor, avise o criador do feed sobre este problema."
#: src/view/com/posts/FeedErrorMessage.tsx:111
msgid "Hmm, some kind of issue occurred when contacting the feed server. Please let the feed owner know about this issue."
msgstr "Hmm, ocorreu algum problema ao entrar em contato com o servidor deste feed. Por favor, avise o criador do feed sobre este problema."
@@ -1817,10 +1726,6 @@ msgstr "Hmm, o servidor do feed teve algum problema. Por favor, avise o criador
msgid "Hmm, we're having trouble finding this feed. It may have been deleted."
msgstr "Hmm, estamos com problemas para encontrar este feed. Ele pode ter sido excluído."
#: src/view/com/posts/FeedErrorMessage.tsx:87
#~ msgid "Hmmm, we're having trouble finding this feed. It may have been deleted."
#~ msgstr "Hmm, estamos com problemas para encontrar este feed. Ele pode ter sido excluído."
#: src/Navigation.tsx:433
#: src/view/shell/bottom-bar/BottomBar.tsx:137
#: src/view/shell/desktop/LeftNav.tsx:306
@@ -1840,11 +1745,6 @@ msgstr "Preferências da Página Inicial"
msgid "Hosting provider"
msgstr "Provedor de hospedagem"
#: src/view/com/auth/create/Step1.tsx:76
#: src/view/com/auth/create/Step1.tsx:81
#~ msgid "Hosting provider address"
#~ msgstr "Endereço do provedor de hospedagem"
#: src/view/com/modals/InAppBrowserConsent.tsx:44
msgid "How should we open this link?"
msgstr "Como devemos abrir este link?"
@@ -1871,7 +1771,7 @@ msgstr "Se nenhum for selecionado, adequado para todas as idades."
#: 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 "Se você quiser alterar sua senha, enviaremos um código que para verificar sua identidade."
#: src/view/com/util/images/Gallery.tsx:38
msgid "Image"
@@ -1886,11 +1786,6 @@ msgstr "Texto alternativo da imagem"
msgid "Image options"
msgstr "Opções de imagem"
#: src/view/com/search/Suggestions.tsx:104
#: src/view/com/search/Suggestions.tsx:115
#~ msgid "In Your Network"
#~ msgstr "Na sua rede"
#: src/view/com/auth/login/SetNewPasswordForm.tsx:138
msgid "Input code sent to your email for password reset"
msgstr "Insira o código enviado para o seu e-mail para redefinir sua senha"
@@ -1903,14 +1798,6 @@ msgstr "Insira o código de confirmação para excluir sua conta"
msgid "Input email for Bluesky account"
msgstr "Insira o e-mail para a sua conta do Bluesky"
#: src/view/com/auth/create/Step2.tsx:109
#~ msgid "Input email for Bluesky waitlist"
#~ msgstr "Insira o e-mail para entrar na lista de espera do Bluesky"
#: src/view/com/auth/create/Step1.tsx:80
#~ msgid "Input hosting provider address"
#~ msgstr "Insira o endereço do provedor de hospedagem"
#: src/view/com/auth/create/Step1.tsx:102
msgid "Input invite code to proceed"
msgstr "Insira o convite para continuar"
@@ -2124,14 +2011,6 @@ msgstr "Curtido por {likeCount} {0}"
msgid "liked your custom feed"
msgstr "curtiram seu feed"
#: src/view/com/notifications/FeedItem.tsx:171
#~ msgid "liked your custom feed '{0}'"
#~ msgstr "curtiram seu feed '{0}'"
#: src/view/com/notifications/FeedItem.tsx:171
#~ msgid "liked your custom feed{0}"
#~ msgstr "curtiu seu feed"
#: src/view/com/notifications/FeedItem.tsx:155
msgid "liked your post"
msgstr "curtiu seu post"
@@ -2144,14 +2023,6 @@ msgstr "Curtidas"
msgid "Likes on this post"
msgstr "Curtidas neste post"
#: src/view/screens/Moderation.tsx:203
#~ msgid "Limit the visibility of my account"
#~ msgstr "Limitar a visibilidade do meu perfil"
#: src/view/screens/Moderation.tsx:203
#~ msgid "Limit the visibility of my account to logged-out users"
#~ msgstr "Limitar a visibilidade do meu perfil para usuários deslogados"
#: src/Navigation.tsx:167
msgid "List"
msgstr "Lista"
@@ -2231,10 +2102,6 @@ msgstr "Registros"
msgid "Log out"
msgstr "Sair"
#: src/view/screens/Moderation.tsx:134
#~ msgid "Logged-out users"
#~ msgstr "Visibilidade do seu perfil"
#: src/view/screens/Moderation.tsx:136
msgid "Logged-out visibility"
msgstr "Visibilidade do seu perfil"
@@ -2243,10 +2110,6 @@ msgstr "Visibilidade do seu perfil"
msgid "Login to account that is not listed"
msgstr "Fazer login em uma conta que não está listada"
#: src/view/screens/ProfileFeed.tsx:472
#~ msgid "Looks like this feed is only available to users with a Bluesky account. Please sign up or sign in to view this feed!"
#~ msgstr "Parece que este feed só está disponível para usuários com uma conta do Bluesky. Por favor, cadastre-se ou entre para ver este feed!"
#: src/view/com/modals/LinkWarning.tsx:65
msgid "Make sure this is where you intend to go!"
msgstr "Certifique-se de onde está indo!"
@@ -2268,10 +2131,6 @@ msgstr "Usuários mencionados"
msgid "Menu"
msgstr "Menu"
#: src/view/com/posts/FeedErrorMessage.tsx:194
#~ msgid "Message from server"
#~ msgstr "Mensagem do servidor"
#: src/view/com/posts/FeedErrorMessage.tsx:197
msgid "Message from server: {0}"
msgstr "Mensagem do servidor: {0}"
@@ -2388,10 +2247,6 @@ msgstr "Contas silenciadas não aparecem no seu feed ou nas suas notificações.
msgid "Muting is private. Muted accounts can interact with you, but you will not see their posts or receive notifications from them."
msgstr "Silenciar é privado. Contas silenciadas podem interagir com você, mas você não verá postagens ou receber notificações delas."
#: src/view/screens/Moderation.tsx:134
#~ msgid "My Account"
#~ msgstr "Minha Conta"
#: src/view/com/modals/BirthDateSettings.tsx:56
msgid "My Birthday"
msgstr "Meu Aniversário"
@@ -2466,7 +2321,7 @@ msgstr "Nova senha"
#: src/view/com/modals/ChangePassword.tsx:215
msgid "New Password"
msgstr ""
msgstr "Nova Senha"
#: src/view/com/feeds/FeedPage.tsx:201
msgctxt "action"
@@ -2487,10 +2342,6 @@ msgctxt "action"
msgid "New Post"
msgstr "Novo Post"
#: src/view/shell/desktop/LeftNav.tsx:258
#~ msgid "New Post"
#~ msgstr "Novo Post"
#: src/view/com/modals/CreateOrEditList.tsx:247
msgid "New User List"
msgstr "Nova lista de usuários"
@@ -2555,11 +2406,6 @@ msgstr "Nenhum resultado"
msgid "No results found for \"{query}\""
msgstr "Nenhum resultado encontrado para \"{query}\""
#: src/view/com/modals/ListAddUser.tsx:142
#: src/view/shell/desktop/Search.tsx:112
#~ msgid "No results found for {0}"
#~ msgstr "Nenhum resultado encontrado para {0}"
#: src/view/com/modals/ListAddRemoveUsers.tsx:127
#: src/view/screens/Search/Search.tsx:280
#: src/view/screens/Search/Search.tsx:308
@@ -2574,10 +2420,6 @@ msgstr "Não, obrigado"
msgid "Nobody"
msgstr "Ninguém"
#: src/view/com/modals/SelfLabel.tsx:136
#~ msgid "Not Applicable"
#~ msgstr "Não Aplicável"
#: src/view/com/modals/SelfLabel.tsx:135
msgid "Not Applicable."
msgstr "Não Aplicável."
@@ -2591,18 +2433,10 @@ msgstr "Não encontrado"
msgid "Not right now"
msgstr "Agora não"
#: src/view/screens/Moderation.tsx:227
#~ msgid "Note: Bluesky is an open and public network, and enabling this will not make your profile private or limit the ability of logged in users to see your posts. This setting only limits the visibility of posts on the Bluesky app and website; third-party apps that display Bluesky content may not respect this setting, and could show your content to logged-out users."
#~ msgstr "Nota: o Bluesky é uma rede aberta e pública. Habilitar esta configuração não tornará seu perfil privado nem impedirá os usuários logados de verem os seus posts. Esta configuração só limita a visibilidade dos posts nos apps oficiais do Bluesky; aplicativos de terceiros podem não respeitá-la e poderão mostrar seu conteúdo para usuários deslogados."
#: src/view/screens/Moderation.tsx:233
msgid "Note: Bluesky is an open and public network. This setting only limits the visibility of your content on the Bluesky app and website, and other apps may not respect this setting. Your content may still be shown to logged-out users by other apps and websites."
msgstr "Nota: o Bluesky é uma rede aberta e pública. Esta configuração limita somente a visibilidade do seu conteúdo no site e aplicativo do Bluesky, e outros aplicativos podem não respeitar esta configuração. Seu conteúdo ainda poderá ser exibido para usuários deslogados por outros aplicativos e sites."
#: src/view/screens/Moderation.tsx:227
#~ msgid "Note: Third-party apps that display Bluesky content may not respect this setting."
#~ msgstr "Nota: Aplicativos de terceiros que mostram conteúdo do Bluesky podem não respeitar esta opção."
#: src/Navigation.tsx:448
#: src/view/screens/Notifications.tsx:120
#: src/view/screens/Notifications.tsx:144
@@ -2778,10 +2612,6 @@ msgstr "Opção {0} de {numItems}"
msgid "Or combine these options:"
msgstr "Ou combine estas opções:"
#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:122
#~ msgid "Or you can try our \"Discover\" algorithm:"
#~ msgstr "Ou você pode tentar nosso algoritmo \"Discover\":"
#: src/view/com/auth/login/ChooseAccountForm.tsx:138
msgid "Other account"
msgstr "Outra conta"
@@ -2945,12 +2775,6 @@ msgctxt "description"
msgid "Post"
msgstr "Post"
#: src/view/com/composer/Composer.tsx:346
#: src/view/com/post-thread/PostThread.tsx:225
#: src/view/screens/PostThread.tsx:80
#~ msgid "Post"
#~ msgstr "Post"
#: src/view/com/post-thread/PostThreadItem.tsx:177
msgid "Post by {0}"
msgstr "Post por {0}"
@@ -3071,10 +2895,6 @@ msgctxt "action"
msgid "Quote Post"
msgstr "Citar Post"
#: src/view/com/modals/Repost.tsx:56
#~ msgid "Quote Post"
#~ msgstr "Citar Post"
#: src/view/screens/PreferencesThreads.tsx:86
msgid "Random (aka \"Poster's Roulette\")"
msgstr "Aleatório"
@@ -3083,11 +2903,6 @@ msgstr "Aleatório"
msgid "Ratios"
msgstr "Índices"
#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:73
#: src/view/com/auth/onboarding/RecommendedFollows.tsx:50
#~ msgid "Recommended"
#~ msgstr "Recomendados"
#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:116
msgid "Recommended Feeds"
msgstr "Feeds Recomendados"
@@ -3220,10 +3035,6 @@ msgstr "Repostar"
msgid "Repost or quote post"
msgstr "Repostar ou citar um post"
#: src/view/screens/PostRepostedBy.tsx:27
#~ msgid "Reposted by"
#~ msgstr "Repostado por"
#: src/view/screens/PostRepostedBy.tsx:27
msgid "Reposted By"
msgstr "Repostado Por"
@@ -3232,10 +3043,6 @@ msgstr "Repostado Por"
msgid "Reposted by {0}"
msgstr "Repostado por {0}"
#: src/view/com/posts/FeedItem.tsx:206
#~ msgid "Reposted by {0})"
#~ msgstr "Repostado por {0})"
#: src/view/com/posts/FeedItem.tsx:224
msgid "Reposted by <0/>"
msgstr "Repostado por <0/>"
@@ -3260,11 +3067,7 @@ msgstr "Solicitar código"
#: src/view/com/modals/ChangePassword.tsx:239
#: src/view/com/modals/ChangePassword.tsx:241
msgid "Request Code"
msgstr ""
#: src/view/screens/Moderation.tsx:188
#~ msgid "Request to limit the visibility of my account"
#~ msgstr "Exigir limitação de visibilidade da minha conta"
msgstr "Solicitar Código"
#: src/view/screens/Settings.tsx:450
msgid "Require alt text before posting"
@@ -3281,7 +3084,7 @@ msgstr "Código de redefinição"
#: src/view/com/modals/ChangePassword.tsx:190
msgid "Reset Code"
msgstr ""
msgstr "Código de Redefinição"
#: src/view/screens/Settings.tsx:806
msgid "Reset onboarding"
@@ -3332,10 +3135,6 @@ msgstr "Tenta a última ação, que deu erro"
msgid "Retry"
msgstr "Tente novamente"
#: src/view/com/modals/ChangeHandle.tsx:169
#~ msgid "Retry change handle"
#~ msgstr "Tentar troca de usuário novamente"
#: src/view/com/auth/create/Step2.tsx:247
msgid "Retry."
msgstr "Tentar novamente."
@@ -3367,10 +3166,6 @@ msgstr "Salvar"
msgid "Save alt text"
msgstr "Salvar texto alternativo"
#: src/view/com/modals/UserAddRemoveLists.tsx:212
#~ msgid "Save changes"
#~ msgstr "Salvar alterações"
#: src/view/com/modals/EditProfile.tsx:232
msgid "Save Changes"
msgstr "Salvar Alterações"
@@ -3425,10 +3220,6 @@ msgstr "Buscar"
msgid "Search for \"{query}\""
msgstr "Pesquisar por \"{query}\""
#: src/view/screens/Search/Search.tsx:390
#~ msgid "Search for posts and users."
#~ msgstr "Buscar por posts e usuários."
#: src/view/com/auth/LoggedOut.tsx:104
#: src/view/com/auth/LoggedOut.tsx:105
#: src/view/com/modals/ListAddRemoveUsers.tsx:70
@@ -3526,10 +3317,6 @@ msgctxt "action"
msgid "Send Email"
msgstr "Enviar E-mail"
#: src/view/com/modals/DeleteAccount.tsx:138
#~ msgid "Send Email"
#~ msgstr "Enviar Email"
#: src/view/shell/Drawer.tsx:298
#: src/view/shell/Drawer.tsx:319
msgid "Send feedback"
@@ -3567,11 +3354,11 @@ msgstr "Definir o tema para acompanhar o sistema"
#: src/view/screens/Settings.tsx:508
msgid "Set dark theme to the dark theme"
msgstr ""
msgstr "Definir o tema escuro para o padrão"
#: src/view/screens/Settings.tsx:501
msgid "Set dark theme to the dim theme"
msgstr ""
msgstr "Definir o tema escuro para a versão menos escura"
#: src/view/com/auth/login/SetNewPasswordForm.tsx:104
msgid "Set new password"
@@ -3617,10 +3404,6 @@ msgstr "Configura o e-mail para recuperação de senha"
msgid "Sets hosting provider for password reset"
msgstr "Configura o provedor de hospedagem para recuperação de senha"
#: src/view/com/auth/create/Step1.tsx:143
#~ msgid "Sets hosting provider to {label}"
#~ msgstr "Configura o provedor de hospedagem para {label}"
#: src/view/com/auth/create/Step1.tsx:78
#: src/view/com/auth/login/LoginForm.tsx:148
msgid "Sets server for the Bluesky client"
@@ -3653,10 +3436,6 @@ msgstr "Compartilhar"
msgid "Share feed"
msgstr "Compartilhar feed"
#: src/view/screens/ProfileFeed.tsx:276
#~ msgid "Share link"
#~ msgstr "Compartilhar link"
#: src/screens/Onboarding/StepModeration/ModerationOption.tsx:43
#: src/view/com/modals/ContentFilteringSettings.tsx:261
#: src/view/com/util/moderation/ContentHider.tsx:107
@@ -3882,10 +3661,6 @@ msgstr "Página de status"
msgid "Step {0} of {numSteps}"
msgstr "Passo {0} de {numSteps}"
#: src/view/com/auth/create/StepHeader.tsx:15
#~ msgid "Step {step} of 3"
#~ msgstr "Passo {step} de 3"
#: src/view/screens/Settings.tsx:276
msgid "Storage cleared, you need to restart the app now."
msgstr "Armazenamento limpo, você precisa reiniciar o app agora."
@@ -3912,10 +3687,6 @@ msgstr "Increver-se no feed {0}"
msgid "Subscribe to this list"
msgstr "Inscreva-se nesta lista"
#: src/view/com/lists/ListCard.tsx:101
#~ msgid "Subscribed"
#~ msgstr "Inscrito"
#: src/view/screens/Search/Search.tsx:373
msgid "Suggested Follows"
msgstr "Sugestões de Seguidores"
@@ -4016,10 +3787,6 @@ msgstr "A Política de Privacidade foi movida para <0/>"
msgid "The support form has been moved. If you need help, please <0/> or visit {HELP_DESK_URL} to get in touch with us."
msgstr "O formulário de suporte foi movido. Se precisar de ajuda, <0/> ou visite {HELP_DESK_URL} para entrar em contato conosco."
#: src/view/screens/Support.tsx:36
#~ msgid "The support form has been moved. If you need help, please<0/> or visit {HELP_DESK_URL} to get in touch with us."
#~ msgstr "O formulário de suporte foi movido. Se precisar de ajuda, por favor <0/> ou visite {HELP_DESK_URL} para entrar em contato conosco."
#: src/view/screens/TermsOfService.tsx:33
msgid "The Terms of Service have been moved to"
msgstr "Os Termos de Serviço foram movidos para"
@@ -4114,14 +3881,6 @@ msgstr "Houve um problema com este número. Por favor, escolha um país e digite
msgid "These are popular accounts you might like:"
msgstr "Estas são contas populares que talvez você goste:"
#: 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."
#: src/view/com/util/moderation/LabelInfo.tsx:45
#~ msgid "This {0} has been labeled."
#~ msgstr "Este {0} foi reportado."
#: src/view/com/util/moderation/ScreenHider.tsx:88
msgid "This {screenDescription} has been flagged:"
msgstr "Este {screenDescription} foi reportado:"
@@ -4164,10 +3923,6 @@ msgstr "Esta informação não é compartilhada com outros usuários."
msgid "This is important in case you ever need to change your email or reset your password."
msgstr "Isso é importante caso você precise alterar seu e-mail ou redefinir sua senha."
#: src/view/com/auth/create/Step1.tsx:55
#~ msgid "This is the service that keeps you online."
#~ msgstr "Este é o serviço que o mantém online."
#: src/view/com/modals/LinkWarning.tsx:58
msgid "This link is taking you to the following website:"
msgstr "Este link está levando você ao seguinte site:"
@@ -4194,7 +3949,7 @@ msgstr "Este usuário está incluído na lista <0/>, que você bloqueou."
#: src/view/com/modals/ModerationDetails.tsx:74
msgid "This user is included in the <0/> list which you have muted."
msgstr ""
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."
@@ -4240,10 +3995,6 @@ msgctxt "action"
msgid "Try again"
msgstr "Tentar novamente"
#: src/view/com/util/error/ErrorScreen.tsx:73
#~ msgid "Try again"
#~ msgstr "Tente novamente"
#: src/view/screens/ProfileList.tsx:505
msgid "Un-block list"
msgstr "Desbloquear lista"
@@ -4426,10 +4177,6 @@ msgstr "Usuários"
msgid "users followed by <0/>"
msgstr "usuários seguidos por <0/>"
#: src/view/com/threadgate/WhoCanReply.tsx:115
#~ msgid "Users followed by <0/>"
#~ msgstr "Usuários seguidos por <0/>"
#: src/view/com/modals/Threadgate.tsx:106
msgid "Users in \"{0}\""
msgstr "Usuários em \"{0}\""
@@ -4504,10 +4251,6 @@ msgstr "Estimamos que sua conta estará pronta em mais ou menos {estimatedTime}.
msgid "We hope you have a wonderful time. Remember, Bluesky is:"
msgstr "Esperamos que você se divirta. Lembre-se, o Bluesky é:"
#: src/view/com/posts/DiscoverFallbackHeader.tsx:29
#~ msgid "We ran out of posts from your follows. Here's the latest from"
#~ msgstr "Não temos mais posts de quem você segue. Aqui estão os mais novos de"
#: src/view/com/posts/DiscoverFallbackHeader.tsx:29
msgid "We ran out of posts from your follows. Here's the latest from <0/>."
msgstr "Não temos mais posts de quem você segue. Aqui estão os mais novos de <0/>."
@@ -4540,14 +4283,6 @@ msgstr "Usaremos isto para customizar a sua experiência."
msgid "We're so excited to have you join us!"
msgstr "Estamos muito felizes em recebê-lo!"
#: src/view/com/posts/FeedErrorMessage.tsx:99
#~ msgid "We're sorry, but this content is not viewable without a Bluesky account."
#~ msgstr "Desculpe, mas este conteúdo não é visível sem uma conta do Bluesky."
#: src/view/com/posts/FeedErrorMessage.tsx:105
#~ msgid "We're sorry, but this feed is currently receiving high traffic and is temporarily unavailable. Please try again later."
#~ msgstr "Desculpe, mas este feed está recebendo muito tráfego e está temporariamente indisponível. Por favor, tente novamente mais tarde."
#: src/view/screens/ProfileList.tsx:85
msgid "We're sorry, but we were unable to resolve this list. If this persists, please contact the list creator, @{handleOrDid}."
msgstr "Tivemos um problema ao exibir esta lista. Se continuar acontecendo, contate o criador da lista: @{handleOrDid}."
@@ -4638,10 +4373,6 @@ msgstr "Você também pode descobrir novos feeds para seguir."
#~ msgid "You can also try our \"Discover\" algorithm:"
#~ msgstr "Você também pode tentar nosso algoritmo \"Discover\":"
#: src/view/com/auth/create/Step1.tsx:106
#~ msgid "You can change hosting providers at any time."
#~ msgstr "Você pode alterar os provedores de hospedagem a qualquer momento."
#: src/screens/Onboarding/StepFollowingFeed.tsx:142
msgid "You can change these settings later."
msgstr "Você pode mudar estas configurações depois."
@@ -4680,7 +4411,7 @@ msgstr "Você bloqueou este usuário. Você não pode ver este conteúdo."
#: 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 "Você utilizou um código inválido. O código segue este padrão: XXXXX-XXXXX."
#: src/view/com/modals/ModerationDetails.tsx:87
msgid "You have muted this user."
@@ -4795,10 +4526,6 @@ msgstr "Seu identificador completo será"
msgid "Your full handle will be <0>@{0}</0>"
msgstr "Seu usuário completo será <0>@{0}</0>"
#: src/view/com/auth/create/Step1.tsx:53
#~ msgid "Your hosting provider"
#~ msgstr "Seu provedor de hospedagem"
#: src/view/screens/Settings.tsx:430
#: src/view/shell/desktop/RightNav.tsx:137
#: src/view/shell/Drawer.tsx:660
@@ -4807,7 +4534,7 @@ msgstr "Seus códigos de convite estão ocultos quando conectado com uma Senha d
#: src/view/com/modals/ChangePassword.tsx:155
msgid "Your password has been changed successfully!"
msgstr ""
msgstr "Sua senha foi alterada com sucesso!"
#: src/view/com/composer/Composer.tsx:267
msgid "Your post has been published"
@@ -4824,18 +4551,6 @@ msgstr "Suas postagens, curtidas e bloqueios são públicos. Silenciamentos são
msgid "Your profile"
msgstr "Seu perfil"
#: src/view/screens/Moderation.tsx:205
#~ msgid "Your profile and account will not be visible to anyone visiting the Bluesky app without an account, or to account holders who are not logged in. Enabling this will not make your profile private."
#~ msgstr "Seu perfil e conta não serão visíveis para pessoas utilizando o app Bluesky sem uma conta, ou para pessoas que têm conta mas estão deslogadas. Habilitar esta opção não torna a sua conta privada."
#: src/view/screens/Moderation.tsx:220
#~ msgid "Your profile and content will not be visible to anyone visiting the Bluesky app without an account. Enabling this will not make your profile private."
#~ msgstr "Seu perfil e seu conteúdo não serão visíveis para pessoas utilizando o app Bluesky sem uma conta. Habilitar esta opção não torna a sua conta privada."
#: src/view/screens/Moderation.tsx:220
#~ msgid "Your profile and posts will not be visible to people visiting the Bluesky app or website without having an account and being logged in."
#~ msgstr "Seu perfil e posts não serão visíveis para pessoas utilizando o app ou site do Bluesky sem uma conta logada."
#: src/view/com/composer/Composer.tsx:266
msgid "Your reply has been published"
msgstr "Sua resposta foi publicada"
+105 -105
View File
@@ -1,6 +1,6 @@
msgid ""
msgstr ""
"POT-Creation-Date: 2024-02-01 09:20+0800\n"
"POT-Creation-Date: 2024-02-07 19:20+0800\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
@@ -9,8 +9,8 @@ msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: \n"
"Last-Translator: Frudrax Cheng <i@cynosura.one>\n"
"Language-Team: Frudrax Cheng, Simon Chan, U2FsdGVkX1\n"
"Last-Translator: Mikan Harada <i@3kn.jp>\n"
"Language-Team: Frudrax Cheng, Simon Chan, U2FsdGVkX1, Mikan Harada\n"
"Plural-Forms: \n"
#: src/view/com/modals/VerifyEmail.tsx:142
@@ -19,7 +19,7 @@ msgstr "(没有邮件)"
#: src/view/shell/desktop/RightNav.tsx:168
msgid "{0, plural, one {# invite code available} other {# invite codes available}}"
msgstr "{0, plural, one {# 邀请码可用} other {# 邀请码可用}}"
msgstr "{0, plural, one {# 邀请码可用} other {# 邀请码可用}}"
#: src/view/com/profile/ProfileHeader.tsx:632
msgid "{following} following"
@@ -32,12 +32,12 @@ msgstr "{invitesAvailable, plural, one {邀请码: # 可用} other {邀请码: #
#: src/view/screens/Settings.tsx:435
#: src/view/shell/Drawer.tsx:664
msgid "{invitesAvailable} invite code available"
msgstr "{invitesAvailable} 邀请码可用"
msgstr "{invitesAvailable} 邀请码可用"
#: src/view/screens/Settings.tsx:437
#: src/view/shell/Drawer.tsx:666
msgid "{invitesAvailable} invite codes available"
msgstr "{invitesAvailable} 邀请码可用"
msgstr "{invitesAvailable} 邀请码可用"
#: src/view/shell/Drawer.tsx:443
msgid "{numUnreadNotifications} unread"
@@ -65,7 +65,7 @@ msgstr "<0>欢迎来到</0><1>Bluesky</1>"
#: src/view/com/profile/ProfileHeader.tsx:597
msgid "⚠Invalid Handle"
msgstr "⚠无效的昵称"
msgstr "⚠无效的用户识别符"
#: src/view/com/util/moderation/LabelInfo.tsx:45
msgid "A content warning has been applied to this {0}."
@@ -101,15 +101,15 @@ msgstr "已屏蔽账户"
#: src/view/com/profile/ProfileHeader.tsx:260
msgid "Account muted"
msgstr "已静音账户"
msgstr "已隐藏账户"
#: src/view/com/modals/ModerationDetails.tsx:86
msgid "Account Muted"
msgstr "已静音账户"
msgstr "已隐藏账户"
#: src/view/com/modals/ModerationDetails.tsx:72
msgid "Account Muted by List"
msgstr "账户已被列表静音"
msgstr "账户已被列表隐藏"
#: src/view/com/util/AccountDropdownBtn.tsx:41
msgid "Account options"
@@ -125,7 +125,7 @@ msgstr "已取消屏蔽账户"
#: src/view/com/profile/ProfileHeader.tsx:273
msgid "Account unmuted"
msgstr "已取消静音账户"
msgstr "已取消隐藏账户"
#: src/view/com/auth/onboarding/RecommendedFeedsItem.tsx:150
#: src/view/com/modals/ListAddRemoveUsers.tsx:264
@@ -221,7 +221,7 @@ msgstr "详细设置"
#: src/view/com/auth/login/ForgotPasswordForm.tsx:217
#: src/view/com/modals/ChangePassword.tsx:168
msgid "Already have a code?"
msgstr ""
msgstr "已经有确认码了?"
#: src/view/com/auth/login/ChooseAccountForm.tsx:98
msgid "Already signed in as @{0}"
@@ -540,7 +540,7 @@ msgstr "撤销账户删除申请"
#: src/view/com/modals/ChangeHandle.tsx:149
msgid "Cancel change handle"
msgstr "撤销修改昵称"
msgstr "撤销修改用户识别符"
#: src/view/com/modals/crop-image/CropImage.web.tsx:134
msgid "Cancel image crop"
@@ -570,12 +570,12 @@ msgstr "更改"
#: src/view/screens/Settings.tsx:690
msgid "Change handle"
msgstr "更改昵称"
msgstr "更改用户识别符"
#: src/view/com/modals/ChangeHandle.tsx:161
#: src/view/screens/Settings.tsx:699
msgid "Change Handle"
msgstr "更改昵称"
msgstr "更改用户识别符"
#: src/view/com/modals/VerifyEmail.tsx:147
msgid "Change my email"
@@ -583,11 +583,11 @@ msgstr "更改我的邮箱地址"
#: src/view/screens/Settings.tsx:726
msgid "Change password"
msgstr ""
msgstr "更改密码"
#: src/view/screens/Settings.tsx:735
msgid "Change Password"
msgstr ""
msgstr "更改密码"
#: src/view/com/composer/select-language/SuggestedLanguage.tsx:73
msgid "Change post language to {0}"
@@ -595,7 +595,7 @@ msgstr "更改帖子的发布语言至 {0}"
#: src/view/screens/Settings.tsx:727
msgid "Change your Bluesky password"
msgstr ""
msgstr "更改你的 Bluesky 密码"
#: src/view/com/modals/ChangeEmail.tsx:109
msgid "Change Your Email"
@@ -681,7 +681,7 @@ msgstr "气象"
#: src/view/com/modals/ChangePassword.tsx:265
#: src/view/com/modals/ChangePassword.tsx:268
msgid "Close"
msgstr ""
msgstr "关闭"
#: src/components/Dialog/index.web.tsx:78
msgid "Close active dialog"
@@ -969,7 +969,7 @@ msgstr "自定义域名"
#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:106
msgid "Custom feeds built by the community bring you new experiences and help you find the content you love."
msgstr ""
msgstr "由社区构建的自定义信息流能为你带来新的体验,并帮助你找到你喜欢的内容。"
#: src/view/screens/PreferencesExternalEmbeds.tsx:55
msgid "Customize media from external sites."
@@ -982,15 +982,15 @@ msgstr "自定义外部站点的媒体。"
#: src/view/screens/Settings.tsx:479
#: src/view/screens/Settings.tsx:505
msgid "Dark"
msgstr "暗色"
msgstr "深黑"
#: src/view/screens/Debug.tsx:63
msgid "Dark mode"
msgstr "色模式"
msgstr "色模式"
#: src/view/screens/Settings.tsx:492
msgid "Dark Theme"
msgstr ""
msgstr "深色模式"
#: src/view/screens/Debug.tsx:83
msgid "Debug panel"
@@ -1024,7 +1024,7 @@ msgstr "删除我的账户"
#: src/view/screens/Settings.tsx:755
msgid "Delete My Account…"
msgstr ""
msgstr "删除我的账户…"
#: src/view/com/util/forms/PostDropdownBtn.tsx:228
msgid "Delete post"
@@ -1059,7 +1059,7 @@ msgstr "有什么想说的吗?"
#: src/view/screens/Settings.tsx:498
msgid "Dim"
msgstr ""
msgstr "暗淡"
#: src/view/com/composer/Composer.tsx:144
msgid "Discard"
@@ -1137,7 +1137,7 @@ msgstr "拖放即可添加图片"
#: 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 "受 Apple 政策限制,成人内容只能在完成注册后在网页端启用显示"
msgstr "受 Apple 政策限制,成人内容只能在完成注册后在网页端启用显示"
#: src/view/com/modals/EditProfile.tsx:185
msgid "e.g. Alice Roberts"
@@ -1183,7 +1183,7 @@ msgstr "编辑列表详情"
#: src/view/com/modals/CreateOrEditList.tsx:250
msgid "Edit Moderation List"
msgstr "编辑管理员列表"
msgstr "编辑限制列表"
#: src/Navigation.tsx:243
#: src/view/screens/Feeds.tsx:403
@@ -1293,7 +1293,7 @@ msgstr "输入验证码"
#: src/view/com/modals/ChangePassword.tsx:151
msgid "Enter the code you received to change your password."
msgstr ""
msgstr "输入你收到的确认码以更改密码。"
#: src/view/com/modals/ChangeHandle.tsx:371
msgid "Enter the domain you want to use"
@@ -1342,7 +1342,7 @@ msgstr "所有人"
#: src/view/com/modals/ChangeHandle.tsx:150
msgid "Exits handle change process"
msgstr "退出修改昵称流程"
msgstr "退出修改用户识别符流程"
#: src/view/com/lightbox/Lightbox.web.tsx:120
msgid "Exits image view"
@@ -1388,7 +1388,7 @@ msgstr "外部媒体设置"
#: src/view/com/modals/AddAppPasswords.tsx:115
#: src/view/com/modals/AddAppPasswords.tsx:119
msgid "Failed to create app password."
msgstr "无法创建 App 专用密码。"
msgstr "创建 App 专用密码失败。"
#: src/view/com/modals/CreateOrEditList.tsx:206
msgid "Failed to create the list. Check your internet connection and try again."
@@ -1633,7 +1633,7 @@ msgstr "转到下一个"
#: src/view/com/modals/ChangeHandle.tsx:265
msgid "Handle"
msgstr "昵称"
msgstr "用户识别符"
#: src/view/com/auth/create/CreateAccount.tsx:197
msgid "Having trouble?"
@@ -1739,7 +1739,7 @@ msgstr "我们该如何打开此链接?"
#: src/view/com/modals/VerifyEmail.tsx:214
msgid "I have a code"
msgstr "我有邀请码"
msgstr "我有验证码"
#: src/view/com/modals/VerifyEmail.tsx:216
msgid "I have a confirmation code"
@@ -1759,7 +1759,7 @@ msgstr "若不勾选,则默认为全年龄向。"
#: 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 "如果你想要更改密码,我们将向你发送一个确认码以验证这是你的账户。"
#: src/view/com/util/images/Gallery.tsx:38
msgid "Image"
@@ -1828,7 +1828,7 @@ msgstr "输入你的密码"
#: src/view/com/auth/create/Step3.tsx:42
msgid "Input your user handle"
msgstr "输入你的用户昵称"
msgstr "输入你的用户识别符"
#: src/view/com/post-thread/PostThreadItem.tsx:231
msgid "Invalid or unsupported post record"
@@ -1845,7 +1845,7 @@ msgstr "邀请"
#: src/view/com/modals/InviteCodes.tsx:93
#: src/view/screens/Settings.tsx:399
msgid "Invite a Friend"
msgstr "邀请一位朋友"
msgstr "邀请朋友"
#: src/view/com/auth/create/Step1.tsx:92
#: src/view/com/auth/create/Step1.tsx:101
@@ -2033,7 +2033,7 @@ msgstr "列表已删除"
#: src/view/screens/ProfileList.tsx:282
msgid "List muted"
msgstr "列表已静音"
msgstr "列表已隐藏"
#: src/view/com/modals/CreateOrEditList.tsx:275
msgid "List Name"
@@ -2045,7 +2045,7 @@ msgstr "取消屏蔽列表"
#: src/view/screens/ProfileList.tsx:301
msgid "List unmuted"
msgstr "取消静音列表"
msgstr "取消隐藏列表"
#: src/Navigation.tsx:110
#: src/view/screens/Profile.tsx:176
@@ -2092,7 +2092,7 @@ msgstr "登出"
#: src/view/screens/Moderation.tsx:136
msgid "Logged-out visibility"
msgstr "登出可见性"
msgstr "未登录用户可见性"
#: src/view/com/auth/login/ChooseAccountForm.tsx:133
msgid "Login to account that is not listed"
@@ -2130,47 +2130,47 @@ msgstr "来自服务器的信息:{0}"
#: src/view/shell/Drawer.tsx:514
#: src/view/shell/Drawer.tsx:515
msgid "Moderation"
msgstr "管理员"
msgstr "限制"
#: src/view/com/lists/ListCard.tsx:92
#: src/view/com/modals/UserAddRemoveLists.tsx:206
msgid "Moderation list by {0}"
msgstr "管理员列表由 {0} 创建"
msgstr "限制列表由 {0} 创建"
#: src/view/screens/ProfileList.tsx:774
msgid "Moderation list by <0/>"
msgstr "管理员列表由 </0> 创建"
msgstr "限制列表由 </0> 创建"
#: src/view/com/lists/ListCard.tsx:90
#: src/view/com/modals/UserAddRemoveLists.tsx:204
#: src/view/screens/ProfileList.tsx:772
msgid "Moderation list by you"
msgstr "管理员列表由你创建"
msgstr "限制列表由你创建"
#: src/view/com/modals/CreateOrEditList.tsx:197
msgid "Moderation list created"
msgstr "管理员列表已创建"
msgstr "限制列表已创建"
#: src/view/com/modals/CreateOrEditList.tsx:183
msgid "Moderation list updated"
msgstr "管理员列表已更新"
msgstr "限制列表已更新"
#: src/view/screens/Moderation.tsx:95
msgid "Moderation lists"
msgstr "管理员列表"
msgstr "限制列表"
#: src/Navigation.tsx:120
#: src/view/screens/ModerationModlists.tsx:58
msgid "Moderation Lists"
msgstr "管理员列表"
msgstr "限制列表"
#: src/view/screens/Settings.tsx:613
msgid "Moderation settings"
msgstr "管理员设置"
msgstr "限制设置"
#: src/view/com/modals/ModerationDetails.tsx:35
msgid "Moderator has chosen to set a general warning on the content."
msgstr "管理员选择对内容设置一般警告。"
msgstr "限制选择对内容设置一般警告。"
#: src/view/shell/desktop/Feeds.tsx:53
msgid "More feeds"
@@ -2192,48 +2192,48 @@ msgstr "最多点赞优先"
#: src/view/com/profile/ProfileHeader.tsx:374
msgid "Mute Account"
msgstr "静音账户"
msgstr "隐藏账户"
#: src/view/screens/ProfileList.tsx:543
msgid "Mute accounts"
msgstr "静音账户"
msgstr "隐藏账户"
#: src/view/screens/ProfileList.tsx:490
msgid "Mute list"
msgstr "静音列表"
msgstr "隐藏列表"
#: src/view/screens/ProfileList.tsx:274
msgid "Mute these accounts?"
msgstr "静音这些账户?"
msgstr "隐藏这些账户?"
#: src/view/screens/ProfileList.tsx:278
msgid "Mute this List"
msgstr "静音这个列表"
msgstr "隐藏这个列表"
#: src/view/com/util/forms/PostDropdownBtn.tsx:171
msgid "Mute thread"
msgstr "静音讨论串"
msgstr "隐藏讨论串"
#: src/view/com/lists/ListCard.tsx:101
msgid "Muted"
msgstr "已经印"
msgstr "已隐藏"
#: src/view/screens/Moderation.tsx:109
msgid "Muted accounts"
msgstr "已静音账户"
msgstr "已隐藏账户"
#: src/Navigation.tsx:125
#: src/view/screens/ModerationMutedAccounts.tsx:107
msgid "Muted Accounts"
msgstr "已静音账户"
msgstr "已隐藏账户"
#: src/view/screens/ModerationMutedAccounts.tsx:115
msgid "Muted accounts have their posts removed from your feed and from your notifications. Mutes are completely private."
msgstr "已静音的账户将不会在你的通知或时间线中显示,被静音账户将不会收到通知。"
msgstr "已隐藏的账户将不会在你的通知或时间线中显示,被隐藏账户将不会收到通知。"
#: src/view/screens/ProfileList.tsx:276
msgid "Muting is private. Muted accounts can interact with you, but you will not see their posts or receive notifications from them."
msgstr "被静音的账户将不会得知你已将他静音,已静音的账户将不会在你的通知或时间线中显示。"
msgstr "被隐藏的账户将不会得知你已将他隐藏,已隐藏的账户将不会在你的通知或时间线中显示。"
#: src/view/com/modals/BirthDateSettings.tsx:56
msgid "My Birthday"
@@ -2293,15 +2293,15 @@ msgstr "永远不会失去对你的关注者或数据的访问。"
#: src/view/screens/Lists.tsx:76
msgctxt "action"
msgid "New"
msgstr "新"
msgstr "新"
#: src/view/screens/ModerationModlists.tsx:78
msgid "New"
msgstr "新"
msgstr "新"
#: src/view/com/modals/CreateOrEditList.tsx:252
msgid "New Moderation List"
msgstr "新的管理员列表"
msgstr "新的限制列表"
#: src/view/com/auth/login/SetNewPasswordForm.tsx:150
msgid "New password"
@@ -2309,7 +2309,7 @@ msgstr "新密码"
#: src/view/com/modals/ChangePassword.tsx:215
msgid "New Password"
msgstr ""
msgstr "新密码"
#: src/view/com/feeds/FeedPage.tsx:201
msgctxt "action"
@@ -2370,7 +2370,7 @@ msgstr "下一张图片"
#: src/view/screens/PreferencesThreads.tsx:106
#: src/view/screens/PreferencesThreads.tsx:129
msgid "No"
msgstr "没有"
msgstr "停用"
#: src/view/screens/ProfileFeed.tsx:584
#: src/view/screens/ProfileList.tsx:754
@@ -2398,7 +2398,7 @@ msgstr "未找到\"{query}\"的结果"
#: src/view/screens/Search/Search.tsx:280
#: src/view/screens/Search/Search.tsx:308
msgid "No results found for {query}"
msgstr "未找到{query}的结果"
msgstr "未找到 {query} 的结果"
#: src/view/com/modals/EmbedConsent.tsx:129
msgid "No thanks"
@@ -2480,7 +2480,7 @@ msgstr "打开"
#: src/view/com/composer/Composer.tsx:470
#: src/view/com/composer/Composer.tsx:471
msgid "Open emoji picker"
msgstr "打开emoji选择器"
msgstr "打开 emoji 选择器"
#: src/view/screens/Settings.tsx:706
msgid "Open links with in-app browser"
@@ -2558,7 +2558,7 @@ msgstr "打开使用自定义域名的模式"
#: src/view/screens/Settings.tsx:614
msgid "Opens moderation settings"
msgstr "打开管理员设置"
msgstr "打开限制设置"
#: src/view/com/auth/login/LoginForm.tsx:236
msgid "Opens password reset form"
@@ -2688,11 +2688,11 @@ msgstr "播放 GIF"
#: src/view/com/auth/create/state.ts:177
msgid "Please choose your handle."
msgstr "请选择你的昵称。"
msgstr "请设置你的用户识别符。"
#: src/view/com/auth/create/state.ts:160
msgid "Please choose your password."
msgstr "请选择你的密码。"
msgstr "请设置你的密码。"
#: 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."
@@ -2850,7 +2850,7 @@ msgstr "公开内容"
#: src/view/screens/ModerationModlists.tsx:61
msgid "Public, shareable lists of users to mute or block in bulk."
msgstr "公开且可共享的批量静音或屏蔽列表。"
msgstr "公开且可共享的批量隐藏或屏蔽列表。"
#: src/view/screens/Lists.tsx:61
msgid "Public, shareable lists which can drive feeds."
@@ -3050,7 +3050,7 @@ msgstr "请求码"
#: src/view/com/modals/ChangePassword.tsx:239
#: src/view/com/modals/ChangePassword.tsx:241
msgid "Request Code"
msgstr ""
msgstr "确认码"
#: src/view/screens/Settings.tsx:450
msgid "Require alt text before posting"
@@ -3067,7 +3067,7 @@ msgstr "重置码"
#: src/view/com/modals/ChangePassword.tsx:190
msgid "Reset Code"
msgstr ""
msgstr "重置代码"
#: src/view/screens/Settings.tsx:806
msgid "Reset onboarding"
@@ -3155,7 +3155,7 @@ msgstr "保存更改"
#: src/view/com/modals/ChangeHandle.tsx:170
msgid "Save handle change"
msgstr "保存新的昵称"
msgstr "保存新的用户识别符"
#: src/view/com/modals/crop-image/CropImage.web.tsx:144
msgid "Save image crop"
@@ -3171,7 +3171,7 @@ msgstr "保存个人资料中所做的变更"
#: src/view/com/modals/ChangeHandle.tsx:171
msgid "Saves handle change to {handle}"
msgstr "保存昵称更改至 {handle}"
msgstr "保存用户识别符更改至 {handle}"
#: src/screens/Onboarding/index.tsx:36
msgid "Science"
@@ -3321,7 +3321,7 @@ msgstr "设置年龄"
#: src/view/screens/Settings.tsx:482
msgid "Set color theme to dark"
msgstr "设置主题为色模式"
msgstr "设置主题为色模式"
#: src/view/screens/Settings.tsx:475
msgid "Set color theme to light"
@@ -3333,11 +3333,11 @@ msgstr "设置主题跟随系统设置"
#: src/view/screens/Settings.tsx:508
msgid "Set dark theme to the dark theme"
msgstr ""
msgstr "设置深色模式至深黑"
#: src/view/screens/Settings.tsx:501
msgid "Set dark theme to the dim theme"
msgstr ""
msgstr "设置深色模式至暗淡"
#: src/view/com/auth/login/SetNewPasswordForm.tsx:104
msgid "Set new password"
@@ -3349,23 +3349,23 @@ msgstr "设置密码"
#: src/view/screens/PreferencesHomeFeed.tsx:225
msgid "Set this setting to \"No\" to hide all quote posts from your feed. Reposts will still be visible."
msgstr "此设置项设为\"否\"以隐藏来自订阅信息流的所有引用帖子,转发仍将可见。"
msgstr "停用此设置项以隐藏来自订阅信息流的所有引用帖子,转发仍将可见。"
#: src/view/screens/PreferencesHomeFeed.tsx:122
msgid "Set this setting to \"No\" to hide all replies from your feed."
msgstr "此设置项设为\"否\"以隐藏来自订阅信息流的所有回复。"
msgstr "停用此设置项以隐藏来自订阅信息流的所有回复。"
#: src/view/screens/PreferencesHomeFeed.tsx:191
msgid "Set this setting to \"No\" to hide all reposts from your feed."
msgstr "此设置项设为\"否\"以隐藏来自订阅信息流的所有转发。"
msgstr "停用此设置项以隐藏来自订阅信息流的所有转发。"
#: src/view/screens/PreferencesThreads.tsx:122
msgid "Set this setting to \"Yes\" to show replies in a threaded view. This is an experimental feature."
msgstr "此设置项设为\"是\"以在分层视图中显示回复。这是一个实验性功能。"
msgstr "启用此设置项以在分层视图中显示回复。这是一个实验性功能。"
#: 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 "此设置项设为\"是\"以在关注信息流中显示已保存信息流的样例。这是一个实验性功能。"
msgstr "启用此设置项以在关注信息流中显示已保存信息流的样例。这是一个实验性功能。"
#: src/screens/Onboarding/Layout.tsx:50
msgid "Set up your account"
@@ -3846,7 +3846,7 @@ msgstr "出现问题了,请检查你的互联网连接并重试。"
#: src/view/com/util/ErrorBoundary.tsx:36
msgid "There was an unexpected issue in the application. Please let us know if this happened to you!"
msgstr "\"应用发生意外错误,请联系我们进行错误反馈!"
msgstr "应用发生意外错误,请联系我们进行错误反馈!"
#: src/screens/Deactivated.tsx:107
msgid "There's been a rush of new users to Bluesky! We'll activate your account as soon as we can."
@@ -3862,7 +3862,7 @@ msgstr "这里是一些受欢迎的账号,你可能会喜欢:"
#: src/view/com/util/moderation/ScreenHider.tsx:88
msgid "This {screenDescription} has been flagged:"
msgstr "{screenDescription}\" 已被标记:"
msgstr "这个 {screenDescription} 已被标记:"
#: src/view/com/util/moderation/ScreenHider.tsx:83
msgid "This account has requested that users sign in to view their profile."
@@ -3928,11 +3928,11 @@ msgstr "此用户包含在你已屏蔽的 <0/> 列表中。"
#: src/view/com/modals/ModerationDetails.tsx:74
msgid "This user is included in the <0/> list which you have muted."
msgstr ""
msgstr "此用户包含在你已隐藏的 <0/> 列表中。"
#: src/view/com/modals/ModerationDetails.tsx:74
#~ msgid "This user is included the <0/> list which you have muted."
#~ msgstr "此用户包含在你已静音的 <0/> 列表中。"
#~ msgstr "此用户包含在你已隐藏的 <0/> 列表中。"
#: src/view/com/modals/SelfLabel.tsx:137
msgid "This warning is only available for posts with media attached."
@@ -3980,7 +3980,7 @@ msgstr "取消屏蔽列表"
#: src/view/screens/ProfileList.tsx:490
msgid "Un-mute list"
msgstr "取消静音列表"
msgstr "取消隐藏列表"
#: src/view/com/auth/create/CreateAccount.tsx:66
#: src/view/com/auth/login/ForgotPasswordForm.tsx:87
@@ -4031,15 +4031,15 @@ msgstr "取消喜欢"
#: src/view/screens/ProfileList.tsx:596
msgid "Unmute"
msgstr "取消静音"
msgstr "取消隐藏"
#: src/view/com/profile/ProfileHeader.tsx:373
msgid "Unmute Account"
msgstr "取消静音账户"
msgstr "取消隐藏账户"
#: src/view/com/util/forms/PostDropdownBtn.tsx:171
msgid "Unmute thread"
msgstr "取消静音讨论串"
msgstr "取消隐藏讨论串"
#: src/view/screens/ProfileFeed.tsx:353
#: src/view/screens/ProfileList.tsx:580
@@ -4048,7 +4048,7 @@ msgstr "取消固定"
#: src/view/screens/ProfileList.tsx:473
msgid "Unpin moderation list"
msgstr "取消固定管理员列表"
msgstr "取消固定限制列表"
#: src/view/screens/ProfileFeed.tsx:345
msgid "Unsave"
@@ -4090,11 +4090,11 @@ msgstr "使用系统默认浏览器"
#: src/view/com/modals/AddAppPasswords.tsx:155
msgid "Use this to sign into the other app along with your handle."
msgstr "使用这个和你的昵称一起登录其他应用。"
msgstr "使用这个和你的用户识别符一起登录其他应用。"
#: src/view/com/modals/ServerInput.tsx:105
msgid "Use your domain as your Bluesky client service provider"
msgstr "使用你的域名作为 Bluesky 客户服务提供"
msgstr "使用你的域名作为 Bluesky 客户端的服务提供"
#: src/view/com/modals/InviteCodes.tsx:200
msgid "Used by:"
@@ -4114,7 +4114,7 @@ msgstr "用户屏蔽了你"
#: src/view/com/auth/create/Step3.tsx:41
msgid "User handle"
msgstr "用户昵称"
msgstr "用户识别符"
#: src/view/com/lists/ListCard.tsx:84
#: src/view/com/modals/UserAddRemoveLists.tsx:198
@@ -4220,7 +4220,7 @@ msgstr "警告"
#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:124
msgid "We also think you'll like \"For You\" by Skygaze:"
msgstr "我们认为还你会喜欢 Skygaze 所维护的 \"For You\""
msgstr "我们认为还你会喜欢 Skygaze 所维护的 \"For You\":"
#: src/screens/Deactivated.tsx:134
msgid "We estimate {estimatedTime} until your account is ready."
@@ -4236,7 +4236,7 @@ msgstr "我们已经看完了你关注的帖子。这是来自 <0/> 的最新消
#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:119
msgid "We recommend our \"Discover\" feed:"
msgstr "我们推荐我们的 \"Discover\" 信息流"
msgstr "我们推荐我们的 \"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."
@@ -4329,7 +4329,7 @@ msgstr "XXXXXX"
#: src/view/screens/PreferencesThreads.tsx:106
#: src/view/screens/PreferencesThreads.tsx:129
msgid "Yes"
msgstr "是的"
msgstr "启用"
#: src/screens/Deactivated.tsx:131
msgid "You are in line."
@@ -4378,11 +4378,11 @@ msgstr "你已屏蔽了此用户,你将无法查看他们发布的内容。"
#: 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 "你输入的邀请码无效。它应该长得像这样 XXXXX-XXXXX。"
#: src/view/com/modals/ModerationDetails.tsx:87
msgid "You have muted this user."
msgstr "你已静音这个用户。"
msgstr "你已隐藏这个用户。"
#: src/view/com/feeds/ProfileFeedgens.tsx:136
msgid "You have no feeds."
@@ -4403,7 +4403,7 @@ msgstr "你尚未创建任何 App 专用密码,可以通过点击下面的按
#: src/view/screens/ModerationMutedAccounts.tsx:131
msgid "You have not muted any accounts yet. To mute an account, go to their profile and selected \"Mute account\" from the menu on their account."
msgstr "你还没有静音任何账号。要静音账号,请转到其个人资料并在其账号上的菜单中选择 \"静音账号\"。"
msgstr "你还没有隐藏任何账号。要隐藏账号,请转到其个人资料并在其账号上的菜单中选择 \"隐藏账号\"。"
#: src/view/com/modals/ContentFilteringSettings.tsx:170
msgid "You must be 18 or older to enable adult content."
@@ -4487,11 +4487,11 @@ msgstr "你的关注信息流为空!关注更多用户去看看他们发了什
#: src/view/com/auth/create/Step3.tsx:45
msgid "Your full handle will be"
msgstr "你的完整昵称将修改为"
msgstr "你的完整用户识别符将修改为"
#: src/view/com/modals/ChangeHandle.tsx:270
msgid "Your full handle will be <0>@{0}</0>"
msgstr "你的完整昵称将修改为 <0>@{0}</0>"
msgstr "你的完整用户识别符将修改为 <0>@{0}</0>"
#: src/view/screens/Settings.tsx:430
#: src/view/shell/desktop/RightNav.tsx:137
@@ -4501,7 +4501,7 @@ msgstr "在使用 App 专用密码登录时,你的邀请码将被隐藏"
#: src/view/com/modals/ChangePassword.tsx:155
msgid "Your password has been changed successfully!"
msgstr ""
msgstr "你的密码已更改成功!"
#: src/view/com/composer/Composer.tsx:267
msgid "Your post has been published"
@@ -4511,7 +4511,7 @@ msgstr "你的帖子已发送"
#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:59
#: src/view/com/auth/onboarding/WelcomeMobile.tsx:59
msgid "Your posts, likes, and blocks are public. Mutes are private."
msgstr "你的帖子、点赞和屏蔽是公开可见的,而静音不可见。"
msgstr "你的帖子、点赞和屏蔽是公开可见的,而隐藏不可见。"
#: src/view/com/modals/SwitchAccount.tsx:84
#: src/view/screens/Settings.tsx:125
@@ -4524,4 +4524,4 @@ msgstr "你的回复已发送"
#: src/view/com/auth/create/Step3.tsx:28
msgid "Your user handle"
msgstr "你的用户昵称"
msgstr "你的用户识别符"
+4 -5
View File
@@ -10,9 +10,8 @@ import {pluralize} from '#/lib/strings/helpers'
import {atoms as a, useTheme, useBreakpoints} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {Text} from '#/components/Typography'
import {Text, P} from '#/components/Typography'
import {isWeb} from '#/platform/detection'
import {H2, P} from '#/components/Typography'
import {ScrollView} from '#/view/com/util/Views'
import {Loader} from '#/components/Loader'
import {Logo} from '#/view/icons/Logo'
@@ -100,10 +99,10 @@ export function Deactivated() {
<Logo width={120} />
</View>
<H2 style={[a.pb_sm]}>
<Text style={[a.text_4xl, a.font_bold, a.pb_sm]}>
<Trans>You're in line</Trans>
</H2>
<P style={[t.atoms.text_contrast_700]}>
</Text>
<P style={[t.atoms.text_contrast_medium]}>
<Trans>
There's been a rush of new users to Bluesky! We'll activate your
account as soon as we can.
+4 -2
View File
@@ -165,7 +165,7 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
isWeb ? a.fixed : a.absolute,
{bottom: 0, left: 0, right: 0},
t.atoms.bg,
t.atoms.border,
t.atoms.border_contrast_low,
a.border_t,
a.align_center,
gtMobile ? a.px_5xl : a.px_xl,
@@ -227,5 +227,7 @@ export function Description({
style,
}: React.PropsWithChildren<TextStyleProp>) {
const t = useTheme()
return <P style={[t.atoms.text_contrast_700, flatten(style)]}>{children}</P>
return (
<P style={[t.atoms.text_contrast_medium, flatten(style)]}>{children}</P>
)
}
@@ -8,7 +8,7 @@ import {msg} from '@lingui/macro'
import {useTheme, atoms as a} from '#/alf'
import * as Toggle from '#/components/forms/Toggle'
import {useFeedSourceInfoQuery, FeedSourceInfo} from '#/state/queries/feed'
import {Text, H3} from '#/components/Typography'
import {Text} from '#/components/Typography'
import {RichText} from '#/components/RichText'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
@@ -94,14 +94,14 @@ function PrimaryFeedCardInner({
</View>
<View style={[a.pt_xs, a.flex_grow]}>
<H3
<Text
style={[
a.text_lg,
a.font_bold,
ctx.selected && styles.textSelected,
]}>
{feed.displayName}
</H3>
</Text>
<Text
style={[
@@ -137,7 +137,7 @@ function PrimaryFeedCardInner({
},
a.mt_md,
a.w_full,
t.name === 'light' ? t.atoms.border : t.atoms.border_contrast,
t.atoms.border_contrast_low,
ctx.selected && {
borderTopColor: t.palette.white,
},
@@ -239,14 +239,14 @@ function FeedCardInner({feed}: {feed: FeedSourceInfo; config: FeedConfig}) {
</View>
<View style={[a.pt_2xs, a.flex_grow]}>
<H3
<Text
style={[
a.text_md,
a.font_bold,
ctx.selected && styles.textSelected,
]}>
{feed.displayName}
</H3>
</Text>
<Text
style={[
{opacity: 0.8},
@@ -281,7 +281,7 @@ function FeedCardInner({feed}: {feed: FeedSourceInfo; config: FeedConfig}) {
},
a.mt_md,
a.w_full,
t.name === 'light' ? t.atoms.border : t.atoms.border_contrast,
t.atoms.border_contrast_low,
ctx.selected && {
borderTopColor: t.palette.primary_200,
},
@@ -360,7 +360,7 @@ export function FeedCardPlaceholder({primary}: {primary?: boolean}) {
},
a.mt_md,
a.w_full,
t.atoms.border,
t.atoms.border_contrast_low,
]}
/>
+18 -3
View File
@@ -115,12 +115,22 @@ export function StepAlgoFeeds() {
onChange={setPrimaryFeedUris}
label={_(msg`Select your primary algorithmic feeds`)}>
<Text
style={[a.text_md, a.pt_4xl, a.pb_md, t.atoms.text_contrast_700]}>
style={[
a.text_md,
a.pt_4xl,
a.pb_md,
t.atoms.text_contrast_medium,
]}>
<Trans>We recommend our "Discover" feed:</Trans>
</Text>
<FeedCard config={PRIMARY_FEEDS[0]} />
<Text
style={[a.text_md, a.pt_4xl, a.pb_lg, t.atoms.text_contrast_700]}>
style={[
a.text_md,
a.pt_4xl,
a.pb_lg,
t.atoms.text_contrast_medium,
]}>
<Trans>We also think you'll like "For You" by Skygaze:</Trans>
</Text>
<FeedCard config={PRIMARY_FEEDS[1]} />
@@ -131,7 +141,12 @@ export function StepAlgoFeeds() {
onChange={setSeconaryFeedUris}
label={_(msg`Select your secondary algorithmic feeds`)}>
<Text
style={[a.text_md, a.pt_4xl, a.pb_lg, t.atoms.text_contrast_700]}>
style={[
a.text_md,
a.pt_4xl,
a.pb_lg,
t.atoms.text_contrast_medium,
]}>
<Trans>There are many feeds to try:</Trans>
</Text>
<View style={[a.gap_md]}>
+3 -3
View File
@@ -101,7 +101,7 @@ export function StepFinished() {
<Trans>Public</Trans>
</Text>
<Text
style={[t.atoms.text_contrast_500, a.text_md, a.leading_snug]}>
style={[t.atoms.text_contrast_medium, a.text_md, a.leading_snug]}>
<Trans>
Your posts, likes, and blocks are public. Mutes are private.
</Trans>
@@ -115,7 +115,7 @@ export function StepFinished() {
<Trans>Open</Trans>
</Text>
<Text
style={[t.atoms.text_contrast_500, a.text_md, a.leading_snug]}>
style={[t.atoms.text_contrast_medium, a.text_md, a.leading_snug]}>
<Trans>Never lose access to your followers or data.</Trans>
</Text>
</View>
@@ -131,7 +131,7 @@ export function StepFinished() {
<Trans>Flexible</Trans>
</Text>
<Text
style={[t.atoms.text_contrast_500, a.text_md, a.leading_snug]}>
style={[t.atoms.text_contrast_medium, a.text_md, a.leading_snug]}>
<Trans>Choose the algorithms that power your custom feeds.</Trans>
</Text>
</View>
@@ -96,7 +96,7 @@ export function AdultContentEnabledPref({
<Text
style={[
a.flex_1,
t.atoms.text_contrast_700,
t.atoms.text_contrast_medium,
a.leading_snug,
{paddingTop: 1},
]}>
@@ -57,7 +57,7 @@ export function ModerationOption({
entering={isMounted.current ? FadeIn : undefined}>
<View style={[a.gap_xs, {width: '50%'}]}>
<Text style={[a.font_bold]}>{groupInfo.title}</Text>
<Text style={[t.atoms.text_contrast_700, a.leading_snug]}>
<Text style={[t.atoms.text_contrast_medium, a.leading_snug]}>
{groupInfo.subtitle}
</Text>
</View>
@@ -95,7 +95,7 @@ export function SuggestedAccountCard({
<Text style={[a.font_bold, a.text_md, a.pb_xs]} numberOfLines={1}>
{profile.displayName}
</Text>
<Text style={[t.atoms.text_contrast_600]}>{profile.handle}</Text>
<Text style={[t.atoms.text_contrast_medium]}>{profile.handle}</Text>
</View>
</View>
@@ -124,7 +124,7 @@ export function SuggestedAccountCard({
borderTopWidth: 1,
},
a.w_full,
t.name === 'light' ? t.atoms.border : t.atoms.border_contrast,
t.atoms.border_contrast_low,
ctx.selected && {
borderTopColor: t.palette.primary_200,
},
@@ -175,7 +175,11 @@ export function StepSuggestedAccounts() {
)}
onPress={handleContinue}>
<ButtonText>
<Trans>Follow All</Trans>
{dids.length === 20 ? (
<Trans>Follow All</Trans>
) : (
<Trans>Follow</Trans>
)}
</ButtonText>
<ButtonIcon icon={saving ? Loader : Plus} position="right" />
</Button>
+13 -11
View File
@@ -22,15 +22,15 @@ export interface ProfileShadow {
blockingUri: string | undefined
}
type ProfileView =
| AppBskyActorDefs.ProfileView
| AppBskyActorDefs.ProfileViewBasic
| AppBskyActorDefs.ProfileViewDetailed
const shadows: WeakMap<ProfileView, Partial<ProfileShadow>> = new WeakMap()
const shadows: WeakMap<
AppBskyActorDefs.ProfileView,
Partial<ProfileShadow>
> = new WeakMap()
const emitter = new EventEmitter()
export function useProfileShadow(profile: ProfileView): Shadow<ProfileView> {
export function useProfileShadow<
TProfileView extends AppBskyActorDefs.ProfileView,
>(profile: TProfileView): Shadow<TProfileView> {
const [shadow, setShadow] = useState(() => shadows.get(profile))
const [prevPost, setPrevPost] = useState(profile)
if (profile !== prevPost) {
@@ -70,10 +70,10 @@ export function updateProfileShadow(
})
}
function mergeShadow(
profile: ProfileView,
function mergeShadow<TProfileView extends AppBskyActorDefs.ProfileView>(
profile: TProfileView,
shadow: Partial<ProfileShadow>,
): Shadow<ProfileView> {
): Shadow<TProfileView> {
return castAsShadow({
...profile,
viewer: {
@@ -89,7 +89,9 @@ function mergeShadow(
})
}
function* findProfilesInCache(did: string): Generator<ProfileView, void> {
function* findProfilesInCache(
did: string,
): Generator<AppBskyActorDefs.ProfileView, void> {
yield* findAllProfilesInListMembersQueryData(queryClient, did)
yield* findAllProfilesInMyBlockedAccountsQueryData(queryClient, did)
yield* findAllProfilesInMyMutedAccountsQueryData(queryClient, did)
-13
View File
@@ -26,17 +26,6 @@ export interface EditProfileModal {
onUpdate?: () => void
}
export interface ProfilePreviewModal {
name: 'profile-preview'
did: string
}
export interface ServerInputModal {
name: 'server-input'
initialService: string
onSelect: (url: string) => void
}
export interface ModerationDetailsModal {
name: 'moderation-details'
context: 'account' | 'content'
@@ -202,7 +191,6 @@ export type Modal =
| ChangeHandleModal
| DeleteAccountModal
| EditProfileModal
| ProfilePreviewModal
| BirthDateSettingsModal
| VerifyEmailModal
| ChangeEmailModal
@@ -228,7 +216,6 @@ export type Modal =
| AltTextImageModal
| CropImageModal
| EditImageModal
| ServerInputModal
| RepostModal
| SelfLabelModal
| ThreadgateModal
+1
View File
@@ -112,6 +112,7 @@ export function transform(legacy: Partial<LegacySchema>): Schema {
hiddenPosts: defaults.hiddenPosts,
externalEmbeds: defaults.externalEmbeds,
lastSelectedHomeFeed: defaults.lastSelectedHomeFeed,
pdsAddressHistory: defaults.pdsAddressHistory,
}
}
+2
View File
@@ -57,6 +57,7 @@ export const schema = z.object({
hiddenPosts: z.array(z.string()).optional(), // should move to server
useInAppBrowser: z.boolean().optional(),
lastSelectedHomeFeed: z.string().optional(),
pdsAddressHistory: z.array(z.string()).optional(),
})
export type Schema = z.infer<typeof schema>
@@ -91,4 +92,5 @@ export const defaults: Schema = {
hiddenPosts: [],
useInAppBrowser: undefined,
lastSelectedHomeFeed: undefined,
pdsAddressHistory: [],
}
+2 -2
View File
@@ -12,7 +12,7 @@ import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
import chunk from 'lodash.chunk'
import {QueryClient} from '@tanstack/react-query'
import {getAgent} from '../../session'
import {precacheProfile as precacheResolvedUri} from '../resolve-uri'
import {precacheProfile} from '../profile'
import {NotificationType, FeedNotification, FeedPage} from './types'
const GROUPABLE_REASONS = ['like', 'repost', 'follow']
@@ -59,7 +59,7 @@ export async function fetchPage({
if (notif.subjectUri) {
notif.subject = subjects.get(notif.subjectUri)
if (notif.subject) {
precacheResolvedUri(queryClient, notif.subject.author) // precache the handle->did resolution
precacheProfile(queryClient, notif.subject.author)
}
}
}
+2 -2
View File
@@ -21,7 +21,7 @@ import {MergeFeedAPI} from 'lib/api/feed/merge'
import {HomeFeedAPI} from '#/lib/api/feed/home'
import {logger} from '#/logger'
import {STALE} from '#/state/queries'
import {precacheFeedPosts as precacheResolvedUris} from './resolve-uri'
import {precacheFeedPostProfiles} from './profile'
import {getAgent} from '#/state/session'
import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences/const'
import {getModerationOpts} from '#/state/queries/preferences/moderation'
@@ -138,7 +138,7 @@ export function usePostFeedQuery(
}
const res = await api.fetch({cursor, limit: PAGE_SIZE})
precacheResolvedUris(queryClient, res.feed) // precache the handle->did resolution
precacheFeedPostProfiles(queryClient, res.feed)
/*
* If this is a public view, we need to check if posts fail moderation.
+2 -2
View File
@@ -10,7 +10,7 @@ import {getAgent} from '#/state/session'
import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
import {findPostInQueryData as findPostInFeedQueryData} from './post-feed'
import {findPostInQueryData as findPostInNotifsQueryData} from './notifications/feed'
import {precacheThreadPosts as precacheResolvedUris} from './resolve-uri'
import {precacheThreadPostProfiles} from './profile'
import {getEmbeddedPost} from './util'
export const RQKEY = (uri: string) => ['post-thread', uri]
@@ -71,7 +71,7 @@ export function usePostThreadQuery(uri: string | undefined) {
const res = await getAgent().getPostThread({uri: uri!})
if (res.success) {
const nodes = responseToThreadNodes(res.data.thread)
precacheResolvedUris(queryClient, nodes) // precache the handle->did resolution
precacheThreadPostProfiles(queryClient, nodes)
return nodes
}
return {type: 'unknown', uri: uri!}
+77 -3
View File
@@ -4,6 +4,9 @@ import {
AppBskyActorDefs,
AppBskyActorProfile,
AppBskyActorGetProfile,
AppBskyFeedDefs,
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
} from '@atproto/api'
import {
useQuery,
@@ -23,9 +26,14 @@ import {RQKEY as RQKEY_MY_MUTED} from './my-muted-accounts'
import {RQKEY as RQKEY_MY_BLOCKED} from './my-blocked-accounts'
import {STALE} from '#/state/queries'
import {track} from '#/lib/analytics/analytics'
import {ThreadNode} from './post-thread'
export const RQKEY = (did: string) => ['profile', did]
export const profilesQueryKey = (handles: string[]) => ['profiles', handles]
export const profileBasicQueryKey = (didOrHandle: string) => [
'profileBasic',
didOrHandle,
]
export function useProfileQuery({
did,
@@ -34,18 +42,26 @@ export function useProfileQuery({
did: string | undefined
staleTime?: number
}) {
return useQuery({
const queryClient = useQueryClient()
return useQuery<AppBskyActorDefs.ProfileViewDetailed>({
// WARNING
// this staleTime is load-bearing
// if you remove it, the UI infinite-loops
// -prf
staleTime,
refetchOnWindowFocus: true,
queryKey: RQKEY(did || ''),
queryKey: RQKEY(did ?? ''),
queryFn: async () => {
const res = await getAgent().getProfile({actor: did || ''})
const res = await getAgent().getProfile({actor: did ?? ''})
return res.data
},
placeholderData: () => {
if (!did) return
return queryClient.getQueryData<AppBskyActorDefs.ProfileViewBasic>(
profileBasicQueryKey(did),
)
},
enabled: !!did,
})
}
@@ -405,6 +421,64 @@ function useProfileUnblockMutation() {
})
}
export function precacheProfile(
queryClient: QueryClient,
profile: AppBskyActorDefs.ProfileViewBasic,
) {
queryClient.setQueryData(profileBasicQueryKey(profile.handle), profile)
queryClient.setQueryData(profileBasicQueryKey(profile.did), profile)
}
export function precacheFeedPostProfiles(
queryClient: QueryClient,
posts: AppBskyFeedDefs.FeedViewPost[],
) {
for (const post of posts) {
// Save the author of the post every time
precacheProfile(queryClient, post.post.author)
precachePostEmbedProfile(queryClient, post.post.embed)
// Cache parent author and embeds
const parent = post.reply?.parent
if (AppBskyFeedDefs.isPostView(parent)) {
precacheProfile(queryClient, parent.author)
precachePostEmbedProfile(queryClient, parent.embed)
}
}
}
function precachePostEmbedProfile(
queryClient: QueryClient,
embed: AppBskyFeedDefs.PostView['embed'],
) {
if (AppBskyEmbedRecord.isView(embed)) {
if (AppBskyEmbedRecord.isViewRecord(embed.record)) {
precacheProfile(queryClient, embed.record.author)
}
} else if (AppBskyEmbedRecordWithMedia.isView(embed)) {
if (AppBskyEmbedRecord.isViewRecord(embed.record.record)) {
precacheProfile(queryClient, embed.record.record.author)
}
}
}
export function precacheThreadPostProfiles(
queryClient: QueryClient,
node: ThreadNode,
) {
if (node.type === 'post') {
precacheProfile(queryClient, node.post.author)
if (node.parent) {
precacheThreadPostProfiles(queryClient, node.parent)
}
if (node.replies?.length) {
for (const reply of node.replies) {
precacheThreadPostProfiles(queryClient, reply)
}
}
}
}
async function whenAppViewReady(
actor: string,
fn: (res: AppBskyActorGetProfile.Response) => boolean,
+23 -49
View File
@@ -1,9 +1,9 @@
import {QueryClient, useQuery, UseQueryResult} from '@tanstack/react-query'
import {AtUri, AppBskyActorDefs, AppBskyFeedDefs} from '@atproto/api'
import {useQuery, useQueryClient, UseQueryResult} from '@tanstack/react-query'
import {AtUri, AppBskyActorDefs} from '@atproto/api'
import {profileBasicQueryKey as RQKEY_PROFILE_BASIC} from './profile'
import {getAgent} from '#/state/session'
import {STALE} from '#/state/queries'
import {ThreadNode} from './post-thread'
export const RQKEY = (didOrHandle: string) => ['resolved-did', didOrHandle]
@@ -22,55 +22,29 @@ export function useResolveUriQuery(uri: string | undefined): UriUseQueryResult {
}
export function useResolveDidQuery(didOrHandle: string | undefined) {
const queryClient = useQueryClient()
return useQuery<string, Error>({
staleTime: STALE.HOURS.ONE,
queryKey: RQKEY(didOrHandle || ''),
async queryFn() {
if (!didOrHandle) {
return ''
}
if (!didOrHandle.startsWith('did:')) {
const res = await getAgent().resolveHandle({handle: didOrHandle})
didOrHandle = res.data.did
}
return didOrHandle
queryKey: RQKEY(didOrHandle ?? ''),
queryFn: async () => {
if (!didOrHandle) return ''
// Just return the did if it's already one
if (didOrHandle.startsWith('did:')) return didOrHandle
const res = await getAgent().resolveHandle({handle: didOrHandle})
return res.data.did
},
initialData: () => {
// Return undefined if no did or handle
if (!didOrHandle) return
const profile =
queryClient.getQueryData<AppBskyActorDefs.ProfileViewBasic>(
RQKEY_PROFILE_BASIC(didOrHandle),
)
return profile?.did
},
enabled: !!didOrHandle,
})
}
export function precacheProfile(
queryClient: QueryClient,
profile:
| AppBskyActorDefs.ProfileView
| AppBskyActorDefs.ProfileViewBasic
| AppBskyActorDefs.ProfileViewDetailed,
) {
queryClient.setQueryData(RQKEY(profile.handle), profile.did)
}
export function precacheFeedPosts(
queryClient: QueryClient,
posts: AppBskyFeedDefs.FeedViewPost[],
) {
for (const post of posts) {
precacheProfile(queryClient, post.post.author)
}
}
export function precacheThreadPosts(
queryClient: QueryClient,
node: ThreadNode,
) {
if (node.type === 'post') {
precacheProfile(queryClient, node.post.author)
if (node.parent) {
precacheThreadPosts(queryClient, node.parent)
}
if (node.replies?.length) {
for (const reply of node.replies) {
precacheThreadPosts(queryClient, reply)
}
}
}
}
+61
View File
@@ -0,0 +1,61 @@
import React from 'react'
import * as persisted from '#/state/persisted'
import {isWeb} from '#/platform/detection'
type StateContext = string
type SetContext = (v: string) => void
const stateContext = React.createContext<StateContext>('home')
const setContext = React.createContext<SetContext>((_: string) => {})
function getInitialFeed() {
if (isWeb) {
if (window.location.pathname === '/') {
const params = new URLSearchParams(window.location.search)
const feedFromUrl = params.get('feed')
if (feedFromUrl) {
// If explicitly booted from a link like /?feed=..., prefer that.
return feedFromUrl
}
}
const feedFromSession = sessionStorage.getItem('lastSelectedHomeFeed')
if (feedFromSession) {
// Fall back to a previously chosen feed for this browser tab.
return feedFromSession
}
}
const feedFromPersisted = persisted.get('lastSelectedHomeFeed')
if (feedFromPersisted) {
// Fall back to the last chosen one across all tabs.
return feedFromPersisted
}
return 'home'
}
export function Provider({children}: React.PropsWithChildren<{}>) {
const [state, setState] = React.useState(getInitialFeed)
const saveState = React.useCallback((feed: string) => {
setState(feed)
if (isWeb) {
try {
sessionStorage.setItem('lastSelectedHomeFeed', feed)
} catch {}
}
persisted.write('lastSelectedHomeFeed', feed)
}, [])
return (
<stateContext.Provider value={state}>
<setContext.Provider value={saveState}>{children}</setContext.Provider>
</stateContext.Provider>
)
}
export function useSelectedFeed() {
return React.useContext(stateContext)
}
export function useSetSelectedFeed() {
return React.useContext(setContext)
}
+3 -3
View File
@@ -83,19 +83,19 @@ export function HomeLoggedOutCTA() {
<View style={[styles.footer, pal.view, pal.border]}>
<TextLink
type="2xl"
href="https://blueskyweb.xyz"
href="https://bsky.social"
text={_(msg`Business`)}
style={[styles.footerLink, pal.link]}
/>
<TextLink
type="2xl"
href="https://blueskyweb.xyz/blog"
href="https://bsky.social/about/blog"
text={_(msg`Blog`)}
style={[styles.footerLink, pal.link]}
/>
<TextLink
type="2xl"
href="https://blueskyweb.xyz/join"
href="https://bsky.social/about/join"
text={_(msg`Jobs`)}
style={[styles.footerLink, pal.link]}
/>
+3 -3
View File
@@ -102,17 +102,17 @@ function Footer({styles}: {styles: ReturnType<typeof useStyles>}) {
return (
<View style={[styles.footer, pal.view, pal.border]}>
<TextLink
href="https://blueskyweb.xyz"
href="https://bsky.social"
text="Business"
style={[styles.footerLink, pal.link]}
/>
<TextLink
href="https://blueskyweb.xyz/blog"
href="https://bsky.social/about/blog"
text="Blog"
style={[styles.footerLink, pal.link]}
/>
<TextLink
href="https://blueskyweb.xyz/join"
href="https://bsky.social/about/join"
text="Jobs"
style={[styles.footerLink, pal.link]}
/>
+77 -25
View File
@@ -3,6 +3,7 @@ import {
ActivityIndicator,
Keyboard,
StyleSheet,
TouchableOpacity,
TouchableWithoutFeedback,
View,
} from 'react-native'
@@ -13,7 +14,6 @@ import {StepHeader} from './StepHeader'
import {s} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {TextInput} from '../util/TextInput'
import {Button} from '../../util/forms/Button'
import {Policies} from './Policies'
import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
import {isWeb} from 'platform/detection'
@@ -21,7 +21,14 @@ import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {logger} from '#/logger'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {useDialogControl} from '#/components/Dialog'
import {ServerInputDialog} from '../server-input'
import {toNiceDomain} from '#/lib/strings/url-helpers'
function sanitizeDate(date: Date): Date {
if (!date || date.toString() === 'Invalid Date') {
@@ -43,16 +50,12 @@ export function Step1({
const pal = usePalette('default')
const {_} = useLingui()
const {openModal} = useModalControls()
const serverInputControl = useDialogControl()
const onPressSelectService = React.useCallback(() => {
openModal({
name: 'server-input',
initialService: uiState.serviceUrl,
onSelect: (url: string) =>
uiDispatch({type: 'set-service-url', value: url}),
})
serverInputControl.open()
Keyboard.dismiss()
}, [uiDispatch, uiState.serviceUrl, openModal])
}, [serverInputControl])
const onPressWaitlist = React.useCallback(() => {
openModal({name: 'waitlist'})
@@ -64,23 +67,72 @@ export function Step1({
return (
<View>
<StepHeader uiState={uiState} title={_(msg`Your account`)}>
<View>
<Button
testID="selectServiceButton"
type="default"
style={{
aspectRatio: 1,
justifyContent: 'center',
alignItems: 'center',
}}
accessibilityLabel={_(msg`Select service`)}
accessibilityHint={_(msg`Sets server for the Bluesky client`)}
onPress={onPressSelectService}>
<FontAwesomeIcon icon="server" size={21} color={pal.colors.text} />
</Button>
<ServerInputDialog
control={serverInputControl}
onSelect={url => uiDispatch({type: 'set-service-url', value: url})}
/>
<StepHeader uiState={uiState} title={_(msg`Your account`)} />
<View style={s.pb20}>
<Text type="md-medium" style={[pal.text, s.mb2]}>
<Trans>Hosting provider</Trans>
</Text>
<View style={[pal.border, {borderWidth: 1, borderRadius: 6}]}>
<View
style={[
pal.borderDark,
{flexDirection: 'row', alignItems: 'center'},
]}>
<FontAwesomeIcon
icon="globe"
style={[pal.textLight, {marginLeft: 14}]}
/>
<TouchableOpacity
testID="loginSelectServiceButton"
style={{
flexDirection: 'row',
flex: 1,
alignItems: 'center',
}}
onPress={onPressSelectService}
accessibilityRole="button"
accessibilityLabel={_(msg`Select service`)}
accessibilityHint={_(msg`Sets server for the Bluesky client`)}>
<Text
type="xl"
style={[
pal.text,
{
flex: 1,
paddingVertical: 10,
paddingRight: 12,
paddingLeft: 10,
},
]}>
{toNiceDomain(uiState.serviceUrl)}
</Text>
<View
style={[
pal.btn,
{
flexDirection: 'row',
alignItems: 'center',
borderRadius: 6,
paddingVertical: 6,
paddingHorizontal: 8,
marginHorizontal: 6,
},
]}>
<FontAwesomeIcon
icon="pen"
size={12}
style={pal.textLight as FontAwesomeIconStyle}
/>
</View>
</TouchableOpacity>
</View>
</View>
</StepHeader>
</View>
{!uiState.serviceDescription ? (
<ActivityIndicator />
+13 -9
View File
@@ -1,6 +1,7 @@
import React, {useState, useEffect} from 'react'
import {
ActivityIndicator,
Keyboard,
TextInput,
TouchableOpacity,
View,
@@ -24,7 +25,9 @@ import {logger} from '#/logger'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {styles} from './styles'
import {useModalControls} from '#/state/modals'
import {useDialogControl} from '#/components/Dialog'
import {ServerInputDialog} from '../server-input'
type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
@@ -51,19 +54,16 @@ export const ForgotPasswordForm = ({
const [email, setEmail] = useState<string>('')
const {screen} = useAnalytics()
const {_} = useLingui()
const {openModal} = useModalControls()
const serverInputControl = useDialogControl()
useEffect(() => {
screen('Signin:ForgotPassword')
}, [screen])
const onPressSelectService = () => {
openModal({
name: 'server-input',
initialService: serviceUrl,
onSelect: setServiceUrl,
})
}
const onPressSelectService = React.useCallback(() => {
serverInputControl.open()
Keyboard.dismiss()
}, [serverInputControl])
const onPressNext = async () => {
if (!EmailValidator.validate(email)) {
@@ -96,6 +96,10 @@ export const ForgotPasswordForm = ({
return (
<>
<View>
<ServerInputDialog
control={serverInputControl}
onSelect={setServiceUrl}
/>
<Text type="title-lg" style={[pal.text, styles.screenTitle]}>
<Trans>Reset password</Trans>
</Text>
+10 -7
View File
@@ -25,7 +25,9 @@ import {logger} from '#/logger'
import {Trans, msg} from '@lingui/macro'
import {styles} from './styles'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {useDialogControl} from '#/components/Dialog'
import {ServerInputDialog} from '../server-input'
type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
@@ -58,15 +60,11 @@ export const LoginForm = ({
const [password, setPassword] = useState<string>('')
const passwordInputRef = useRef<TextInput>(null)
const {_} = useLingui()
const {openModal} = useModalControls()
const {login} = useSessionApi()
const serverInputControl = useDialogControl()
const onPressSelectService = () => {
openModal({
name: 'server-input',
initialService: serviceUrl,
onSelect: setServiceUrl,
})
serverInputControl.open()
Keyboard.dismiss()
track('Signin:PressedSelectService')
}
@@ -130,6 +128,11 @@ export const LoginForm = ({
const isReady = !!serviceDescription && !!identifier && !!password
return (
<View testID="loginForm">
<ServerInputDialog
control={serverInputControl}
onSelect={setServiceUrl}
/>
<Text type="sm-bold" style={[pal.text, styles.groupLabel]}>
<Trans>Sign into</Trans>
</Text>
+173
View File
@@ -0,0 +1,173 @@
import React from 'react'
import {View} from 'react-native'
import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro'
import {PROD_SERVICE} from 'lib/constants'
import * as persisted from '#/state/persisted'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {Text, P} from '#/components/Typography'
import {Button, ButtonText} from '#/components/Button'
import * as ToggleButton from '#/components/forms/ToggleButton'
import * as TextField from '#/components/forms/TextField'
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
export function ServerInputDialog({
control,
onSelect,
}: {
control: Dialog.DialogOuterProps['control']
onSelect: (url: string) => void
}) {
const {_} = useLingui()
const t = useTheme()
const {gtMobile} = useBreakpoints()
const [pdsAddressHistory, setPdsAddressHistory] = React.useState<string[]>(
persisted.get('pdsAddressHistory') || [],
)
const [fixedOption, setFixedOption] = React.useState([PROD_SERVICE])
const [customAddress, setCustomAddress] = React.useState('')
const onClose = React.useCallback(() => {
let url
if (fixedOption[0] === 'custom') {
url = customAddress.trim().toLowerCase()
if (!url) {
return
}
} else {
url = fixedOption[0]
}
if (!url.startsWith('http://') && !url.startsWith('https://')) {
if (url === 'localhost' || url.startsWith('localhost:')) {
url = `http://${url}`
} else {
url = `https://${url}`
}
}
if (fixedOption[0] === 'custom') {
if (!pdsAddressHistory.includes(url)) {
const newHistory = [url, ...pdsAddressHistory.slice(0, 4)]
setPdsAddressHistory(newHistory)
persisted.write('pdsAddressHistory', newHistory)
}
}
onSelect(url)
}, [
fixedOption,
customAddress,
onSelect,
pdsAddressHistory,
setPdsAddressHistory,
])
return (
<Dialog.Outer
control={control}
nativeOptions={{sheet: {snapPoints: ['100%']}}}
onClose={onClose}>
<Dialog.Handle />
<Dialog.ScrollableInner
accessibilityDescribedBy="dialog-description"
accessibilityLabelledBy="dialog-title">
<View style={[a.relative, a.gap_md, a.w_full]}>
<Text nativeID="dialog-title" style={[a.text_2xl, a.font_bold]}>
<Trans>Choose Service</Trans>
</Text>
<P nativeID="dialog-description" style={[a.text_sm]}>
<Trans>Select the service that hosts your data.</Trans>
</P>
<ToggleButton.Group
label="Preferences"
values={fixedOption}
onChange={setFixedOption}>
<ToggleButton.Button name={PROD_SERVICE} label={_(msg`Bluesky`)}>
{_(msg`Bluesky`)}
</ToggleButton.Button>
<ToggleButton.Button
testID="customSelectBtn"
name="custom"
label={_(msg`Custom`)}>
{_(msg`Custom`)}
</ToggleButton.Button>
</ToggleButton.Group>
{fixedOption[0] === 'custom' && (
<View
style={[
a.border,
t.atoms.border_contrast_low,
a.rounded_sm,
a.px_md,
a.py_md,
]}>
<TextField.Label nativeID="address-input-label">
<Trans>Server address</Trans>
</TextField.Label>
<TextField.Root>
<TextField.Icon icon={Globe} />
<Dialog.Input
testID="customServerTextInput"
value={customAddress}
onChangeText={setCustomAddress}
label={_(msg`my-server.com`)}
accessibilityLabelledBy="address-input-label"
autoCapitalize="none"
keyboardType="url"
/>
</TextField.Root>
{pdsAddressHistory.length > 0 && (
<View style={[a.flex_row, a.flex_wrap, a.mt_xs]}>
{pdsAddressHistory.map(uri => (
<Button
key={uri}
variant="ghost"
color="primary"
label={uri}
style={[a.px_sm, a.py_xs, a.rounded_sm, a.gap_sm]}
onPress={() => setCustomAddress(uri)}>
<ButtonText>{uri}</ButtonText>
</Button>
))}
</View>
)}
</View>
)}
<View style={[a.py_xs]}>
<P
style={[
t.atoms.text_contrast_medium,
a.text_sm,
a.leading_snug,
a.flex_1,
]}>
<Trans>
Bluesky is an open network where you can choose your hosting
provider. Custom hosting is now available in beta for
developers.
</Trans>
</P>
</View>
<View style={gtMobile && [a.flex_row, a.justify_end]}>
<Button
testID="doneBtn"
variant="outline"
color="primary"
size="small"
onPress={() => control.close()}
label={_(msg`Done`)}>
{_(msg`Done`)}
</Button>
</View>
</View>
</Dialog.ScrollableInner>
</Dialog.Outer>
)
}
@@ -28,6 +28,7 @@ import {useTheme} from 'lib/ThemeContext'
import {isUriImage} from 'lib/media/util'
import {downloadAndResize} from 'lib/media/manip'
import {POST_IMG_MAX} from 'lib/constants'
import {isIOS} from 'platform/detection'
export interface TextInputRef {
focus: () => void
@@ -252,6 +253,7 @@ const styles = StyleSheet.create({
fontSize: 18,
letterSpacing: 0.2,
fontWeight: '400',
lineHeight: 23.4, // 1.3*16
// This is broken on ios right now, so don't set it there.
lineHeight: isIOS ? undefined : 23.4, // 1.3*16
},
})
@@ -228,7 +228,10 @@ export const TextInput = React.forwardRef(function TextInputImpl(
return (
<>
<View style={styles.container}>
<EditorContent editor={editor} />
<EditorContent
editor={editor}
style={{color: pal.text.color as string}}
/>
</View>
{isDropping && (
@@ -1,6 +1,6 @@
import React from 'react'
import {LabelPreference} from '@atproto/api'
import {StyleSheet, Pressable, View} from 'react-native'
import {StyleSheet, Pressable, View, Linking} from 'react-native'
import LinearGradient from 'react-native-linear-gradient'
import {ScrollView} from './util'
import {s, colors, gradients} from 'lib/styles'
@@ -129,6 +129,10 @@ function AdultContentEnabledPref() {
}
}, [variables, preferences, mutate, _])
const onAdultContentLinkPress = React.useCallback(() => {
Linking.openURL('https://bsky.app/')
}, [])
return (
<View style={s.mb10}>
{isIOS ? (
@@ -138,8 +142,9 @@ function AdultContentEnabledPref() {
Adult content can only be enabled via the Web at{' '}
<TextLink
style={pal.link}
href="https://bsky.app"
href=""
text="bsky.app"
onPress={onAdultContentLinkPress}
/>
.
</Trans>
+2 -39
View File
@@ -1,19 +1,13 @@
import React, {useRef, useEffect} from 'react'
import {StyleSheet} from 'react-native'
import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context'
import {SafeAreaView} from 'react-native-safe-area-context'
import BottomSheet from '@gorhom/bottom-sheet'
import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop'
import {usePalette} from 'lib/hooks/usePalette'
import {timeout} from 'lib/async/timeout'
import {navigate} from '../../../Navigation'
import once from 'lodash.once'
import {useModals, useModalControls} from '#/state/modals'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import * as ConfirmModal from './Confirm'
import * as EditProfileModal from './EditProfile'
import * as ProfilePreviewModal from './ProfilePreview'
import * as ServerInputModal from './ServerInput'
import * as RepostModal from './Repost'
import * as SelfLabelModal from './SelfLabel'
import * as ThreadgateModal from './Threadgate'
@@ -50,34 +44,14 @@ export function ModalsContainer() {
const {closeModal} = useModalControls()
const bottomSheetRef = useRef<BottomSheet>(null)
const pal = usePalette('default')
const safeAreaInsets = useSafeAreaInsets()
const activeModal = activeModals[activeModals.length - 1]
const navigateOnce = once(navigate)
// It seems like the bottom sheet bugs out when this callback changes.
const onBottomSheetAnimate = useNonReactiveCallback(
(_fromIndex: number, toIndex: number) => {
if (activeModal?.name === 'profile-preview' && toIndex === 1) {
// begin loading the profile screen behind the scenes
navigateOnce('Profile', {name: activeModal.did})
}
},
)
const onBottomSheetChange = async (snapPoint: number) => {
if (snapPoint === -1) {
closeModal()
} else if (activeModal?.name === 'profile-preview' && snapPoint === 1) {
await navigateOnce('Profile', {name: activeModal.did})
// There is no particular callback for when the view has actually been presented.
// This delay gives us a decent chance the navigation has flushed *and* images have loaded.
// It's acceptable because the data is already being fetched + it usually takes longer anyway.
// TODO: Figure out why avatar/cover don't always show instantly from cache.
await timeout(200)
closeModal()
}
}
const onClose = () => {
bottomSheetRef.current?.close()
closeModal()
@@ -91,7 +65,6 @@ export function ModalsContainer() {
}
}, [isModalActive, bottomSheetRef, activeModal?.name])
let needsSafeTopInset = false
let snapPoints: (string | number)[] = DEFAULT_SNAPPOINTS
let element
if (activeModal?.name === 'confirm') {
@@ -100,13 +73,6 @@ export function ModalsContainer() {
} else if (activeModal?.name === 'edit-profile') {
snapPoints = EditProfileModal.snapPoints
element = <EditProfileModal.Component {...activeModal} />
} else if (activeModal?.name === 'profile-preview') {
snapPoints = ProfilePreviewModal.snapPoints
element = <ProfilePreviewModal.Component {...activeModal} />
needsSafeTopInset = true // Need to align with the target profile screen.
} else if (activeModal?.name === 'server-input') {
snapPoints = ServerInputModal.snapPoints
element = <ServerInputModal.Component {...activeModal} />
} else if (activeModal?.name === 'report') {
snapPoints = ReportModal.snapPoints
element = <ReportModal.Component {...activeModal} />
@@ -200,12 +166,10 @@ export function ModalsContainer() {
)
}
const topInset = needsSafeTopInset ? safeAreaInsets.top - HANDLE_HEIGHT : 0
return (
<BottomSheet
ref={bottomSheetRef}
snapPoints={snapPoints}
topInset={topInset}
handleHeight={HANDLE_HEIGHT}
index={isModalActive ? 0 : -1}
enablePanDownToClose
@@ -216,7 +180,6 @@ export function ModalsContainer() {
}
handleIndicatorStyle={{backgroundColor: pal.text.color}}
handleStyle={[styles.handle, pal.view]}
onAnimate={onBottomSheetAnimate}
onChange={onBottomSheetChange}>
{element}
</BottomSheet>
-6
View File
@@ -9,8 +9,6 @@ import {useModals, useModalControls} from '#/state/modals'
import type {Modal as ModalIface} from '#/state/modals'
import * as ConfirmModal from './Confirm'
import * as EditProfileModal from './EditProfile'
import * as ProfilePreviewModal from './ProfilePreview'
import * as ServerInputModal from './ServerInput'
import * as ReportModal from './report/Modal'
import * as AppealLabelModal from './AppealLabel'
import * as CreateOrEditListModal from './CreateOrEditList'
@@ -85,10 +83,6 @@ function Modal({modal}: {modal: ModalIface}) {
element = <ConfirmModal.Component {...modal} />
} else if (modal.name === 'edit-profile') {
element = <EditProfileModal.Component {...modal} />
} else if (modal.name === 'profile-preview') {
element = <ProfilePreviewModal.Component {...modal} />
} else if (modal.name === 'server-input') {
element = <ServerInputModal.Component {...modal} />
} else if (modal.name === 'report') {
element = <ReportModal.Component {...modal} />
} else if (modal.name === 'appeal-label') {
-134
View File
@@ -1,134 +0,0 @@
import React, {useState, useEffect} from 'react'
import {ActivityIndicator, StyleSheet, View} from 'react-native'
import {AppBskyActorDefs, ModerationOpts, moderateProfile} from '@atproto/api'
import {ThemedText} from '../util/text/ThemedText'
import {usePalette} from 'lib/hooks/usePalette'
import {useAnalytics} from 'lib/analytics/analytics'
import {ProfileHeader} from '../profile/ProfileHeader'
import {InfoCircleIcon} from 'lib/icons'
import {useNavigationState} from '@react-navigation/native'
import {s} from 'lib/styles'
import {useModerationOpts} from '#/state/queries/preferences'
import {useProfileQuery} from '#/state/queries/profile'
import {ErrorScreen} from '../util/error/ErrorScreen'
import {CenteredView} from '../util/Views'
import {cleanError} from '#/lib/strings/errors'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
export const snapPoints = [520, '100%']
export function Component({did}: {did: string}) {
const pal = usePalette('default')
const {_} = useLingui()
const moderationOpts = useModerationOpts()
const {
data: profile,
error: profileError,
refetch: refetchProfile,
isLoading: isLoadingProfile,
} = useProfileQuery({
did: did,
})
if (isLoadingProfile || !moderationOpts) {
return (
<CenteredView style={[pal.view, s.flex1]}>
<ProfileHeader
profile={null}
moderation={null}
isProfilePreview={true}
/>
</CenteredView>
)
}
if (profileError) {
return (
<ErrorScreen
title={_(msg`Oops!`)}
message={cleanError(profileError)}
onPressTryAgain={refetchProfile}
/>
)
}
if (profile && moderationOpts) {
return <ComponentLoaded profile={profile} moderationOpts={moderationOpts} />
}
// should never happen
return (
<ErrorScreen
title={_(msg`Oops!`)}
message={_(msg`Something went wrong and we're not sure what.`)}
onPressTryAgain={refetchProfile}
/>
)
}
function ComponentLoaded({
profile: profileUnshadowed,
moderationOpts,
}: {
profile: AppBskyActorDefs.ProfileViewDetailed
moderationOpts: ModerationOpts
}) {
const pal = usePalette('default')
const profile = useProfileShadow(profileUnshadowed)
const {screen} = useAnalytics()
const moderation = React.useMemo(
() => moderateProfile(profile, moderationOpts),
[profile, moderationOpts],
)
// track the navigator state to detect if a page-load occurred
const navState = useNavigationState(state => state)
const [initNavState] = useState(navState)
const isLoading = initNavState !== navState
useEffect(() => {
screen('Profile:Preview')
}, [screen])
return (
<View testID="profilePreview" style={[pal.view, s.flex1]}>
<View style={[styles.headerWrapper]}>
<ProfileHeader
profile={profile}
moderation={moderation}
hideBackButton
isProfilePreview
/>
</View>
<View style={[styles.hintWrapper, pal.view]}>
<View style={styles.hint}>
{isLoading ? (
<ActivityIndicator />
) : (
<>
<InfoCircleIcon size={21} style={pal.textLight} />
<ThemedText type="xl" fg="light">
<Trans>Swipe up to see more</Trans>
</ThemedText>
</>
)}
</View>
</View>
</View>
)
}
const styles = StyleSheet.create({
headerWrapper: {
height: 440,
},
hintWrapper: {
height: 80,
},
hint: {
flexDirection: 'row',
justifyContent: 'center',
gap: 8,
paddingHorizontal: 14,
borderRadius: 6,
},
})
-189
View File
@@ -1,189 +0,0 @@
import React, {useState} from 'react'
import {Platform, StyleSheet, TouchableOpacity, View} from 'react-native'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {ScrollView, TextInput} from './util'
import {Text} from '../util/text/Text'
import {s, colors} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {useTheme} from 'lib/ThemeContext'
import {LOCAL_DEV_SERVICE, STAGING_SERVICE, PROD_SERVICE} from 'lib/constants'
import {LOGIN_INCLUDE_DEV_SERVERS} from 'lib/build-flags'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
export const snapPoints = ['80%']
export function Component({onSelect}: {onSelect: (url: string) => void}) {
const theme = useTheme()
const pal = usePalette('default')
const [customUrl, setCustomUrl] = useState<string>('')
const {_} = useLingui()
const {closeModal} = useModalControls()
const doSelect = (url: string) => {
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = `https://${url}`
}
closeModal()
onSelect(url)
}
return (
<View style={[pal.view, s.flex1]} testID="serverInputModal">
<Text type="2xl-bold" style={[pal.text, s.textCenter]}>
<Trans>Choose Service</Trans>
</Text>
<ScrollView style={styles.inner}>
<View style={styles.group}>
{LOGIN_INCLUDE_DEV_SERVERS ? (
<>
<TouchableOpacity
testID="localDevServerButton"
style={styles.btn}
onPress={() => doSelect(LOCAL_DEV_SERVICE)}
accessibilityRole="button">
<Text style={styles.btnText}>
<Trans>Local dev server</Trans>
</Text>
<FontAwesomeIcon
icon="arrow-right"
style={s.white as FontAwesomeIconStyle}
/>
</TouchableOpacity>
<TouchableOpacity
style={styles.btn}
onPress={() => doSelect(STAGING_SERVICE)}
accessibilityRole="button">
<Text style={styles.btnText}>
<Trans>Staging</Trans>
</Text>
<FontAwesomeIcon
icon="arrow-right"
style={s.white as FontAwesomeIconStyle}
/>
</TouchableOpacity>
</>
) : undefined}
<TouchableOpacity
style={styles.btn}
onPress={() => doSelect(PROD_SERVICE)}
accessibilityRole="button"
accessibilityLabel={_(msg`Select Bluesky Social`)}
accessibilityHint="Sets Bluesky Social as your service provider">
<Text style={styles.btnText}>
<Trans>Bluesky.Social</Trans>
</Text>
<FontAwesomeIcon
icon="arrow-right"
style={s.white as FontAwesomeIconStyle}
/>
</TouchableOpacity>
</View>
<View style={styles.group}>
<Text style={[pal.text, styles.label]}>
<Trans>Other service</Trans>
</Text>
<View style={s.flexRow}>
<TextInput
testID="customServerTextInput"
style={[pal.borderDark, pal.text, styles.textInput]}
placeholder="e.g. https://bsky.app"
placeholderTextColor={colors.gray4}
autoCapitalize="none"
autoComplete="off"
autoCorrect={false}
keyboardAppearance={theme.colorScheme}
value={customUrl}
onChangeText={setCustomUrl}
accessibilityLabel={_(msg`Custom domain`)}
// TODO: Simplify this wording further to be understandable by everyone
accessibilityHint={_(
msg`Use your domain as your Bluesky client service provider`,
)}
/>
<TouchableOpacity
testID="customServerSelectBtn"
style={[pal.borderDark, pal.text, styles.textInputBtn]}
onPress={() => doSelect(customUrl)}
accessibilityRole="button"
accessibilityLabel={`Confirm service. ${
customUrl === ''
? _(msg`Button disabled. Input custom domain to proceed.`)
: ''
}`}
accessibilityHint=""
// TODO - accessibility: Need to inform state change on failure
disabled={customUrl === ''}>
<FontAwesomeIcon
icon="check"
style={[pal.text as FontAwesomeIconStyle, styles.checkIcon]}
size={18}
/>
</TouchableOpacity>
</View>
</View>
</ScrollView>
</View>
)
}
const styles = StyleSheet.create({
inner: {
padding: 14,
},
group: {
marginBottom: 20,
},
label: {
fontWeight: 'bold',
paddingHorizontal: 4,
paddingBottom: 4,
},
textInput: {
flex: 1,
borderWidth: 1,
borderTopLeftRadius: 6,
borderBottomLeftRadius: 6,
paddingHorizontal: 14,
paddingVertical: 12,
fontSize: 16,
},
textInputBtn: {
borderWidth: 1,
borderLeftWidth: 0,
borderTopRightRadius: 6,
borderBottomRightRadius: 6,
paddingHorizontal: 14,
paddingVertical: 10,
},
btn: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.blue3,
borderRadius: 6,
paddingHorizontal: 14,
paddingVertical: 10,
marginBottom: 6,
},
btnText: {
flex: 1,
fontSize: 18,
fontWeight: '500',
color: colors.white,
},
checkIcon: {
position: 'relative',
...Platform.select({
android: {
top: 8,
},
ios: {
top: 2,
},
}),
},
})
+1 -1
View File
@@ -18,7 +18,7 @@ import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {getAgent} from '#/state/session'
const DMCA_LINK = 'https://blueskyweb.xyz/support/copyright'
const DMCA_LINK = 'https://bsky.social/about/support/copyright'
export const snapPoints = [575]
+1 -1
View File
@@ -31,7 +31,7 @@ export const Pager = React.forwardRef(function PagerImpl(
const anchorRef = React.useRef(null)
React.useImperativeHandle(ref, () => ({
setPage: (index: number) => setSelectedPage(index),
setPage: (index: number) => onTabBarSelect(index),
}))
const onTabBarSelect = React.useCallback(
+43 -22
View File
@@ -61,26 +61,19 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
const headerHeight = headerOnlyHeight + tabBarHeight
// capture the header bar sizing
const onTabBarLayout = React.useCallback(
(evt: LayoutChangeEvent) => {
const height = evt.nativeEvent.layout.height
if (height > 0) {
// The rounding is necessary to prevent jumps on iOS
setTabBarHeight(Math.round(height))
}
},
[setTabBarHeight],
)
const onHeaderOnlyLayout = React.useCallback(
(evt: LayoutChangeEvent) => {
const height = evt.nativeEvent.layout.height
if (height > 0) {
// The rounding is necessary to prevent jumps on iOS
setHeaderOnlyHeight(Math.round(height))
}
},
[setHeaderOnlyHeight],
)
const onTabBarLayout = useNonReactiveCallback((evt: LayoutChangeEvent) => {
const height = evt.nativeEvent.layout.height
if (height > 0) {
// The rounding is necessary to prevent jumps on iOS
setTabBarHeight(Math.round(height))
}
})
const onHeaderOnlyLayout = useNonReactiveCallback((height: number) => {
if (height > 0) {
// The rounding is necessary to prevent jumps on iOS
setHeaderOnlyHeight(Math.round(height))
}
})
const renderTabBar = React.useCallback(
(props: RenderTabBarFnProps) => {
@@ -228,7 +221,7 @@ let PagerTabBar = ({
testID?: string
scrollY: SharedValue<number>
renderHeader?: () => JSX.Element
onHeaderOnlyLayout: (e: LayoutChangeEvent) => void
onHeaderOnlyLayout: (height: number) => void
onTabBarLayout: (e: LayoutChangeEvent) => void
onCurrentPageSelected?: (index: number) => void
onSelect?: (index: number) => void
@@ -240,12 +233,40 @@ let PagerTabBar = ({
},
],
}))
const pendingHeaderHeight = React.useRef<null | number>(null)
return (
<Animated.View
pointerEvents="box-none"
style={[styles.tabBarMobile, headerTransform]}>
<View onLayout={onHeaderOnlyLayout} pointerEvents="box-none">
<View
pointerEvents="box-none"
collapsable={false}
onLayout={e => {
if (isHeaderReady) {
onHeaderOnlyLayout(e.nativeEvent.layout.height)
pendingHeaderHeight.current = null
} else {
// Stash it away for when `isHeaderReady` turns `true` later.
pendingHeaderHeight.current = e.nativeEvent.layout.height
}
}}>
{renderHeader?.()}
{
// When `isHeaderReady` turns `true`, we want to send the parent layout.
// However, if that didn't lead to a layout change, parent `onLayout` wouldn't get called again.
// We're conditionally rendering an empty view so that we can send the last measurement.
isHeaderReady && (
<View
onLayout={() => {
// We're assuming the parent `onLayout` already ran (parent -> child ordering).
if (pendingHeaderHeight.current !== null) {
onHeaderOnlyLayout(pendingHeaderHeight.current)
pendingHeaderHeight.current = null
}
}}
/>
)
}
</View>
<View
onLayout={onTabBarLayout}
+31 -3
View File
@@ -31,6 +31,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
children,
testID,
items,
isHeaderReady,
renderHeader,
initialPage,
onPageSelected,
@@ -46,6 +47,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
<PagerTabBar
items={items}
renderHeader={renderHeader}
isHeaderReady={isHeaderReady}
currentPage={currentPage}
onCurrentPageSelected={onCurrentPageSelected}
onSelect={props.onSelect}
@@ -54,7 +56,14 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
/>
)
},
[items, renderHeader, currentPage, onCurrentPageSelected, testID],
[
items,
isHeaderReady,
renderHeader,
currentPage,
onCurrentPageSelected,
testID,
],
)
const onPageSelectedInner = React.useCallback(
@@ -80,8 +89,14 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
{toArray(children)
.filter(Boolean)
.map((child, i) => {
const isReady = isHeaderReady
return (
<View key={i} collapsable={false}>
<View
key={i}
collapsable={false}
style={{
display: isReady ? undefined : 'none',
}}>
<PagerItem isFocused={i === currentPage} renderTab={child} />
</View>
)
@@ -94,6 +109,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
let PagerTabBar = ({
currentPage,
items,
isHeaderReady,
testID,
renderHeader,
onCurrentPageSelected,
@@ -104,6 +120,7 @@ let PagerTabBar = ({
items: string[]
testID?: string
renderHeader?: () => JSX.Element
isHeaderReady: boolean
onCurrentPageSelected?: (index: number) => void
onSelect?: (index: number) => void
tabBarAnchor?: JSX.Element | null | undefined
@@ -112,7 +129,12 @@ let PagerTabBar = ({
const {isMobile} = useWebMediaQueries()
return (
<>
<View style={[!isMobile && styles.headerContainerDesktop, pal.border]}>
<View
style={[
!isMobile && styles.headerContainerDesktop,
pal.border,
!isHeaderReady && styles.loadingHeader,
]}>
{renderHeader?.()}
</View>
{tabBarAnchor}
@@ -123,6 +145,9 @@ let PagerTabBar = ({
? styles.tabBarContainerMobile
: styles.tabBarContainerDesktop,
pal.border,
{
display: isHeaderReady ? undefined : 'none',
},
]}>
<TabBar
testID={testID}
@@ -183,6 +208,9 @@ const styles = StyleSheet.create({
paddingLeft: 14,
paddingRight: 14,
},
loadingHeader: {
borderColor: 'transparent',
},
})
function toArray<T>(v: T | T[]): T[] {
@@ -0,0 +1,154 @@
import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {useNavigation} from '@react-navigation/native'
import {AppBskyActorDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {logger} from '#/logger'
import {Text} from 'view/com/util/text/Text'
import * as Toast from 'view/com/util/Toast'
import {s} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {Shadow, useProfileShadow} from 'state/cache/profile-shadow'
import {track} from 'lib/analytics/analytics'
import {
useProfileFollowMutationQueue,
useProfileQuery,
} from 'state/queries/profile'
import {useRequireAuth} from 'state/session'
export function PostThreadFollowBtn({did}: {did: string}) {
const {data: profile, isLoading} = useProfileQuery({did})
// We will never hit this - the profile will always be cached or loaded above
// but it keeps the typechecker happy
if (isLoading || !profile) return null
return <PostThreadFollowBtnLoaded profile={profile} />
}
function PostThreadFollowBtnLoaded({
profile: profileUnshadowed,
}: {
profile: AppBskyActorDefs.ProfileViewDetailed
}) {
const navigation = useNavigation()
const {_} = useLingui()
const pal = usePalette('default')
const palInverted = usePalette('inverted')
const {isTabletOrDesktop} = useWebMediaQueries()
const profile: Shadow<AppBskyActorDefs.ProfileViewBasic> =
useProfileShadow(profileUnshadowed)
const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile)
const requireAuth = useRequireAuth()
const isFollowing = !!profile.viewer?.following
const [wasFollowing, setWasFollowing] = React.useState<boolean>(isFollowing)
// This prevents the button from disappearing as soon as we follow.
const showFollowBtn = React.useMemo(
() => !isFollowing || !wasFollowing,
[isFollowing, wasFollowing],
)
/**
* We want this button to stay visible even after following, so that the user can unfollow if they want.
* However, we need it to disappear after we push to a screen and then come back. We also need it to
* show up if we view the post while following, go to the profile and unfollow, then come back to the
* post.
*
* We want to update wasFollowing both on blur and on focus so that we hit all these cases. On native,
* we could do this only on focus because the transition animation gives us time to not notice the
* sudden rendering of the button. However, on web if we do this, there's an obvious flicker once the
* button renders. So, we update the state in both cases.
*/
React.useEffect(() => {
const updateWasFollowing = () => {
if (wasFollowing !== isFollowing) {
setWasFollowing(isFollowing)
}
}
const unsubscribeFocus = navigation.addListener('focus', updateWasFollowing)
const unsubscribeBlur = navigation.addListener('blur', updateWasFollowing)
return () => {
unsubscribeFocus()
unsubscribeBlur()
}
}, [isFollowing, wasFollowing, navigation])
const onPress = React.useCallback(() => {
if (!isFollowing) {
requireAuth(async () => {
try {
track('ProfileHeader:FollowButtonClicked')
await queueFollow()
} catch (e: any) {
if (e?.name !== 'AbortError') {
logger.error('Failed to follow', {message: String(e)})
Toast.show(_(msg`There was an issue! ${e.toString()}`))
}
}
})
} else {
requireAuth(async () => {
try {
track('ProfileHeader:UnfollowButtonClicked')
await queueUnfollow()
} catch (e: any) {
if (e?.name !== 'AbortError') {
logger.error('Failed to unfollow', {message: String(e)})
Toast.show(_(msg`There was an issue! ${e.toString()}`))
}
}
})
}
}, [isFollowing, requireAuth, queueFollow, _, queueUnfollow])
if (!showFollowBtn) return null
return (
<View style={{width: isTabletOrDesktop ? 130 : 120}}>
<View style={styles.btnOuter}>
<TouchableOpacity
testID="followBtn"
onPress={onPress}
style={[styles.btn, !isFollowing ? palInverted.view : pal.viewLight]}
accessibilityRole="button"
accessibilityLabel={_(msg`Follow ${profile.handle}`)}
accessibilityHint={_(
msg`Shows posts from ${profile.handle} in your feed`,
)}>
{isTabletOrDesktop && (
<FontAwesomeIcon
icon={!isFollowing ? 'plus' : 'check'}
style={[!isFollowing ? palInverted.text : pal.text, s.mr5]}
/>
)}
<Text
type="button"
style={[!isFollowing ? palInverted.text : pal.text, s.bold]}
numberOfLines={1}>
{!isFollowing ? <Trans>Follow</Trans> : <Trans>Following</Trans>}
</Text>
</TouchableOpacity>
</View>
</View>
)
}
const styles = StyleSheet.create({
btnOuter: {
marginLeft: 'auto',
},
btn: {
flexDirection: 'row',
borderRadius: 50,
paddingVertical: 8,
paddingHorizontal: 14,
},
})
+101 -150
View File
@@ -9,6 +9,7 @@ import {
} from '@atproto/api'
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {PostThreadFollowBtn} from 'view/com/post-thread/PostThreadFollowBtn'
import {Link, TextLink} from '../util/Link'
import {RichText} from '../util/text/RichText'
import {Text} from '../util/text/Text'
@@ -23,7 +24,6 @@ import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers'
import {PostMeta} from '../util/PostMeta'
import {PostEmbeds} from '../util/post-embeds'
import {PostCtrls} from '../util/post-ctrls/PostCtrls'
import {PostDropdownBtn} from '../util/forms/PostDropdownBtn'
import {PostHider} from '../util/moderation/PostHider'
import {ContentHider} from '../util/moderation/ContentHider'
import {PostAlerts} from '../util/moderation/PostAlerts'
@@ -31,7 +31,6 @@ import {PostSandboxWarning} from '../util/PostSandboxWarning'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {usePalette} from 'lib/hooks/usePalette'
import {formatCount} from '../util/numeric/format'
import {TimeElapsed} from 'view/com/util/TimeElapsed'
import {makeProfileLink} from 'lib/routes/links'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {MAX_POST_LINES} from 'lib/constants'
@@ -43,7 +42,7 @@ import {useModerationOpts} from '#/state/queries/preferences'
import {useOpenLink} from '#/state/preferences/in-app-browser'
import {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow'
import {ThreadPost} from '#/state/queries/post-thread'
import {useSession} from '#/state/session'
import {useSession} from 'state/session'
import {WhoCanReply} from '../threadgate/WhoCanReply'
export function PostThreadItem({
@@ -115,7 +114,6 @@ export function PostThreadItem({
}
function PostThreadItemDeleted() {
const styles = useStyles()
const pal = usePalette('default')
return (
<View style={[styles.outer, pal.border, pal.view, s.p20, s.flexRow]}>
@@ -162,11 +160,10 @@ let PostThreadItemLoaded = ({
const {_} = useLingui()
const langPrefs = useLanguagePrefs()
const {openComposer} = useComposerControls()
const {currentAccount} = useSession()
const [limitLines, setLimitLines] = React.useState(
() => countLines(richText?.text) >= MAX_POST_LINES,
)
const styles = useStyles()
const {currentAccount} = useSession()
const hasEngagement = post.likeCount || post.repostCount
const rootUri = record.reply?.root?.uri || post.uri
@@ -188,9 +185,6 @@ let PostThreadItemLoaded = ({
return makeProfileLink(post.author, 'post', urip.rkey, 'reposted-by')
}, [post.uri, post.author])
const repostsTitle = _(msg`Reposts of this post`)
const isModeratedPost =
moderation.decisions.post.cause?.type === 'label' &&
moderation.decisions.post.cause.label.src !== currentAccount?.did
const translatorUrl = getTranslatorLink(
record?.text || '',
@@ -255,7 +249,7 @@ let PostThreadItemLoaded = ({
style={[styles.outer, styles.outerHighlighted, pal.border, pal.view]}
accessible={false}>
<PostSandboxWarning />
<View style={styles.layout}>
<View style={[styles.layout]}>
<View style={[styles.layoutAvi, {paddingBottom: 8}]}>
<PreviewableUserAvatar
size={42}
@@ -268,33 +262,18 @@ let PostThreadItemLoaded = ({
<View style={styles.layoutContent}>
<View
style={[styles.meta, styles.metaExpandedLine1, {zIndex: 1}]}>
<View style={[s.flexRow]}>
<Link
style={styles.metaItem}
href={authorHref}
title={authorTitle}>
<Text
type="xl-bold"
style={[pal.text]}
numberOfLines={1}
lineHeight={1.2}>
{sanitizeDisplayName(
post.author.displayName ||
sanitizeHandle(post.author.handle),
)}
</Text>
</Link>
<TimeElapsed timestamp={post.indexedAt}>
{({timeElapsed}) => (
<Text
type="md"
style={[styles.metaItem, pal.textLight]}
title={niceDate(post.indexedAt)}>
&middot;&nbsp;{timeElapsed}
</Text>
<Link style={s.flex1} href={authorHref} title={authorTitle}>
<Text
type="xl-bold"
style={[pal.text]}
numberOfLines={1}
lineHeight={1.2}>
{sanitizeDisplayName(
post.author.displayName ||
sanitizeHandle(post.author.handle),
)}
</TimeElapsed>
</View>
</Text>
</Link>
</View>
<View style={styles.meta}>
{isAuthorMuted && (
@@ -321,33 +300,16 @@ let PostThreadItemLoaded = ({
</Text>
</View>
)}
<Link
style={styles.metaItem}
href={authorHref}
title={authorTitle}>
<Link style={s.flex1} href={authorHref} title={authorTitle}>
<Text type="md" style={[pal.textLight]} numberOfLines={1}>
{sanitizeHandle(post.author.handle, '@')}
</Text>
</Link>
</View>
</View>
<PostDropdownBtn
testID="postDropdownBtn"
postAuthor={post.author}
postCid={post.cid}
postUri={post.uri}
record={record}
richText={richText}
showAppealLabelItem={
post.author.did === currentAccount?.did && isModeratedPost
}
style={{
paddingVertical: 6,
paddingHorizontal: 10,
marginLeft: 'auto',
width: 40,
}}
/>
{currentAccount?.did !== post.author.did && (
<PostThreadFollowBtn did={post.author.did} />
)}
</View>
<View style={[s.pl10, s.pr10, s.pb10]}>
<ContentHider
@@ -437,7 +399,7 @@ let PostThreadItemLoaded = ({
) : (
<></>
)}
<View style={[s.pl10, s.pb5]}>
<View style={[s.pl10, s.pr10, s.pb5]}>
<PostCtrls
big
post={post}
@@ -649,7 +611,6 @@ function PostOuterWrapper({
}>) {
const {isMobile} = useWebMediaQueries()
const pal = usePalette('default')
const styles = useStyles()
if (treeView && depth > 0) {
return (
<View
@@ -726,94 +687,84 @@ function ExpandedPostDetails({
)
}
const useStyles = () => {
const {isDesktop} = useWebMediaQueries()
return StyleSheet.create({
outer: {
borderTopWidth: 1,
paddingLeft: 8,
},
outerHighlighted: {
paddingTop: 16,
paddingLeft: 8,
paddingRight: 8,
},
noTopBorder: {
borderTopWidth: 0,
},
layout: {
flexDirection: 'row',
gap: 10,
paddingLeft: 8,
},
layoutAvi: {},
layoutContent: {
flex: 1,
paddingRight: 10,
},
meta: {
flexDirection: 'row',
paddingTop: 2,
paddingBottom: 2,
},
metaExpandedLine1: {
paddingTop: 0,
paddingBottom: 0,
},
metaItem: {
paddingRight: 5,
maxWidth: isDesktop ? 380 : 220,
},
alert: {
marginBottom: 6,
},
postTextContainer: {
flexDirection: 'row',
alignItems: 'center',
flexWrap: 'wrap',
paddingBottom: 4,
paddingRight: 10,
},
postTextLargeContainer: {
paddingHorizontal: 0,
paddingRight: 0,
paddingBottom: 10,
},
translateLink: {
marginBottom: 6,
},
contentHider: {
marginBottom: 6,
},
contentHiderChild: {
marginTop: 6,
},
expandedInfo: {
flexDirection: 'row',
padding: 10,
borderTopWidth: 1,
borderBottomWidth: 1,
marginTop: 5,
marginBottom: 15,
},
expandedInfoItem: {
marginRight: 10,
},
loadMore: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'flex-start',
gap: 4,
paddingHorizontal: 20,
},
replyLine: {
width: 2,
marginLeft: 'auto',
marginRight: 'auto',
},
cursor: {
// @ts-ignore web only
cursor: 'pointer',
},
})
}
const styles = StyleSheet.create({
outer: {
borderTopWidth: 1,
paddingLeft: 8,
},
outerHighlighted: {
paddingTop: 16,
paddingLeft: 8,
paddingRight: 8,
},
noTopBorder: {
borderTopWidth: 0,
},
layout: {
flexDirection: 'row',
paddingHorizontal: 8,
},
layoutAvi: {},
layoutContent: {
flex: 1,
marginLeft: 10,
},
meta: {
flexDirection: 'row',
paddingVertical: 2,
},
metaExpandedLine1: {
paddingVertical: 0,
},
alert: {
marginBottom: 6,
},
postTextContainer: {
flexDirection: 'row',
alignItems: 'center',
flexWrap: 'wrap',
paddingBottom: 4,
paddingRight: 10,
},
postTextLargeContainer: {
paddingHorizontal: 0,
paddingRight: 0,
paddingBottom: 10,
},
translateLink: {
marginBottom: 6,
},
contentHider: {
marginBottom: 6,
},
contentHiderChild: {
marginTop: 6,
},
expandedInfo: {
flexDirection: 'row',
padding: 10,
borderTopWidth: 1,
borderBottomWidth: 1,
marginTop: 5,
marginBottom: 15,
},
expandedInfoItem: {
marginRight: 10,
},
loadMore: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'flex-start',
gap: 4,
paddingHorizontal: 20,
},
replyLine: {
width: 2,
marginLeft: 'auto',
marginRight: 'auto',
},
cursor: {
// @ts-ignore web only
cursor: 'pointer',
},
})
+52 -91
View File
@@ -1,4 +1,4 @@
import React, {memo} from 'react'
import React, {memo, useMemo} from 'react'
import {
StyleSheet,
TouchableOpacity,
@@ -10,7 +10,8 @@ import {useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
import {
AppBskyActorDefs,
ProfileModeration,
ModerationOpts,
moderateProfile,
RichText as RichTextAPI,
} from '@atproto/api'
import {Trans, msg} from '@lingui/macro'
@@ -43,79 +44,57 @@ import {usePalette} from 'lib/hooks/usePalette'
import {useAnalytics} from 'lib/analytics/analytics'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {BACK_HITSLOP} from 'lib/constants'
import {isInvalidHandle} from 'lib/strings/handles'
import {isInvalidHandle, sanitizeHandle} from 'lib/strings/handles'
import {makeProfileLink} from 'lib/routes/links'
import {pluralize} from 'lib/strings/helpers'
import {toShareUrl} from 'lib/strings/url-helpers'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles'
import {shareUrl} from 'lib/sharing'
import {s, colors} from 'lib/styles'
import {logger} from '#/logger'
import {useSession, getAgent} from '#/state/session'
import {useSession} from '#/state/session'
import {Shadow} from '#/state/cache/types'
import {useRequireAuth} from '#/state/session'
import {LabelInfo} from '../util/moderation/LabelInfo'
import {useProfileShadow} from 'state/cache/profile-shadow'
interface Props {
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> | null
moderation: ProfileModeration | null
hideBackButton?: boolean
isProfilePreview?: boolean
}
export function ProfileHeader({
profile,
moderation,
hideBackButton = false,
isProfilePreview,
}: Props) {
let ProfileHeaderLoading = (_props: {}): React.ReactNode => {
const pal = usePalette('default')
// loading
// =
if (!profile || !moderation) {
return (
<View style={pal.view}>
<LoadingPlaceholder width="100%" height={153} />
<View
style={[pal.view, {borderColor: pal.colors.background}, styles.avi]}>
<LoadingPlaceholder width={80} height={80} style={styles.br40} />
</View>
<View style={styles.content}>
<View style={[styles.buttonsLine]}>
<LoadingPlaceholder width={167} height={31} style={styles.br50} />
</View>
return (
<View style={pal.view}>
<LoadingPlaceholder width="100%" height={150} style={{borderRadius: 0}} />
<View
style={[pal.view, {borderColor: pal.colors.background}, styles.avi]}>
<LoadingPlaceholder width={80} height={80} style={styles.br40} />
</View>
<View style={styles.content}>
<View style={[styles.buttonsLine]}>
<LoadingPlaceholder width={167} height={31} style={styles.br50} />
</View>
</View>
)
}
// loaded
// =
return (
<ProfileHeaderLoaded
profile={profile}
moderation={moderation}
hideBackButton={hideBackButton}
isProfilePreview={isProfilePreview}
/>
</View>
)
}
ProfileHeaderLoading = memo(ProfileHeaderLoading)
export {ProfileHeaderLoading}
interface LoadedProps {
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
moderation: ProfileModeration
interface Props {
profile: AppBskyActorDefs.ProfileViewDetailed
descriptionRT: RichTextAPI | null
moderationOpts: ModerationOpts
hideBackButton?: boolean
isProfilePreview?: boolean
isPlaceholderProfile?: boolean
}
let ProfileHeaderLoaded = ({
profile,
moderation,
let ProfileHeader = ({
profile: profileUnshadowed,
descriptionRT,
moderationOpts,
hideBackButton = false,
isProfilePreview,
}: LoadedProps): React.ReactNode => {
isPlaceholderProfile,
}: Props): React.ReactNode => {
const profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> =
useProfileShadow(profileUnshadowed)
const pal = usePalette('default')
const palInverted = usePalette('inverted')
const {currentAccount, hasSession} = useSession()
@@ -132,37 +111,10 @@ let ProfileHeaderLoaded = ({
const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile)
const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
const queryClient = useQueryClient()
/*
* BEGIN handle bio facet resolution
*/
// should be undefined on first render to trigger a resolution
const prevProfileDescription = React.useRef<string | undefined>()
const [descriptionRT, setDescriptionRT] = React.useState<
RichTextAPI | undefined
>(
profile.description
? new RichTextAPI({text: profile.description})
: undefined,
const moderation = useMemo(
() => moderateProfile(profile, moderationOpts),
[profile, moderationOpts],
)
React.useEffect(() => {
async function resolveRTFacets() {
// new each time
const rt = new RichTextAPI({text: profile.description || ''})
await rt.detectFacets(getAgent())
// replace existing RT instance
setDescriptionRT(rt)
}
if (profile.description !== prevProfileDescription.current) {
// update prev immediately
prevProfileDescription.current = profile.description
resolveRTFacets()
}
}, [profile.description, setDescriptionRT])
/*
* END handle bio facet resolution
*/
const invalidateProfileQuery = React.useCallback(() => {
queryClient.invalidateQueries({
@@ -443,9 +395,17 @@ let ProfileHeaderLoaded = ({
const pluralizedFollowers = pluralize(profile.followersCount || 0, 'follower')
return (
<View style={pal.view} pointerEvents="box-none">
<View style={[pal.view]} pointerEvents="box-none">
<View pointerEvents="none">
<UserBanner banner={profile.banner} moderation={moderation.avatar} />
{isPlaceholderProfile ? (
<LoadingPlaceholder
width="100%"
height={150}
style={{borderRadius: 0}}
/>
) : (
<UserBanner banner={profile.banner} moderation={moderation.avatar} />
)}
</View>
<View style={styles.content} pointerEvents="box-none">
<View style={[styles.buttonsLine]} pointerEvents="box-none">
@@ -479,7 +439,7 @@ let ProfileHeaderLoaded = ({
)
) : !profile.viewer?.blockedBy ? (
<>
{!isProfilePreview && hasSession && (
{hasSession && (
<TouchableOpacity
testID="suggestedFollowsBtn"
onPress={() => setShowSuggestedFollows(!showSuggestedFollows)}
@@ -598,7 +558,7 @@ let ProfileHeaderLoaded = ({
{invalidHandle ? _(msg`⚠Invalid Handle`) : `@${profile.handle}`}
</ThemedText>
</View>
{!blockHide && (
{!isPlaceholderProfile && !blockHide && (
<>
<View style={styles.metricsLine} pointerEvents="box-none">
<Link
@@ -649,7 +609,7 @@ let ProfileHeaderLoaded = ({
</Text>
</View>
{descriptionRT && !moderation.profile.blur ? (
<View pointerEvents={isNative ? 'auto' : 'none'}>
<View pointerEvents="auto">
<RichText
testID="profileHeaderDescription"
style={[styles.description, pal.text]}
@@ -667,7 +627,7 @@ let ProfileHeaderLoaded = ({
<ProfileHeaderModCard />
</View>
{!isProfilePreview && showSuggestedFollows && (
{showSuggestedFollows && (
<ProfileHeaderSuggestedFollows
actorDid={profile.did}
requestDismiss={() => {
@@ -714,7 +674,8 @@ let ProfileHeaderLoaded = ({
</View>
)
}
ProfileHeaderLoaded = memo(ProfileHeaderLoaded)
ProfileHeader = memo(ProfileHeader)
export {ProfileHeader}
const styles = StyleSheet.create({
banner: {
+4 -1
View File
@@ -123,6 +123,7 @@ let UserAvatar = ({
usePlainRNImage = false,
}: UserAvatarProps): React.ReactNode => {
const pal = usePalette('default')
const backgroundColor = pal.colors.backgroundLight
const aviStyle = useMemo(() => {
if (type === 'algo' || type === 'list') {
@@ -130,14 +131,16 @@ let UserAvatar = ({
width: size,
height: size,
borderRadius: size > 32 ? 8 : 3,
backgroundColor,
}
}
return {
width: size,
height: size,
borderRadius: Math.floor(size / 2),
backgroundColor,
}
}, [type, size])
}, [type, size, backgroundColor])
const alert = useMemo(() => {
if (!moderation?.alert) {
+8 -3
View File
@@ -3,7 +3,10 @@ import {StyleSheet, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {ModerationUI} from '@atproto/api'
import {Image} from 'expo-image'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
import {colors} from 'lib/styles'
import {useTheme} from 'lib/ThemeContext'
import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
import {
usePhotoLibraryPermission,
@@ -13,8 +16,6 @@ import {usePalette} from 'lib/hooks/usePalette'
import {isWeb, isAndroid} from 'platform/detection'
import {Image as RNImage} from 'react-native-image-crop-picker'
import {NativeDropdown, DropdownItem} from './forms/NativeDropdown'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
export function UserBanner({
banner,
@@ -26,6 +27,7 @@ export function UserBanner({
onSelectNewBanner?: (img: RNImage | null) => void
}) {
const pal = usePalette('default')
const theme = useTheme()
const {_} = useLingui()
const {requestCameraAccessIfNeeded} = useCameraPermission()
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
@@ -142,7 +144,10 @@ export function UserBanner({
!((moderation?.blur && isAndroid) /* android crashes with blur */) ? (
<Image
testID="userBannerImage"
style={styles.bannerImage}
style={[
styles.bannerImage,
{backgroundColor: theme.palette.default.backgroundLight},
]}
resizeMode="cover"
source={{uri: banner}}
blurRadius={moderation?.blur ? 100 : 0}
+12 -32
View File
@@ -1,9 +1,8 @@
import React from 'react'
import {Pressable, StyleProp, ViewStyle} from 'react-native'
import {StyleProp, ViewStyle} from 'react-native'
import {Link} from './Link'
import {isAndroid, isWeb} from 'platform/detection'
import {isWeb} from 'platform/detection'
import {makeProfileLink} from 'lib/routes/links'
import {useModalControls} from '#/state/modals'
import {usePrefetchProfileQuery} from '#/state/queries/profile'
interface UserPreviewLinkProps {
@@ -14,38 +13,19 @@ interface UserPreviewLinkProps {
export function UserPreviewLink(
props: React.PropsWithChildren<UserPreviewLinkProps>,
) {
const {openModal} = useModalControls()
const prefetchProfileQuery = usePrefetchProfileQuery()
if (isWeb || isAndroid) {
return (
<Link
onPointerEnter={() => {
if (isWeb) {
prefetchProfileQuery(props.did)
}
}}
href={makeProfileLink(props)}
title={props.handle}
asAnchor
style={props.style}>
{props.children}
</Link>
)
}
return (
<Pressable
onPress={() =>
openModal({
name: 'profile-preview',
did: props.did,
})
}
accessibilityRole="button"
accessibilityLabel={props.handle}
accessibilityHint=""
<Link
onPointerEnter={() => {
if (isWeb) {
prefetchProfileQuery(props.did)
}
}}
href={makeProfileLink(props)}
title={props.handle}
asAnchor
style={props.style}>
{props.children}
</Pressable>
</Link>
)
}
+53 -45
View File
@@ -11,6 +11,8 @@ import {Button} from '../forms/Button'
import {CenteredView} from '../Views'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {ViewHeader} from 'view/com/util/ViewHeader'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
export function ErrorScreen({
title,
@@ -18,66 +20,72 @@ export function ErrorScreen({
details,
onPressTryAgain,
testID,
showHeader,
}: {
title: string
message: string
details?: string
onPressTryAgain?: () => void
testID?: string
showHeader?: boolean
}) {
const theme = useTheme()
const {isMobile} = useWebMediaQueries()
const pal = usePalette('default')
const {_} = useLingui()
return (
<CenteredView testID={testID} style={[styles.outer, pal.view]}>
<View style={styles.errorIconContainer}>
<View
style={[
styles.errorIcon,
{backgroundColor: theme.palette.inverted.background},
]}>
<FontAwesomeIcon
icon="exclamation"
style={pal.textInverted as FontAwesomeIconStyle}
size={24}
/>
</View>
</View>
<Text type="title-lg" style={[styles.title, pal.text]}>
{title}
</Text>
<Text style={[styles.message, pal.text]}>{message}</Text>
{details && (
<Text
testID={`${testID}-details`}
style={[styles.details, pal.text, pal.viewLight]}>
{details}
</Text>
)}
{onPressTryAgain && (
<View style={styles.btnContainer}>
<Button
testID="errorScreenTryAgainButton"
type="default"
style={[styles.btn]}
onPress={onPressTryAgain}
accessibilityLabel={_(msg`Retry`)}
accessibilityHint={_(
msg`Retries the last action, which errored out`,
)}>
<>
{showHeader && isMobile && <ViewHeader title="Error" showBorder />}
<CenteredView testID={testID} style={[styles.outer, pal.view]}>
<View style={styles.errorIconContainer}>
<View
style={[
styles.errorIcon,
{backgroundColor: theme.palette.inverted.background},
]}>
<FontAwesomeIcon
icon="arrows-rotate"
style={pal.link as FontAwesomeIconStyle}
size={16}
icon="exclamation"
style={pal.textInverted as FontAwesomeIconStyle}
size={24}
/>
<Text type="button" style={[styles.btnText, pal.link]}>
<Trans context="action">Try again</Trans>
</Text>
</Button>
</View>
</View>
)}
</CenteredView>
<Text type="title-lg" style={[styles.title, pal.text]}>
{title}
</Text>
<Text style={[styles.message, pal.text]}>{message}</Text>
{details && (
<Text
testID={`${testID}-details`}
style={[styles.details, pal.text, pal.viewLight]}>
{details}
</Text>
)}
{onPressTryAgain && (
<View style={styles.btnContainer}>
<Button
testID="errorScreenTryAgainButton"
type="default"
style={[styles.btn]}
onPress={onPressTryAgain}
accessibilityLabel={_(msg`Retry`)}
accessibilityHint={_(
msg`Retries the last action, which errored out`,
)}>
<FontAwesomeIcon
icon="arrows-rotate"
style={pal.link as FontAwesomeIconStyle}
size={16}
/>
<Text type="button" style={[styles.btnText, pal.link]}>
<Trans context="action">Try again</Trans>
</Text>
</Button>
</View>
)}
</CenteredView>
</>
)
}
+44 -15
View File
@@ -9,6 +9,7 @@ import {
import {
AppBskyFeedDefs,
AppBskyFeedPost,
AtUri,
RichText as RichTextAPI,
} from '@atproto/api'
import {Text} from '../text/Text'
@@ -30,6 +31,10 @@ import {Shadow} from '#/state/cache/types'
import {useRequireAuth} from '#/state/session'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as ArrowOutOfBox} from '#/components/icons/ArrowOutOfBox'
import {toShareUrl} from 'lib/strings/url-helpers'
import {shareUrl} from 'lib/sharing'
import {makeProfileLink} from 'lib/routes/links'
let PostCtrls = ({
big,
@@ -116,11 +121,18 @@ let PostCtrls = ({
closeModal,
])
const onShare = useCallback(() => {
const urip = new AtUri(post.uri)
const href = makeProfileLink(post.author, 'post', urip.rkey)
const url = toShareUrl(href)
shareUrl(url)
}, [post.uri, post.author])
return (
<View style={[styles.ctrls, style]}>
<View
style={[
styles.ctrl,
big ? styles.ctrlBig : styles.ctrl,
post.viewer?.replyDisabled ? {opacity: 0.5} : undefined,
]}>
<TouchableOpacity
@@ -149,7 +161,7 @@ let PostCtrls = ({
) : undefined}
</TouchableOpacity>
</View>
<View style={styles.ctrl}>
<View style={big ? styles.ctrlBig : styles.ctrl}>
<RepostButton
big={big}
isReposted={!!post.viewer?.repost}
@@ -158,7 +170,7 @@ let PostCtrls = ({
onQuote={onQuote}
/>
</View>
<View style={styles.ctrl}>
<View style={big ? styles.ctrlBig : styles.ctrl}>
<TouchableOpacity
testID="likeBtn"
style={[styles.btn, !big && styles.btnPad]}
@@ -193,20 +205,34 @@ let PostCtrls = ({
) : undefined}
</TouchableOpacity>
</View>
{big ? undefined : (
<View style={styles.ctrl}>
<PostDropdownBtn
testID="postDropdownBtn"
postAuthor={post.author}
postCid={post.cid}
postUri={post.uri}
record={record}
richText={richText}
showAppealLabelItem={showAppealLabelItem}
style={styles.btnPad}
/>
{big && (
<View style={styles.ctrlBig}>
<TouchableOpacity
testID="likeBtn"
style={[styles.btn]}
onPress={onShare}
accessibilityRole="button"
accessibilityLabel={`${
post.viewer?.like ? _(msg`Unlike`) : _(msg`Like`)
} (${post.likeCount} ${pluralize(post.likeCount || 0, 'like')})`}
accessibilityHint=""
hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
<ArrowOutOfBox style={[defaultCtrlColor, styles.mt1]} width={22} />
</TouchableOpacity>
</View>
)}
<View style={big ? styles.ctrlBig : styles.ctrl}>
<PostDropdownBtn
testID="postDropdownBtn"
postAuthor={post.author}
postCid={post.cid}
postUri={post.uri}
record={record}
richText={richText}
showAppealLabelItem={showAppealLabelItem}
style={styles.btnPad}
/>
</View>
</View>
)
}
@@ -223,6 +249,9 @@ const styles = StyleSheet.create({
flex: 1,
alignItems: 'flex-start',
},
ctrlBig: {
alignItems: 'center',
},
btn: {
flexDirection: 'row',
alignItems: 'center',
+2 -2
View File
@@ -37,8 +37,8 @@ export const CommunityGuidelinesScreen = (_props: Props) => {
The Community Guidelines have been moved to{' '}
<TextLink
style={pal.link}
href="https://blueskyweb.xyz/support/community-guidelines"
text="blueskyweb.xyz/support/community-guidelines"
href="https://bsky.social/about/support/community-guidelines"
text="bsky.social/about/support/community-guidelines"
/>
</Trans>
</Text>
+2 -2
View File
@@ -34,8 +34,8 @@ export const CopyrightPolicyScreen = (_props: Props) => {
The Copyright Policy has been moved to{' '}
<TextLink
style={pal.link}
href="https://blueskyweb.xyz/support/community-guidelines"
text="blueskyweb.xyz/support/community-guidelines"
href="https://bsky.social/about/support/copyright"
text="bsky.social/about/support/copyright"
/>
</Trans>
</Text>
+18 -13
View File
@@ -7,7 +7,7 @@ import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState'
import {FollowingEndOfFeed} from 'view/com/posts/FollowingEndOfFeed'
import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState'
import {FeedsTabBar} from '../com/pager/FeedsTabBar'
import {Pager, RenderTabBarFnProps} from 'view/com/pager/Pager'
import {Pager, RenderTabBarFnProps, PagerRef} from 'view/com/pager/Pager'
import {FeedPage} from 'view/com/feeds/FeedPage'
import {HomeLoggedOutCTA} from '../com/auth/HomeLoggedOutCTA'
import {useSetMinimalShellMode, useSetDrawerSwipeDisabled} from '#/state/shell'
@@ -16,25 +16,19 @@ import {usePinnedFeedsInfos, FeedSourceInfo} from '#/state/queries/feed'
import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
import {emitSoftReset} from '#/state/events'
import {useSession} from '#/state/session'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import * as persisted from '#/state/persisted'
import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed'
type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
export function HomeScreen(props: Props) {
const {data: preferences} = usePreferencesQuery()
const {feeds: pinnedFeedInfos, isLoading: isPinnedFeedsLoading} =
usePinnedFeedsInfos()
const {isDesktop} = useWebMediaQueries()
const [rawInitialFeed] = React.useState<string>(
() => persisted.get('lastSelectedHomeFeed') ?? 'home',
)
if (preferences && pinnedFeedInfos && !isPinnedFeedsLoading) {
return (
<HomeScreenReady
{...props}
preferences={preferences}
pinnedFeedInfos={pinnedFeedInfos}
rawInitialFeed={isDesktop ? 'home' : rawInitialFeed}
/>
)
} else {
@@ -49,11 +43,9 @@ export function HomeScreen(props: Props) {
function HomeScreenReady({
preferences,
pinnedFeedInfos,
rawInitialFeed,
}: Props & {
preferences: UsePreferencesQueryResponse
pinnedFeedInfos: FeedSourceInfo[]
rawInitialFeed: string
}) {
const allFeeds = React.useMemo(() => {
const feeds: FeedDescriptor[] = []
@@ -68,12 +60,24 @@ function HomeScreenReady({
return feeds
}, [pinnedFeedInfos])
const [rawSelectedFeed, setSelectedFeed] =
React.useState<string>(rawInitialFeed)
const rawSelectedFeed = useSelectedFeed()
const setSelectedFeed = useSetSelectedFeed()
const maybeFoundIndex = allFeeds.indexOf(rawSelectedFeed as FeedDescriptor)
const selectedIndex = Math.max(0, maybeFoundIndex)
const selectedFeed = allFeeds[selectedIndex]
const pagerRef = React.useRef<PagerRef>(null)
const lastPagerReportedIndexRef = React.useRef(selectedIndex)
React.useLayoutEffect(() => {
// Since the pager is not a controlled component, adjust it imperatively
// if the selected index gets out of sync with what it last reported.
// This is supposed to only happen on the web when you use the right nav.
if (selectedIndex !== lastPagerReportedIndexRef.current) {
lastPagerReportedIndexRef.current = selectedIndex
pagerRef.current?.setPage(selectedIndex)
}
}, [selectedIndex])
const {hasSession} = useSession()
const setMinimalShellMode = useSetMinimalShellMode()
const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
@@ -93,7 +97,7 @@ function HomeScreenReady({
setDrawerSwipeDisabled(index > 0)
const feed = allFeeds[index]
setSelectedFeed(feed)
persisted.write('lastSelectedHomeFeed', feed)
lastPagerReportedIndexRef.current = index
},
[setDrawerSwipeDisabled, setSelectedFeed, setMinimalShellMode, allFeeds],
)
@@ -147,6 +151,7 @@ function HomeScreenReady({
return hasSession ? (
<Pager
key={allFeeds.join(',')}
ref={pagerRef}
testID="homeScreen"
initialPage={selectedIndex}
onPageSelected={onPageSelected}
+12
View File
@@ -29,6 +29,9 @@ import {listenSoftReset, emitSoftReset} from '#/state/events'
import {truncateAndInvalidate} from '#/state/queries/util'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {isNative} from '#/platform/detection'
import {FAB} from '../com/util/fab/FAB'
import {ComposeIcon2} from 'lib/icons'
import {useComposerControls} from '#/state/shell/composer'
type Props = NativeStackScreenProps<
NotificationsTabNavigatorParams,
@@ -47,6 +50,7 @@ export function NotificationsScreen({}: Props) {
const unreadApi = useUnreadNotificationsApi()
const hasNew = !!unreadNotifs
const isScreenFocused = useIsFocused()
const {openComposer} = useComposerControls()
// event handlers
// =
@@ -156,6 +160,14 @@ export function NotificationsScreen({}: Props) {
showIndicator={hasNew}
/>
)}
<FAB
testID="composeFAB"
onPress={() => openComposer({})}
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
accessibilityRole="button"
accessibilityLabel={_(msg`New post`)}
accessibilityHint=""
/>
</View>
)
}
+2 -2
View File
@@ -34,8 +34,8 @@ export const PrivacyPolicyScreen = (_props: Props) => {
The Privacy Policy has been moved to{' '}
<TextLink
style={pal.link}
href="https://blueskyweb.xyz/support/privacy-policy"
text="blueskyweb.xyz/support/privacy-policy"
href="https://bsky.social/about/support/privacy-policy"
text="bsky.social/about/support/privacy-policy"
/>
</Trans>
</Text>
+75 -28
View File
@@ -1,7 +1,12 @@
import React, {useMemo} from 'react'
import {StyleSheet, View} from 'react-native'
import {useFocusEffect} from '@react-navigation/native'
import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
import {
AppBskyActorDefs,
moderateProfile,
ModerationOpts,
RichText as RichTextAPI,
} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
@@ -11,7 +16,7 @@ import {ScreenHider} from 'view/com/util/moderation/ScreenHider'
import {Feed} from 'view/com/posts/Feed'
import {ProfileLists} from '../com/lists/ProfileLists'
import {ProfileFeedgens} from '../com/feeds/ProfileFeedgens'
import {ProfileHeader} from '../com/profile/ProfileHeader'
import {ProfileHeader, ProfileHeaderLoading} from '../com/profile/ProfileHeader'
import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
import {ErrorScreen} from '../com/util/error/ErrorScreen'
import {EmptyState} from '../com/util/EmptyState'
@@ -28,7 +33,7 @@ import {
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {useProfileQuery} from '#/state/queries/profile'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {useSession} from '#/state/session'
import {useSession, getAgent} from '#/state/session'
import {useModerationOpts} from '#/state/queries/preferences'
import {useProfileExtraInfoQuery} from '#/state/queries/profile-extra-info'
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
@@ -50,6 +55,7 @@ interface SectionRef {
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Profile'>
export function ProfileScreen({route}: Props) {
const {_} = useLingui()
const {currentAccount} = useSession()
const name =
route.params.name === 'me' ? currentAccount?.did : route.params.name
@@ -65,6 +71,7 @@ export function ProfileScreen({route}: Props) {
error: profileError,
refetch: refetchProfile,
isLoading: isLoadingProfile,
isPlaceholderData: isPlaceholderProfile,
} = useProfileQuery({
did: resolvedDid,
})
@@ -84,27 +91,23 @@ export function ProfileScreen({route}: Props) {
}
}, [profile?.viewer?.blockedBy, resolvedDid])
if (isLoadingDid || isLoadingProfile || !moderationOpts) {
// Most pushes will happen here, since we will have only placeholder data
if (isLoadingDid || isLoadingProfile) {
return (
<CenteredView>
<ProfileHeader
profile={null}
moderation={null}
isProfilePreview={true}
/>
<ProfileHeaderLoading />
</CenteredView>
)
}
if (resolveError || profileError) {
return (
<CenteredView>
<ErrorScreen
testID="profileErrorScreen"
title="Oops!"
message={cleanError(resolveError || profileError)}
onPressTryAgain={onPressTryAgain}
/>
</CenteredView>
<ErrorScreen
testID="profileErrorScreen"
title={profileError ? _(msg`Not Found`) : _(msg`Oops!`)}
message={cleanError(resolveError || profileError)}
onPressTryAgain={onPressTryAgain}
showHeader
/>
)
}
if (profile && moderationOpts) {
@@ -112,31 +115,33 @@ export function ProfileScreen({route}: Props) {
<ProfileScreenLoaded
profile={profile}
moderationOpts={moderationOpts}
isPlaceholderProfile={isPlaceholderProfile}
hideBackButton={!!route.params.hideBackButton}
/>
)
}
// should never happen
return (
<CenteredView>
<ErrorScreen
testID="profileErrorScreen"
title="Oops!"
message="Something went wrong and we're not sure what."
onPressTryAgain={onPressTryAgain}
/>
</CenteredView>
<ErrorScreen
testID="profileErrorScreen"
title="Oops!"
message="Something went wrong and we're not sure what."
onPressTryAgain={onPressTryAgain}
showHeader
/>
)
}
function ProfileScreenLoaded({
profile: profileUnshadowed,
isPlaceholderProfile,
moderationOpts,
hideBackButton,
}: {
profile: AppBskyActorDefs.ProfileViewDetailed
moderationOpts: ModerationOpts
hideBackButton: boolean
isPlaceholderProfile: boolean
}) {
const profile = useProfileShadow(profileUnshadowed)
const {hasSession, currentAccount} = useSession()
@@ -156,6 +161,10 @@ function ProfileScreenLoaded({
useSetTitle(combinedDisplayName(profile))
const description = profile.description ?? ''
const hasDescription = description !== ''
const [descriptionRT, isResolvingDescriptionRT] = useRichText(description)
const showPlaceholder = isPlaceholderProfile || isResolvingDescriptionRT
const moderation = useMemo(
() => moderateProfile(profile, moderationOpts),
[profile, moderationOpts],
@@ -269,11 +278,20 @@ function ProfileScreenLoaded({
return (
<ProfileHeader
profile={profile}
moderation={moderation}
descriptionRT={hasDescription ? descriptionRT : null}
moderationOpts={moderationOpts}
hideBackButton={hideBackButton}
isPlaceholderProfile={showPlaceholder}
/>
)
}, [profile, moderation, hideBackButton])
}, [
profile,
descriptionRT,
hasDescription,
moderationOpts,
hideBackButton,
showPlaceholder,
])
return (
<ScreenHider
@@ -283,7 +301,7 @@ function ProfileScreenLoaded({
moderation={moderation.account}>
<PagerWithHeader
testID="profilePager"
isHeaderReady={true}
isHeaderReady={!showPlaceholder}
items={sectionTitles}
onPageSelected={onPageSelected}
onCurrentPageSelected={onCurrentPageSelected}
@@ -440,6 +458,35 @@ function ProfileEndOfFeed() {
)
}
function useRichText(text: string): [RichTextAPI, boolean] {
const [prevText, setPrevText] = React.useState(text)
const [rawRT, setRawRT] = React.useState(() => new RichTextAPI({text}))
const [resolvedRT, setResolvedRT] = React.useState<RichTextAPI | null>(null)
if (text !== prevText) {
setPrevText(text)
setRawRT(new RichTextAPI({text}))
setResolvedRT(null)
// This will queue an immediate re-render
}
React.useEffect(() => {
let ignore = false
async function resolveRTFacets() {
// new each time
const resolvedRT = new RichTextAPI({text})
await resolvedRT.detectFacets(getAgent())
if (!ignore) {
setResolvedRT(resolvedRT)
}
}
resolveRTFacets()
return () => {
ignore = true
}
}, [text])
const isResolving = resolvedRT === null
return [resolvedRT ?? rawRT, isResolving]
}
const styles = StyleSheet.create({
container: {
flexDirection: 'column',
+2 -2
View File
@@ -863,13 +863,13 @@ export function SettingsScreen({}: Props) {
<TextLink
type="md"
style={pal.link}
href="https://blueskyweb.xyz/support/tos"
href="https://bsky.social/about/support/tos"
text={_(msg`Terms of Service`)}
/>
<TextLink
type="md"
style={pal.link}
href="https://blueskyweb.xyz/support/privacy-policy"
href="https://bsky.social/about/support/privacy-policy"
text={_(msg`Privacy Policy`)}
/>
</View>
+4 -4
View File
@@ -14,19 +14,19 @@ export function Links() {
<View style={[a.gap_md, a.align_start]}>
<InlineLink
to="https://blueskyweb.xyz"
to="https://bsky.social"
warnOnMismatchingTextChild
style={[a.text_md]}>
External
</InlineLink>
<InlineLink to="https://blueskyweb.xyz" style={[a.text_md]}>
<InlineLink to="https://bsky.social" style={[a.text_md]}>
<H3>External with custom children</H3>
</InlineLink>
<InlineLink
to="https://blueskyweb.xyz"
to="https://bsky.social"
warnOnMismatchingTextChild
style={[a.text_lg]}>
https://blueskyweb.xyz
https://bsky.social
</InlineLink>
<InlineLink
to="https://bsky.app/profile/bsky.app"
+10 -10
View File
@@ -14,22 +14,22 @@ export function Theming() {
<Text style={[a.font_bold, a.pt_xl, a.px_md]}>theme.atoms.text</Text>
<View style={[a.flex_1, t.atoms.border, a.border_t]} />
<Text style={[a.font_bold, t.atoms.text_contrast_600, a.px_md]}>
theme.atoms.text_contrast_600
<View style={[a.flex_1, t.atoms.border_contrast_high, a.border_t]} />
<Text style={[a.font_bold, t.atoms.text_contrast_high, a.px_md]}>
theme.atoms.text_contrast_high
</Text>
<View style={[a.flex_1, t.atoms.border, a.border_t]} />
<Text style={[a.font_bold, t.atoms.text_contrast_500, a.px_md]}>
theme.atoms.text_contrast_500
<View style={[a.flex_1, t.atoms.border_contrast_medium, a.border_t]} />
<Text style={[a.font_bold, t.atoms.text_contrast_medium, a.px_md]}>
theme.atoms.text_contrast_medium
</Text>
<View style={[a.flex_1, t.atoms.border, a.border_t]} />
<Text style={[a.font_bold, t.atoms.text_contrast_400, a.px_md]}>
theme.atoms.text_contrast_400
<View style={[a.flex_1, t.atoms.border_contrast_low, a.border_t]} />
<Text style={[a.font_bold, t.atoms.text_contrast_low, a.px_md]}>
theme.atoms.text_contrast_low
</Text>
<View style={[a.flex_1, t.atoms.border_contrast, a.border_t]} />
<View style={[a.flex_1, t.atoms.border_contrast_low, a.border_t]} />
<View style={[a.w_full, a.gap_md]}>
<View style={[t.atoms.bg, a.justify_center, a.p_md]}>
+3 -11
View File
@@ -2,20 +2,12 @@ import React from 'react'
import {View} from 'react-native'
import {atoms as a} from '#/alf'
import {Text, H1, H2, H3, H4, H5, H6, P} from '#/components/Typography'
import {Text} from '#/components/Typography'
import {RichText} from '#/components/RichText'
export function Typography() {
return (
<View style={[a.gap_md]}>
<H1>H1 Heading</H1>
<H2>H2 Heading</H2>
<H3>H3 Heading</H3>
<H4>H4 Heading</H4>
<H5>H5 Heading</H5>
<H6>H6 Heading</H6>
<P>P Paragraph</P>
<Text style={[a.text_5xl]}>atoms.text_5xl</Text>
<Text style={[a.text_4xl]}>atoms.text_4xl</Text>
<Text style={[a.text_3xl]}>atoms.text_3xl</Text>
@@ -29,11 +21,11 @@ export function Typography() {
<RichText
resolveFacets
value={`This is rich text. It can have mentions like @bsky.app or links like https://blueskyweb.xyz`}
value={`This is rich text. It can have mentions like @bsky.app or links like https://bsky.social`}
/>
<RichText
resolveFacets
value={`This is rich text. It can have mentions like @bsky.app or links like https://blueskyweb.xyz`}
value={`This is rich text. It can have mentions like @bsky.app or links like https://bsky.social`}
style={[a.text_xl]}
/>
</View>
+2 -2
View File
@@ -33,8 +33,8 @@ export const TermsOfServiceScreen = (_props: Props) => {
<Trans>The Terms of Service have been moved to</Trans>{' '}
<TextLink
style={pal.link}
href="https://blueskyweb.xyz/support/tos"
text="blueskyweb.xyz/support/tos"
href="https://bsky.social/about/support/tos"
text="bsky.social/about/support/tos"
/>
</Text>
</View>
+2 -2
View File
@@ -255,13 +255,13 @@ let DrawerContent = ({}: {}): React.ReactNode => {
<TextLink
type="md"
style={pal.link}
href="https://blueskyweb.xyz/support/tos"
href="https://bsky.social/about/support/tos"
text={_(msg`Terms of Service`)}
/>
<TextLink
type="md"
style={pal.link}
href="https://blueskyweb.xyz/support/privacy-policy"
href="https://bsky.social/about/support/privacy-policy"
text={_(msg`Privacy Policy`)}
/>
</View>
+40 -27
View File
@@ -1,18 +1,24 @@
import React from 'react'
import {View, StyleSheet} from 'react-native'
import {useNavigationState} from '@react-navigation/native'
import {useNavigationState, useNavigation} from '@react-navigation/native'
import {usePalette} from 'lib/hooks/usePalette'
import {TextLink} from 'view/com/util/Link'
import {getCurrentRoute} from 'lib/routes/helpers'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
import {usePinnedFeedsInfos} from '#/state/queries/feed'
import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed'
import {FeedDescriptor} from '#/state/queries/post-feed'
import {NavigationProp} from 'lib/routes/types'
import {emitSoftReset} from '#/state/events'
export function DesktopFeeds() {
const pal = usePalette('default')
const {_} = useLingui()
const {feeds} = usePinnedFeedsInfos()
const {feeds: pinnedFeedInfos} = usePinnedFeedsInfos()
const selectedFeed = useSelectedFeed()
const setSelectedFeed = useSetSelectedFeed()
const navigation = useNavigation<NavigationProp>()
const route = useNavigationState(state => {
if (!state) {
return {name: 'Home'}
@@ -22,30 +28,34 @@ export function DesktopFeeds() {
return (
<View style={[styles.container, pal.view]}>
<FeedItem href="/" title="Following" current={route.name === 'Home'} />
{feeds
.filter(f => f.displayName !== 'Following')
.map(feed => {
try {
const params = route.params as Record<string, string>
const routeName =
feed.type === 'feed' ? 'ProfileFeed' : 'ProfileList'
return (
<FeedItem
key={feed.uri}
href={feed.route.href}
title={feed.displayName}
current={
route.name === routeName &&
params.name === feed.route.params.name &&
params.rkey === feed.route.params.rkey
}
/>
)
} catch {
return null
}
})}
{pinnedFeedInfos.map(feedInfo => {
const uri = feedInfo.uri
let feed: FeedDescriptor
if (!uri) {
feed = 'home'
} else if (uri.includes('app.bsky.feed.generator')) {
feed = `feedgen|${uri}`
} else if (uri.includes('app.bsky.graph.list')) {
feed = `list|${uri}`
} else {
return null
}
return (
<FeedItem
key={feed}
href={'/?' + new URLSearchParams([['feed', feed]])}
title={feedInfo.displayName}
current={route.name === 'Home' && feed === selectedFeed}
onPress={() => {
setSelectedFeed(feed)
navigation.navigate('Home')
if (feed === selectedFeed) {
emitSoftReset()
}
}}
/>
)
})}
<View style={{paddingTop: 8, paddingBottom: 6}}>
<TextLink
type="lg"
@@ -62,10 +72,12 @@ function FeedItem({
title,
href,
current,
onPress,
}: {
title: string
href: string
current: boolean
onPress: () => void
}) {
const pal = usePalette('default')
return (
@@ -74,6 +86,7 @@ function FeedItem({
type="xl"
href={href}
text={title}
onPress={onPress}
style={[
current ? pal.text : pal.textLight,
{letterSpacing: 0.15, fontWeight: current ? '500' : 'normal'},
+1 -1
View File
@@ -334,7 +334,7 @@ export function DesktopLeftNav() {
}
iconFilled={
<HashtagIcon
strokeWidth={2.5}
strokeWidth={4}
style={pal.text as FontAwesomeIconStyle}
size={isDesktop ? 24 : 28}
/>
+2 -2
View File
@@ -80,7 +80,7 @@ export function DesktopRightNav({routeName}: {routeName: string}) {
<TextLink
type="md"
style={pal.link}
href="https://blueskyweb.xyz/support/privacy-policy"
href="https://bsky.social/about/support/privacy-policy"
text={_(msg`Privacy`)}
/>
<Text type="md" style={pal.textLight}>
@@ -89,7 +89,7 @@ export function DesktopRightNav({routeName}: {routeName: string}) {
<TextLink
type="md"
style={pal.link}
href="https://blueskyweb.xyz/support/tos"
href="https://bsky.social/about/support/tos"
text={_(msg`Terms`)}
/>
<Text type="md" style={pal.textLight}>
+4 -5
View File
@@ -1,8 +1,7 @@
<!DOCTYPE html>
<html lang="%LANG_ISO_CODE%">
<html>
<head>
<meta charset="utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta charset="utf-8">
<!--
This viewport works for phones with notches.
It's optimized for gestures by disabling global zoom.
@@ -234,8 +233,8 @@
width: 80%;
"
>
<p>Oh no! It looks like JavaScript is not enabled in your browser.</p>
<p style="margin: 20px 0;">
<p lang="en">Oh no! It looks like JavaScript is not enabled in your browser.</p>
<p lang="en" style="margin: 20px 0;">
<button
type="submit"
style="