diff --git a/.env.example b/.env.example index 96a1548d66..e90541cabb 100644 --- a/.env.example +++ b/.env.example @@ -34,5 +34,8 @@ EXPO_PUBLIC_SENTRY_DSN= # Bitdrift API key. If undefined, Bitdrift will be disabled. EXPO_PUBLIC_BITDRIFT_API_KEY= -# bapp-config web worker URL -BAPP_CONFIG_DEV_URL= +# geolocation web worker URL +GEOLOCATION_DEV_URL= + +# live-events web worker URL +LIVE_EVENTS_DEV_URL= diff --git a/.eslintrc.js b/.eslintrc.js index 37ed895aa4..16e844a050 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -35,6 +35,7 @@ module.exports = { 'Admonition', 'Admonition.Admonition', 'Toast.Action', + 'toast.Action', 'AgeAssuranceAdmonition', 'Span', 'StackedButton', diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 0000000000..247217fe2d --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,54 @@ +name: Claude Code + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + actions: read # Required for Claude to read CI results on PRs + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + + # This is an optional setting that allows Claude to read CI results on PRs + additional_permissions: | + actions: read + + # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. + # prompt: 'Update the pull request description to include a summary of changes.' + + # Optional: Add claude_args to customize behavior and configuration + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options + # claude_args: '--allowed-tools Bash(gh pr:*)' + + # NOTE(sfn): we can add a custom system prompt here + + claude_args: | + --model claude-opus-4-5-20251101 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..e1a7d174ed --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,591 @@ +# CLAUDE.md - Bluesky Social App Development Guide + +This document provides guidance for working effectively in the Bluesky Social app codebase. + +## Project Overview + +Bluesky Social is a cross-platform social media application built with React Native and Expo. It runs on iOS, Android, and Web, connecting to the AT Protocol (atproto) decentralized social network. + +**Tech Stack:** +- React Native 0.81 with Expo 54 +- TypeScript +- React Navigation for routing +- TanStack Query (React Query) for data fetching +- Lingui for internationalization +- Custom design system called ALF (Application Layout Framework) + +## Essential Commands + +```bash +# Development +yarn start # Start Expo dev server +yarn web # Start web version +yarn android # Run on Android +yarn ios # Run on iOS + +# Testing & Quality +yarn test # Run Jest tests +yarn lint # Run ESLint +yarn typecheck # Run TypeScript type checking + +# Internationalization +yarn intl:extract # Extract translation strings +yarn intl:compile # Compile translations for runtime + +# Build +yarn build-web # Build web version +yarn prebuild # Generate native projects +``` + +## Project Structure + +``` +src/ +├── alf/ # Design system (ALF) - themes, atoms, tokens +├── components/ # Shared UI components (Button, Dialog, Menu, etc.) +├── screens/ # Full-page screen components (newer pattern) +├── view/ +│ ├── screens/ # Full-page screens (legacy location) +│ ├── com/ # Reusable view components +│ └── shell/ # App shell (navigation bars, tabs) +├── state/ +│ ├── queries/ # TanStack Query hooks +│ ├── preferences/ # User preferences (React Context) +│ ├── session/ # Authentication state +│ └── persisted/ # Persistent storage layer +├── lib/ # Utilities, constants, helpers +├── locale/ # i18n configuration and language files +└── Navigation.tsx # Main navigation configuration +``` + +## Styling System (ALF) + +ALF is the custom design system. It uses Tailwind-inspired naming with underscores instead of hyphens. + +### Basic Usage + +```tsx +import {atoms as a, useTheme} from '#/alf' + +function MyComponent() { + const t = useTheme() + + return ( + + + Hello + + + ) +} +``` + +### Key Concepts + +**Static Atoms** - Theme-independent styles imported from `atoms`: +```tsx +import {atoms as a} from '#/alf' +// a.flex_row, a.p_md, a.gap_sm, a.rounded_md, a.text_lg, etc. +``` + +**Theme Atoms** - Theme-dependent colors from `useTheme()`: +```tsx +const t = useTheme() +// t.atoms.bg, t.atoms.text, t.atoms.border_contrast_low, etc. +// t.palette.primary_500, t.palette.negative_400, etc. +``` + +**Platform Utilities** - For platform-specific styles: +```tsx +import {web, native, ios, android, platform} from '#/alf' + +const styles = [ + a.p_md, + web({cursor: 'pointer'}), + native({paddingBottom: 20}), + platform({ios: {...}, android: {...}, web: {...}}), +] +``` + +**Breakpoints** - Responsive design: +```tsx +import {useBreakpoints} from '#/alf' + +const {gtPhone, gtMobile, gtTablet} = useBreakpoints() +if (gtMobile) { + // Tablet or desktop layout +} +``` + +### Naming Conventions + +- Spacing: `xxs`, `xs`, `sm`, `md`, `lg`, `xl`, `xxl` (t-shirt sizes) +- Text: `text_xs`, `text_sm`, `text_md`, `text_lg`, `text_xl` +- Gaps/Padding: `gap_sm`, `p_md`, `px_lg`, `py_xl` +- Flex: `flex_row`, `flex_1`, `align_center`, `justify_between` +- Borders: `border`, `border_t`, `rounded_md`, `rounded_full` + +## Component Patterns + +### Dialog Component + +Dialogs use a bottom sheet on native and a modal on web. Use `useDialogControl()` hook to manage state. + +```tsx +import * as Dialog from '#/components/Dialog' + +function MyFeature() { + const control = Dialog.useDialogControl() + + return ( + <> + + + + {/* Native drag handle */} + + + Title + + + Dialog content here + + + + + + ) +} +``` + +### Menu Component + +Menus render as a dropdown on web and a bottom sheet dialog on native. + +```tsx +import * as Menu from '#/components/Menu' + +function MyMenu() { + return ( + + + {({props}) => ( + + )} + + + + + + + Edit + + + + Delete + + + + + ) +} +``` + +### Button Component + +```tsx +import {Button, ButtonText, ButtonIcon} from '#/components/Button' + +// Solid primary button (most common) + + +// With icon + + +// Icon-only button + + +// Ghost variant (deprecated - use color prop) + +``` + +**Button Props:** +- `color`: `'primary'` | `'secondary'` | `'negative'` | `'primary_subtle'` | `'negative_subtle'` +- `size`: `'tiny'` | `'small'` | `'large'` +- `shape`: `'default'` (pill) | `'round'` | `'square'` | `'rectangular'` +- `variant`: `'solid'` | `'outline'` | `'ghost'` (deprecated, use `color`) + +### Typography + +```tsx +import {Text, H1, H2, P} from '#/components/Typography' + +

Heading

+

Paragraph text with default styling.

+Custom text + +// For text with emoji, add the emoji prop +Hello! 👋 +``` + +### TextField + +```tsx +import * as TextField from '#/components/forms/TextField' + +Email + + + + +``` + +## Internationalization (i18n) + +All user-facing strings must be wrapped for translation using Lingui. + +```tsx +import {msg, Trans, plural} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +function MyComponent() { + const {_} = useLingui() + + // Simple strings - use msg() with _() function + const title = _(msg`Settings`) + const errorMessage = _(msg`Something went wrong`) + + // Strings with variables + const greeting = _(msg`Hello, ${name}!`) + + // Pluralization + const countLabel = _(plural(count, { + one: '# item', + other: '# items', + })) + + // JSX content - use Trans component + return ( + + Welcome to Bluesky + + ) +} +``` + +**Commands:** +```bash +yarn intl:extract # Extract new strings to locale files +yarn intl:compile # Compile for runtime (required after changes) +``` + +## State Management + +### TanStack Query (Data Fetching) + +```tsx +// src/state/queries/profile.ts +import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query' + +// Query key pattern +const RQKEY_ROOT = 'profile' +export const RQKEY = (did: string) => [RQKEY_ROOT, did] + +// Query hook +export function useProfileQuery({did}: {did: string}) { + const agent = useAgent() + + return useQuery({ + queryKey: RQKEY(did), + queryFn: async () => { + const res = await agent.getProfile({actor: did}) + return res.data + }, + staleTime: STALE.MINUTES.FIVE, + enabled: !!did, + }) +} + +// Mutation hook +export function useUpdateProfile() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async (data) => { + // Update logic + }, + onSuccess: (_, variables) => { + queryClient.invalidateQueries({queryKey: RQKEY(variables.did)}) + }, + }) +} +``` + +**Stale Time Constants** (from `src/state/queries/index.ts`): +```tsx +STALE.SECONDS.FIFTEEN // 15 seconds +STALE.MINUTES.ONE // 1 minute +STALE.MINUTES.FIVE // 5 minutes +STALE.HOURS.ONE // 1 hour +STALE.INFINITY // Never stale +``` + +### Preferences (React Context) + +```tsx +// Simple boolean preference pattern +import {useAutoplayDisabled, useSetAutoplayDisabled} from '#/state/preferences' + +function SettingsScreen() { + const autoplayDisabled = useAutoplayDisabled() + const setAutoplayDisabled = useSetAutoplayDisabled() + + return ( + + ) +} +``` + +### Session State + +```tsx +import {useSession, useAgent} from '#/state/session' + +function MyComponent() { + const {hasSession, currentAccount} = useSession() + const agent = useAgent() + + if (!hasSession) { + return + } + + // Use agent for API calls + const response = await agent.getProfile({actor: currentAccount.did}) +} +``` + +## Navigation + +Navigation uses React Navigation with type-safe route parameters. + +```tsx +// Screen component +import {type NativeStackScreenProps} from '@react-navigation/native-stack' +import {type CommonNavigatorParams} from '#/lib/routes/types' + +type Props = NativeStackScreenProps + +export function ProfileScreen({route, navigation}: Props) { + const {name} = route.params // Type-safe params + + return ( + + {/* Screen content */} + + ) +} + +// Programmatic navigation +import {useNavigation} from '@react-navigation/native' + +const navigation = useNavigation() +navigation.navigate('Profile', {name: 'alice.bsky.social'}) + +// Or use the navigate helper +import {navigate} from '#/Navigation' +navigate('Profile', {name: 'alice.bsky.social'}) +``` + +## Platform-Specific Code + +Use file extensions for platform-specific implementations: + +``` +Component.tsx # Shared/default +Component.web.tsx # Web-only +Component.native.tsx # iOS + Android +Component.ios.tsx # iOS-only +Component.android.tsx # Android-only +``` + +Example from Dialog: +- `src/components/Dialog/index.tsx` - Native (uses BottomSheet) +- `src/components/Dialog/index.web.tsx` - Web (uses modal with Radix primitives) + +Platform detection: +```tsx +import {isWeb, isNative, isIOS, isAndroid} from '#/platform/detection' + +if (isNative) { + // Native-specific logic +} +``` + +## Import Aliases + +Always use the `#/` alias for absolute imports: + +```tsx +// Good +import {useSession} from '#/state/session' +import {atoms as a, useTheme} from '#/alf' +import {Button} from '#/components/Button' + +// Avoid +import {useSession} from '../../../state/session' +``` + +## Footguns + +Common pitfalls to avoid in this codebase: + +### Dialog Close Callback (Critical) + +**Always use `control.close(() => ...)` when performing actions after closing a dialog.** The callback ensures the action runs after the dialog's close animation completes. Failing to do this causes race conditions with React state updates. + +```tsx +// WRONG - causes bugs with state updates, navigation, opening other dialogs +const onConfirm = () => { + control.close() + navigation.navigate('Home') // May race with dialog animation +} + +// WRONG - same problem +const onConfirm = () => { + control.close() + otherDialogControl.open() // Will likely fail or cause visual glitches +} + +// CORRECT - action runs after dialog fully closes +const onConfirm = () => { + control.close(() => { + navigation.navigate('Home') + }) +} + +// CORRECT - opening another dialog after close +const onConfirm = () => { + control.close(() => { + otherDialogControl.open() + }) +} + +// CORRECT - state updates after close +const onConfirm = () => { + control.close(() => { + setSomeState(newValue) + onCallback?.() + }) +} +``` + +This applies to: +- Navigation (`navigation.navigate()`, `navigation.push()`) +- Opening other dialogs or menus +- State updates that affect UI (`setState`, `queryClient.invalidateQueries`) +- Callbacks passed from parent components + +The Menu component on iOS specifically uses this pattern - see `src/components/Menu/index.tsx:151`. + +### Controlled vs Uncontrolled Inputs + +Prefer `defaultValue` over `value` for TextInput on the old architecture: + +```tsx +// Preferred - uncontrolled + + +// Avoid when possible - controlled (can cause performance issues) + +``` + +### Platform-Specific Behavior + +Some components behave differently across platforms: +- `Dialog.Handle` - Only renders on native (drag handle for bottom sheet) +- `Dialog.Close` - Only renders on web (X button) +- `Menu.Divider` - Only renders on web +- `Menu.ContainerItem` - Only works on native + +Always test on multiple platforms when using these components. + +### React Compiler is Enabled + +This codebase uses React Compiler, so **don't proactively add `useMemo` or `useCallback`**. The compiler handles memoization automatically. + +```tsx +// UNNECESSARY - React Compiler handles this +const handlePress = useCallback(() => { + doSomething() +}, [doSomething]) + +// JUST WRITE THIS +const handlePress = () => { + doSomething() +} +``` + +Only use `useMemo`/`useCallback` when you have a specific reason, such as: +- The value is immediately used in an effect's dependency array +- You're passing a callback to a non-React library that needs referential stability + +## Best Practices + +1. **Accessibility**: Always provide `label` prop for interactive elements, use `accessibilityHint` where helpful + +2. **Translations**: Wrap ALL user-facing strings with `msg()` or `` + +3. **Styling**: Combine static atoms with theme atoms, use platform utilities for platform-specific styles + +4. **State**: Use TanStack Query for server state, React Context for UI preferences + +5. **Components**: Check if a component exists in `#/components/` before creating new ones + +6. **Types**: Define explicit types for props, use `NativeStackScreenProps` for screens + +7. **Testing**: Components should have `testID` props for E2E testing + +## Key Files Reference + +| Purpose | Location | +|---------|----------| +| Theme definitions | `src/alf/themes.ts` | +| Design tokens | `src/alf/tokens.ts` | +| Static atoms | `src/alf/atoms.ts` (extends `@bsky.app/alf`) | +| Navigation config | `src/Navigation.tsx` | +| Route definitions | `src/routes.ts` | +| Route types | `src/lib/routes/types.ts` | +| Query hooks | `src/state/queries/*.ts` | +| Session state | `src/state/session/index.tsx` | +| i18n setup | `src/locale/i18n.ts` | diff --git a/conductor.json b/conductor.json new file mode 100644 index 0000000000..ef5c740b55 --- /dev/null +++ b/conductor.json @@ -0,0 +1,6 @@ +{ + "scripts": { + "setup": "yarn install", + "run": "yarn web --port $CONDUCTOR_PORT" + } +} \ No newline at end of file diff --git a/package.json b/package.json index 185e530bd3..32b78e18fb 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "icons:optimize": "svgo -f ./assets/icons" }, "dependencies": { - "@atproto/api": "^0.18.13", + "@atproto/api": "^0.18.15", "@bitdrift/react-native": "^0.6.8", "@braintree/sanitize-url": "^6.0.2", "@bsky.app/alf": "^0.1.6", @@ -284,6 +284,7 @@ "@react-native/babel-preset": "0.81.5", "@react-native/normalize-colors": "0.81.5", "**/@expo/image-utils": "0.8.7", + "**/@react-native-async-storage/async-storage": "2.2.0", "**/expo-constants": "18.0.8", "**/expo-device": "7.1.4", "**/zod": "3.23.8", diff --git a/patches/expo-font+14.0.10.patch b/patches/expo-font+14.0.10.patch new file mode 100644 index 0000000000..ac7526e23d --- /dev/null +++ b/patches/expo-font+14.0.10.patch @@ -0,0 +1,16 @@ +diff --git a/node_modules/expo-font/ios/FontLoaderModule.swift b/node_modules/expo-font/ios/FontLoaderModule.swift +index 183480f..7b64f6e 100644 +--- a/node_modules/expo-font/ios/FontLoaderModule.swift ++++ b/node_modules/expo-font/ios/FontLoaderModule.swift +@@ -2,10 +2,9 @@ import ExpoModulesCore + + public final class FontLoaderModule: Module { + // could be a Set, but to be able to pass to JS we keep it as an array +- private var registeredFonts: [String] ++ private lazy var registeredFonts: [String] = queryCustomNativeFonts() + + public required init(appContext: AppContext) { +- self.registeredFonts = queryCustomNativeFonts() + super.init(appContext: appContext) + } + diff --git a/src/App.native.tsx b/src/App.native.tsx index 34c3cc204c..2c4d6fa413 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -69,6 +69,10 @@ import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbe import {ToastOutlet} from '#/components/Toast' import {Provider as AgeAssuranceV2Provider} from '#/ageAssurance' import {prefetchAgeAssuranceConfig} from '#/ageAssurance' +import { + prefetchLiveEvents, + Provider as LiveEventsProvider, +} from '#/features/liveEvents/context' import * as Geo from '#/geolocation' import {Splash} from '#/Splash' import {BottomSheetProvider} from '../modules/bottom-sheet' @@ -92,6 +96,7 @@ if (isAndroid) { */ Geo.resolve() prefetchAgeAssuranceConfig() +prefetchLiveEvents() function InnerApp() { const [isReady, setIsReady] = React.useState(false) @@ -141,49 +146,51 @@ function InnerApp() { - - - - {/* LabelDefsProvider MUST come before ModerationOptsProvider */} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + {/* LabelDefsProvider MUST come before ModerationOptsProvider */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App.web.tsx b/src/App.web.tsx index 956c52005c..460d9ff174 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -57,6 +57,10 @@ import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbe import {ToastOutlet} from '#/components/Toast' import {Provider as AgeAssuranceV2Provider} from '#/ageAssurance' import {prefetchAgeAssuranceConfig} from '#/ageAssurance' +import { + prefetchLiveEvents, + Provider as LiveEventsProvider, +} from '#/features/liveEvents/context' import * as Geo from '#/geolocation' import {Splash} from '#/Splash' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' @@ -67,6 +71,7 @@ import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottom */ Geo.resolve() prefetchAgeAssuranceConfig() +prefetchLiveEvents() function InnerApp() { const [isReady, setIsReady] = React.useState(false) @@ -117,45 +122,47 @@ function InnerApp() { - - - - {/* LabelDefsProvider MUST come before ModerationOptsProvider */} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + {/* LabelDefsProvider MUST come before ModerationOptsProvider */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/alf/index.tsx b/src/alf/index.tsx index eed3fcbeb2..3aff9ddb57 100644 --- a/src/alf/index.tsx +++ b/src/alf/index.tsx @@ -11,7 +11,12 @@ import { import {themes} from '#/alf/themes' import {type Device} from '#/storage' -export {type TextStyleProp, type Theme, type ViewStyleProp} from '@bsky.app/alf' +export { + type TextStyleProp, + type Theme, + utils, + type ViewStyleProp, +} from '@bsky.app/alf' export {atoms} from '#/alf/atoms' export * from '#/alf/breakpoints' export * from '#/alf/fonts' diff --git a/src/components/FeedCard.tsx b/src/components/FeedCard.tsx index c5e6200527..1fcedc6916 100644 --- a/src/components/FeedCard.tsx +++ b/src/components/FeedCard.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, {useMemo} from 'react' import {type GestureResponderEvent, View} from 'react-native' import { type AppBskyFeedDefs, @@ -21,19 +21,21 @@ import { import {useSession} from '#/state/session' import * as Toast from '#/view/com/util/Toast' import {UserAvatar} from '#/view/com/util/UserAvatar' -import {atoms as a, useTheme} from '#/alf' +import {atoms as a, select, useTheme} from '#/alf' import { Button, ButtonIcon, type ButtonProps, ButtonText, } from '#/components/Button' +import {Live_Stroke2_Corner0_Rounded as LiveIcon} from '#/components/icons/Live' import {Pin_Stroke2_Corner0_Rounded as PinIcon} from '#/components/icons/Pin' import {Link as InternalLink, type LinkProps} from '#/components/Link' import {Loader} from '#/components/Loader' import * as Prompt from '#/components/Prompt' import {RichText, type RichTextProps} from '#/components/RichText' import {Text} from '#/components/Typography' +import {useActiveLiveEventFeedUris} from '#/features/liveEvents/context' import type * as bsky from '#/types/bsky' import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from './icons/Trash' @@ -49,7 +51,11 @@ export function Default(props: Props) {
- +
@@ -118,14 +124,40 @@ export function AvatarPlaceholder({size = 40}: Omit) { export function TitleAndByline({ title, creator, + uri, }: { title: string creator?: bsky.profile.AnyProfileView + uri?: string }) { const t = useTheme() + const activeLiveEvents = useActiveLiveEventFeedUris() + const liveColor = useMemo( + () => + select(t.name, { + dark: t.palette.negative_600, + dim: t.palette.negative_600, + light: t.palette.negative_500, + }), + [t], + ) return ( + {uri && activeLiveEvents.has(uri) && ( + + + + Happening now + + + )} + ) diff --git a/src/components/Post/Embed/FeedEmbed.tsx b/src/components/Post/Embed/FeedEmbed.tsx index 26ff9b5298..a726c313e1 100644 --- a/src/components/Post/Embed/FeedEmbed.tsx +++ b/src/components/Post/Embed/FeedEmbed.tsx @@ -17,16 +17,16 @@ export function FeedEmbed({ return ( + style={[a.border, t.atoms.border_contrast_low, a.p_sm, a.rounded_md]}> - + - ) diff --git a/src/components/PostControls/BookmarkButton.tsx b/src/components/PostControls/BookmarkButton.tsx index f729515202..ac391a1ae9 100644 --- a/src/components/PostControls/BookmarkButton.tsx +++ b/src/components/PostControls/BookmarkButton.tsx @@ -8,6 +8,7 @@ import type React from 'react' import {useCleanError} from '#/lib/hooks/useCleanError' import {logger} from '#/logger' import {type Shadow} from '#/state/cache/post-shadow' +import {useFeedFeedbackContext} from '#/state/feed-feedback' import {useBookmarkMutation} from '#/state/queries/bookmarks/useBookmarkMutation' import {useRequireAuth} from '#/state/session' import {useTheme} from '#/alf' @@ -32,6 +33,7 @@ export const BookmarkButton = memo(function BookmarkButton({ const {mutateAsync: bookmark} = useBookmarkMutation() const cleanError = useCleanError() const requireAuth = useRequireAuth() + const {feedDescriptor} = useFeedFeedbackContext() const {viewer} = post const isBookmarked = !!viewer?.bookmarked @@ -50,7 +52,12 @@ export const BookmarkButton = memo(function BookmarkButton({ post, }) - logger.metric('post:bookmark', {logContext}) + logger.metric('post:bookmark', { + uri: post.uri, + authorDid: post.author.did, + logContext, + feedDescriptor, + }) toast.show( @@ -85,7 +92,12 @@ export const BookmarkButton = memo(function BookmarkButton({ uri: post.uri, }) - logger.metric('post:unbookmark', {logContext}) + logger.metric('post:unbookmark', { + uri: post.uri, + authorDid: post.author.did, + logContext, + feedDescriptor, + }) toast.show( diff --git a/src/components/PostControls/PostMenu/PostMenuItems.tsx b/src/components/PostControls/PostMenu/PostMenuItems.tsx index f8b410be97..ce93e81ab1 100644 --- a/src/components/PostControls/PostMenu/PostMenuItems.tsx +++ b/src/components/PostControls/PostMenu/PostMenuItems.tsx @@ -98,6 +98,7 @@ let PostMenuItems = ({ richText, threadgateRecord, onShowLess, + logContext, }: { testID: string post: Shadow @@ -111,6 +112,7 @@ let PostMenuItems = ({ timestamp: string threadgateRecord?: AppBskyFeedThreadgate.Record onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' }): React.ReactNode => { const {hasSession, currentAccount} = useSession() const {_} = useLingui() @@ -210,9 +212,21 @@ let PostMenuItems = ({ try { if (isThreadMuted) { unmuteThread() + logger.metric('post:unmute', { + uri: postUri, + authorDid: postAuthor.did, + logContext, + feedDescriptor: feedFeedback.feedDescriptor, + }) Toast.show(_(msg`You will now receive notifications for this thread`)) } else { muteThread() + logger.metric('post:mute', { + uri: postUri, + authorDid: postAuthor.did, + logContext, + feedDescriptor: feedFeedback.feedDescriptor, + }) Toast.show( _(msg`You will no longer receive notifications for this thread`), ) @@ -272,6 +286,12 @@ let PostMenuItems = ({ feedContext: postFeedContext, reqId: postReqId, }) + logger.metric('post:showMore', { + uri: postUri, + authorDid: postAuthor.did, + logContext, + feedDescriptor: feedFeedback.feedDescriptor, + }) Toast.show( _(msg({message: 'Feedback sent to feed operator', context: 'toast'})), ) @@ -284,6 +304,12 @@ let PostMenuItems = ({ feedContext: postFeedContext, reqId: postReqId, }) + logger.metric('post:showLess', { + uri: postUri, + authorDid: postAuthor.did, + logContext, + feedDescriptor: feedFeedback.feedDescriptor, + }) if (onShowLess) { onShowLess({ item: postUri, diff --git a/src/components/PostControls/PostMenu/index.tsx b/src/components/PostControls/PostMenu/index.tsx index 950bc4f6d9..f418587a5c 100644 --- a/src/components/PostControls/PostMenu/index.tsx +++ b/src/components/PostControls/PostMenu/index.tsx @@ -29,6 +29,7 @@ let PostMenuButton = ({ threadgateRecord, onShowLess, hitSlop, + logContext, }: { testID: string post: Shadow @@ -41,6 +42,7 @@ let PostMenuButton = ({ threadgateRecord?: AppBskyFeedThreadgate.Record onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void hitSlop?: Insets + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' }): React.ReactNode => { const {_} = useLingui() @@ -87,6 +89,7 @@ let PostMenuButton = ({ timestamp={timestamp} threadgateRecord={threadgateRecord} onShowLess={onShowLess} + logContext={logContext} /> )} diff --git a/src/components/PostControls/ShareMenu/index.tsx b/src/components/PostControls/ShareMenu/index.tsx index 6127ca41db..3fd9583113 100644 --- a/src/components/PostControls/ShareMenu/index.tsx +++ b/src/components/PostControls/ShareMenu/index.tsx @@ -16,6 +16,7 @@ import {useGate} from '#/lib/statsig/statsig' import {toShareUrl} from '#/lib/strings/url-helpers' import {logger} from '#/logger' import {type Shadow} from '#/state/cache/post-shadow' +import {useFeedFeedbackContext} from '#/state/feed-feedback' import {EventStopper} from '#/view/com/util/EventStopper' import {native} from '#/alf' import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ArrowOutOfBoxIcon} from '#/components/icons/ArrowOutOfBox' @@ -35,6 +36,7 @@ let ShareMenuButton = ({ threadgateRecord, onShare, hitSlop, + logContext, }: { testID: string post: Shadow @@ -45,9 +47,11 @@ let ShareMenuButton = ({ threadgateRecord?: AppBskyFeedThreadgate.Record onShare: () => void hitSlop?: Insets + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' }): React.ReactNode => { const {_} = useLingui() const gate = useGate() + const {feedDescriptor} = useFeedFeedbackContext() const ShareIcon = gate('alt_share_icon') ? ArrowShareRightIcon @@ -65,13 +69,27 @@ let ShareMenuButton = ({ setTimeout(menuControl.open) logger.metric( - 'share:open', - {context: big ? 'thread' : 'feed'}, + 'post:share', + { + uri: post.uri, + authorDid: post.author.did, + logContext, + feedDescriptor, + postContext: big ? 'thread' : 'feed', + }, {statsig: true}, ) }, }), - [menuControl, setHasBeenOpen, big], + [ + menuControl, + setHasBeenOpen, + big, + logContext, + feedDescriptor, + post.uri, + post.author.did, + ], ) const onNativeLongPress = () => { diff --git a/src/components/PostControls/index.tsx b/src/components/PostControls/index.tsx index ec0016815f..45688c2617 100644 --- a/src/components/PostControls/index.tsx +++ b/src/components/PostControls/index.tsx @@ -13,6 +13,7 @@ import {CountWheel} from '#/lib/custom-animations/CountWheel' import {AnimatedLikeIcon} from '#/lib/custom-animations/LikeIcon' import {useHaptics} from '#/lib/haptics' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' +import {logger} from '#/logger' import {type Shadow} from '#/state/cache/types' import {useFeedFeedbackContext} from '#/state/feed-feedback' import { @@ -174,6 +175,12 @@ let PostControls = ({ feedContext, reqId, }) + logger.metric('post:clickQuotePost', { + uri: post.uri, + authorDid: post.author.did, + logContext, + feedDescriptor, + }) openComposer({ quote: post, onPost: onPostReply, @@ -217,7 +224,16 @@ let PostControls = ({ testID="replyBtn" onPress={ !replyDisabled - ? () => requireAuth(() => onPressReply()) + ? () => + requireAuth(() => { + logger.metric('post:clickReply', { + uri: post.uri, + authorDid: post.author.did, + logContext, + feedDescriptor, + }) + onPressReply() + }) : undefined } label={_( @@ -315,6 +331,7 @@ let PostControls = ({ left: secondaryControlSpacingStyles.gap / 2, right: secondaryControlSpacingStyles.gap / 2, }} + logContext={logContext} /> diff --git a/src/components/ProgressGuide/FollowDialog.tsx b/src/components/ProgressGuide/FollowDialog.tsx index bf567091b3..ca91665e94 100644 --- a/src/components/ProgressGuide/FollowDialog.tsx +++ b/src/components/ProgressGuide/FollowDialog.tsx @@ -31,8 +31,8 @@ import { import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {useInteractionState} from '#/components/hooks/useInteractionState' +import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon} from '#/components/icons/Arrow' import {MagnifyingGlass_Stroke2_Corner0_Rounded as SearchIcon} from '#/components/icons/MagnifyingGlass' -import {PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon} from '#/components/icons/Person' import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' import {boostInterests, InterestTabs} from '#/components/InterestTabs' import * as ProfileCard from '#/components/ProfileCard' @@ -60,10 +60,16 @@ type Item = key: string } -export function FollowDialog({guide}: {guide: Follow10ProgressGuide}) { +export function FollowDialog({ + guide, + showArrow, +}: { + guide: Follow10ProgressGuide + showArrow?: boolean +}) { const {_} = useLingui() const control = Dialog.useDialogControl() - const {gtMobile} = useBreakpoints() + const {gtPhone} = useBreakpoints() const {height: minHeight} = useWindowDimensions() return ( @@ -74,13 +80,12 @@ export function FollowDialog({guide}: {guide: Follow10ProgressGuide}) { control.open() logEvent('progressGuide:followDialog:open', {}) }} - size={gtMobile ? 'small' : 'large'} - color="primary" - variant="solid"> - + size={gtPhone ? 'small' : 'large'} + color="primary"> Find people to follow + {showArrow && } diff --git a/src/components/ProgressGuide/List.tsx b/src/components/ProgressGuide/List.tsx index cae307a6dd..81abc586f3 100644 --- a/src/components/ProgressGuide/List.tsx +++ b/src/components/ProgressGuide/List.tsx @@ -2,37 +2,62 @@ import {type StyleProp, View, type ViewStyle} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useProfileFollowsQuery} from '#/state/queries/profile-follows' +import {useSession} from '#/state/session' import { useProgressGuide, useProgressGuideControls, } from '#/state/shell/progress-guide' -import {atoms as a, useTheme} from '#/alf' +import {UserAvatar} from '#/view/com/util/UserAvatar' +import {atoms as a, useBreakpoints, useLayoutBreakpoints, useTheme} from '#/alf' import {Button, ButtonIcon} from '#/components/Button' +import {Person_Stroke2_Corner2_Rounded as PersonIcon} from '#/components/icons/Person' import {TimesLarge_Stroke2_Corner0_Rounded as Times} from '#/components/icons/Times' import {Text} from '#/components/Typography' +import type * as bsky from '#/types/bsky' import {FollowDialog} from './FollowDialog' import {ProgressGuideTask} from './Task' +const TOTAL_AVATARS = 10 + export function ProgressGuideList({style}: {style?: StyleProp}) { const t = useTheme() const {_} = useLingui() + const {gtPhone} = useBreakpoints() + const {rightNavVisible} = useLayoutBreakpoints() + const {currentAccount} = useSession() const followProgressGuide = useProgressGuide('follow-10') const followAndLikeProgressGuide = useProgressGuide('like-10-and-follow-7') const guide = followProgressGuide || followAndLikeProgressGuide const {endProgressGuide} = useProgressGuideControls() + const {data: follows} = useProfileFollowsQuery(currentAccount?.did, { + limit: TOTAL_AVATARS, + }) + + const actualFollowsCount = follows?.pages?.[0]?.follows?.length ?? 0 + + // Hide if user already follows 10+ people + if (guide?.guide === 'follow-10' && actualFollowsCount >= TOTAL_AVATARS) { + return null + } + + // Inline layout when left nav visible but no right sidebar (800-1100px) + const inlineLayout = gtPhone && !rightNavVisible if (guide) { return ( - + - - Getting started + + Follow 10 people to get started {guide.guide === 'follow-10' && ( - <> - - - + + + + )} {guide.guide === 'like-10-and-follow-7' && ( <> @@ -76,3 +109,73 @@ export function ProgressGuideList({style}: {style?: StyleProp}) { } return null } + +function StackedAvatars({follows}: {follows?: bsky.profile.AnyProfileView[]}) { + const t = useTheme() + const {centerColumnOffset} = useLayoutBreakpoints() + + // Smaller avatars for narrower viewport + const avatarSize = centerColumnOffset ? 30 : 37 + const overlap = centerColumnOffset ? 9 : 11 + const iconSize = centerColumnOffset ? 14 : 18 + + // Use actual follows count, not the guide's event counter + const followedAvatars = follows?.slice(0, TOTAL_AVATARS) ?? [] + const remainingSlots = TOTAL_AVATARS - followedAvatars.length + + // Total width calculation: first avatar + (remaining * visible portion) + const totalWidth = avatarSize + (TOTAL_AVATARS - 1) * (avatarSize - overlap) + + return ( + + {/* Show followed user avatars */} + {followedAvatars.map((follow, i) => ( + + + + ))} + {/* Show placeholder avatars for remaining slots */} + {Array(remainingSlots) + .fill(0) + .map((_, i) => ( + + + + ))} + + ) +} diff --git a/src/components/ProgressGuide/Task.tsx b/src/components/ProgressGuide/Task.tsx index 449a28fcd3..997c777af6 100644 --- a/src/components/ProgressGuide/Task.tsx +++ b/src/components/ProgressGuide/Task.tsx @@ -31,11 +31,11 @@ export function ProgressGuideTask({ size={20} thickness={3} borderWidth={0} - unfilledColor={t.palette.contrast_50} + unfilledColor={t.palette.contrast_100} /> )} - + [a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*))/i export type RichTextProps = TextStyleProp & Pick & { @@ -115,7 +118,8 @@ export function RichText({ , ) } else if (link && AppBskyRichtextFacet.validateLink(link).success) { - if (disableLinks) { + const isValidLink = URL_REGEX.test(link.uri) + if (!isValidLink || disableLinks) { els.push(toShortUrl(segment.text)) } else { els.push( diff --git a/src/components/TrendingTopics.tsx b/src/components/TrendingTopics.tsx index b2ee8140e9..1f18713be4 100644 --- a/src/components/TrendingTopics.tsx +++ b/src/components/TrendingTopics.tsx @@ -20,8 +20,12 @@ export function TrendingTopic({ topic: raw, size, style, -}: {topic: TrendingTopic; size?: 'large' | 'small'} & ViewStyleProp) { - const t = useTheme() + hovered, +}: { + topic: TrendingTopic + size?: 'large' | 'small' + hovered?: boolean +} & ViewStyleProp) { const topic = useTopic(raw) const isSmall = size === 'small' @@ -33,18 +37,14 @@ export function TrendingTopic({ style={[ a.flex_row, a.align_center, - a.rounded_full, - a.border, - t.atoms.border_contrast_medium, - t.atoms.bg, isSmall ? [ { - paddingVertical: 5, - paddingHorizontal: 10, + paddingVertical: 2, + paddingHorizontal: 4, }, ] - : [a.py_sm, a.px_md], + : [a.py_xs, a.px_sm], hasIcon && {gap: 6}, style, ]}> @@ -93,6 +93,7 @@ export function TrendingTopic({ a.font_semi_bold, a.leading_tight, isSmall ? [a.text_sm] : [a.text_md, {paddingBottom: 1}], + hovered && {textDecorationLine: 'underline'}, ]} numberOfLines={1}> {topic.displayName} diff --git a/src/components/interstitials/Trending.tsx b/src/components/interstitials/Trending.tsx index 2580ef28f3..830dfb3b66 100644 --- a/src/components/interstitials/Trending.tsx +++ b/src/components/interstitials/Trending.tsx @@ -41,7 +41,7 @@ export function Inner() { }, [setTrendingDisabled]) return error || noTopics ? null : ( - + {topic.topic} diff --git a/src/components/live/GoLiveDisabledDialog.tsx b/src/components/live/GoLiveDisabledDialog.tsx index 9a3c04286d..3d548bbb8f 100644 --- a/src/components/live/GoLiveDisabledDialog.tsx +++ b/src/components/live/GoLiveDisabledDialog.tsx @@ -46,6 +46,9 @@ export function DialogInner({ if (!agent.session?.did) { throw new Error('Not logged in') } + if (!status.uri || !status.cid) { + throw new Error('Status is missing uri or cid') + } if (__DEV__) { logger.info('Submitting go live appeal', { @@ -57,8 +60,8 @@ export function DialogInner({ reasonType: ToolsOzoneReportDefs.REASONAPPEAL, subject: { $type: 'com.atproto.repo.strongRef', - uri: status.uri!, - cid: status.cid!, + uri: status.uri, + cid: status.cid, }, reason: details, }, diff --git a/src/components/moderation/ReportDialog/const.ts b/src/components/moderation/ReportDialog/const.ts index 14ff397955..7e5075cce2 100644 --- a/src/components/moderation/ReportDialog/const.ts +++ b/src/components/moderation/ReportDialog/const.ts @@ -3,6 +3,8 @@ import { ToolsOzoneReportDefs as OzoneReportDefs, } from '@atproto/api' +import {type ParsedReportSubject} from '#/components/moderation/ReportDialog/types' + export const DMCA_LINK = 'https://bsky.social/about/support/copyright' export const SUPPORT_PAGE = 'https://bsky.social/about/support' @@ -112,3 +114,10 @@ export const BSKY_LABELER_ONLY_REPORT_REASONS: Set = OzoneReportDefs.REASONCHILDSAFETYOTHER, OzoneReportDefs.REASONVIOLENCEEXTREMISTCONTENT, ]) + +/** + * Set of _parsed_ subject types that should only be sent to Bluesky's + * moderation service. + */ +export const BSKY_LABELER_ONLY_SUBJECT_TYPES: Set = + new Set(['convoMessage', 'status']) diff --git a/src/components/moderation/ReportDialog/index.tsx b/src/components/moderation/ReportDialog/index.tsx index f4cea4856a..14354332cb 100644 --- a/src/components/moderation/ReportDialog/index.tsx +++ b/src/components/moderation/ReportDialog/index.tsx @@ -32,6 +32,7 @@ import {Text} from '#/components/Typography' import {useSubmitReportMutation} from './action' import { BSKY_LABELER_ONLY_REPORT_REASONS, + BSKY_LABELER_ONLY_SUBJECT_TYPES, NEW_TO_OLD_REASONS_MAP, SUPPORT_PAGE, } from './const' @@ -127,8 +128,10 @@ function Inner(props: ReportDialogProps) { const isBskyOnlyReason = state?.selectedOption?.reason ? BSKY_LABELER_ONLY_REPORT_REASONS.has(state.selectedOption.reason) : false - // some subjects (chats) only go to Bluesky - const isBskyOnlySubject = props.subject.type === 'convoMessage' + // some subjects ONLY go to Bluesky + const isBskyOnlySubject = BSKY_LABELER_ONLY_SUBJECT_TYPES.has( + props.subject.type, + ) /** * Labelers that support this `subject` and its NSID collection @@ -835,12 +838,7 @@ function LabelerCard({ {title} + style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}> By {sanitizeHandle(labeler.creator.handle, '@')} diff --git a/src/components/moderation/ReportDialog/utils/parseReportSubject.ts b/src/components/moderation/ReportDialog/utils/parseReportSubject.ts index 0c07decc4c..950536199c 100644 --- a/src/components/moderation/ReportDialog/utils/parseReportSubject.ts +++ b/src/components/moderation/ReportDialog/utils/parseReportSubject.ts @@ -34,10 +34,11 @@ export function parseReportSubject( nsid: 'app.bsky.actor.profile', } } else if (AppBskyActorDefs.isStatusView(subject)) { + if (!subject.uri || !subject.cid) return return { type: 'status', - uri: subject.uri!, - cid: subject.cid!, + uri: subject.uri, + cid: subject.cid, nsid: 'app.bsky.actor.status', } } else if (AppBskyGraphDefs.isListView(subject)) { diff --git a/src/env/common.ts b/src/env/common.ts index b0d75d403b..04e98c49cb 100644 --- a/src/env/common.ts +++ b/src/env/common.ts @@ -108,8 +108,18 @@ export const GCP_PROJECT_ID: number = * URLs for the app config web worker. Can be a * locally running server, see `env.example` for more. */ -export const BAPP_CONFIG_DEV_URL = process.env.BAPP_CONFIG_DEV_URL -export const BAPP_CONFIG_PROD_URL = `https://ip.bsky.app` -export const BAPP_CONFIG_URL = IS_DEV - ? (BAPP_CONFIG_DEV_URL ?? BAPP_CONFIG_PROD_URL) - : BAPP_CONFIG_PROD_URL +export const GEOLOCATION_DEV_URL = process.env.GEOLOCATION_DEV_URL +export const GEOLOCATION_PROD_URL = `https://ip.bsky.app` +export const GEOLOCATION_URL = IS_DEV + ? (GEOLOCATION_DEV_URL ?? GEOLOCATION_PROD_URL) + : GEOLOCATION_PROD_URL + +/** + * URLs for the live-event config web worker. Can be a + * locally running server, see `env.example` for more. + */ +export const LIVE_EVENTS_DEV_URL = process.env.LIVE_EVENTS_DEV_URL +export const LIVE_EVENTS_PROD_URL = `https://live-events.workers.bsky.app` +export const LIVE_EVENTS_URL = IS_DEV + ? (LIVE_EVENTS_DEV_URL ?? LIVE_EVENTS_PROD_URL) + : LIVE_EVENTS_PROD_URL diff --git a/src/features/liveEvents/components/DiscoverFeedLiveEventFeedsAndTrendingBanner.tsx b/src/features/liveEvents/components/DiscoverFeedLiveEventFeedsAndTrendingBanner.tsx new file mode 100644 index 0000000000..de8bc81e7a --- /dev/null +++ b/src/features/liveEvents/components/DiscoverFeedLiveEventFeedsAndTrendingBanner.tsx @@ -0,0 +1,89 @@ +import {View} from 'react-native' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {useTrendingSettings} from '#/state/preferences/trending' +import {atoms as a, useLayoutBreakpoints} from '#/alf' +import {Button} from '#/components/Button' +import {DotGrid_Stroke2_Corner0_Rounded as EllipsisIcon} from '#/components/icons/DotGrid' +import {TrendingInterstitial} from '#/components/interstitials/Trending' +import {LiveEventFeedCardWide} from '#/features/liveEvents/components/LiveEventFeedCardWide' +import { + LiveEventFeedOptionsMenu, + useDialogControl, +} from '#/features/liveEvents/components/LiveEventFeedOptionsMenu' +import {useUserPreferencedLiveEvents} from '#/features/liveEvents/context' +import {type LiveEventFeed} from '#/features/liveEvents/types' + +export function DiscoverFeedLiveEventFeedsAndTrendingBanner() { + const events = useUserPreferencedLiveEvents() + const {rightNavVisible} = useLayoutBreakpoints() + const {trendingDisabled} = useTrendingSettings() + + if (!events.feeds.length) { + if (!rightNavVisible && !trendingDisabled) { + // only show trending on mobile when live event banner is not shown + return + } else { + // no feed, no trending + return null + } + } + + // On desktop, we show in the sidebar + if (rightNavVisible) return null + + return events.feeds.map(feed => ) +} + +function Inner({feed}: {feed: LiveEventFeed}) { + const {_} = useLingui() + const optionsMenuControl = useDialogControl() + const layout = feed.layouts.wide + + return ( + <> + + + + + + + + + + + ) +} diff --git a/src/features/liveEvents/components/ExploreScreenLiveEventFeedsBanner.tsx b/src/features/liveEvents/components/ExploreScreenLiveEventFeedsBanner.tsx new file mode 100644 index 0000000000..bf80ed6a2e --- /dev/null +++ b/src/features/liveEvents/components/ExploreScreenLiveEventFeedsBanner.tsx @@ -0,0 +1,17 @@ +import {View} from 'react-native' + +import {atoms as a, useTheme} from '#/alf' +import {LiveEventFeedCardWide} from '#/features/liveEvents/components/LiveEventFeedCardWide' +import {useLiveEvents} from '#/features/liveEvents/context' + +export function ExploreScreenLiveEventFeedsBanner() { + const t = useTheme() + const events = useLiveEvents() + return events.feeds.map(feed => ( + + + + )) +} diff --git a/src/features/liveEvents/components/LiveEventFeedCardCompact.tsx b/src/features/liveEvents/components/LiveEventFeedCardCompact.tsx new file mode 100644 index 0000000000..69c24b2dc5 --- /dev/null +++ b/src/features/liveEvents/components/LiveEventFeedCardCompact.tsx @@ -0,0 +1,132 @@ +import {useEffect, useMemo} from 'react' +import {View} from 'react-native' +import {Image} from 'expo-image' +import {LinearGradient} from 'expo-linear-gradient' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {isBskyCustomFeedUrl} from '#/lib/strings/url-helpers' +import {logger} from '#/logger' +import {atoms as a, utils} from '#/alf' +import {Live_Stroke2_Corner0_Rounded as LiveIcon} from '#/components/icons/Live' +import {Link} from '#/components/Link' +import {Text} from '#/components/Typography' +import { + type LiveEventFeed, + type LiveEventFeedMetricContext, +} from '#/features/liveEvents/types' + +const roundedStyles = [a.rounded_md, a.curve_continuous] + +export function LiveEventFeedCardCompact({ + feed, + metricContext, +}: { + feed: LiveEventFeed + metricContext: LiveEventFeedMetricContext +}) { + const {_} = useLingui() + + const layout = feed.layouts.compact + const overlayColor = layout.overlayColor + const textColor = layout.textColor + const url = useMemo(() => { + // Validated in multiple places on the backend + if (isBskyCustomFeedUrl(feed.url)) { + return new URL(feed.url).pathname + } + return '/' + }, [feed.url]) + + useEffect(() => { + logger.metric('liveEvents:feedBanner:seen', { + feed: feed.url, + context: metricContext, + }) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + return ( + { + logger.metric('liveEvents:feedBanner:click', { + feed: feed.url, + context: metricContext, + }) + }}> + {({hovered, pressed}) => ( + + + + + + + + + + + + + {layout.title} + + + + + + )} + + ) +} diff --git a/src/features/liveEvents/components/LiveEventFeedCardWide.tsx b/src/features/liveEvents/components/LiveEventFeedCardWide.tsx new file mode 100644 index 0000000000..1ed3a49df2 --- /dev/null +++ b/src/features/liveEvents/components/LiveEventFeedCardWide.tsx @@ -0,0 +1,139 @@ +import {useEffect, useMemo} from 'react' +import {View} from 'react-native' +import {Image} from 'expo-image' +import {LinearGradient} from 'expo-linear-gradient' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {isBskyCustomFeedUrl} from '#/lib/strings/url-helpers' +import {logger} from '#/logger' +import {atoms as a, useBreakpoints, utils} from '#/alf' +import {Link} from '#/components/Link' +import {Text} from '#/components/Typography' +import { + type LiveEventFeed, + type LiveEventFeedMetricContext, +} from '#/features/liveEvents/types' + +const roundedStyles = [a.rounded_lg, a.curve_continuous] + +export function LiveEventFeedCardWide({ + feed, + metricContext, +}: { + feed: LiveEventFeed + metricContext: LiveEventFeedMetricContext +}) { + const {_} = useLingui() + const {gtPhone} = useBreakpoints() + + const layout = feed.layouts.wide + const overlayColor = layout.overlayColor + const textColor = layout.textColor + const url = useMemo(() => { + // Validated in multiple places on the backend + if (isBskyCustomFeedUrl(feed.url)) { + return new URL(feed.url).pathname + } + return '/' + }, [feed.url]) + + useEffect(() => { + logger.metric('liveEvents:feedBanner:seen', { + feed: feed.url, + context: metricContext, + }) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + return ( + { + logger.metric('liveEvents:feedBanner:click', { + feed: feed.url, + context: metricContext, + }) + }}> + {({hovered, pressed}) => ( + + + + + + + + + + + + {feed.preview ? ( + Preview + ) : ( + Happening now + )} + + + {layout.title} + + + + + + )} + + ) +} diff --git a/src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx b/src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx new file mode 100644 index 0000000000..b2f50840a9 --- /dev/null +++ b/src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx @@ -0,0 +1,171 @@ +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {useCleanError} from '#/lib/hooks/useCleanError' +import {isNative} from '#/platform/detection' +import {atoms as a, web} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {Loader} from '#/components/Loader' +import * as toast from '#/components/Toast' +import {Span, Text} from '#/components/Typography' +import {useUpdateLiveEventPreferences} from '#/features/liveEvents/preferences' +import { + type LiveEventFeed, + type LiveEventFeedMetricContext, +} from '#/features/liveEvents/types' + +export {useDialogControl} from '#/components/Dialog' + +export function LiveEventFeedOptionsMenu({ + control, + feed, + metricContext, +}: { + control: Dialog.DialogControlProps + feed: LiveEventFeed + metricContext: LiveEventFeedMetricContext +}) { + const {_} = useLingui() + return ( + + + + + + + + ) +} + +function Inner({ + control, + feed, + metricContext, +}: { + control: Dialog.DialogControlProps + feed: LiveEventFeed + metricContext: LiveEventFeedMetricContext +}) { + const {_} = useLingui() + const { + isPending, + mutate: update, + error: rawError, + variables, + } = useUpdateLiveEventPreferences({ + feed, + metricContext, + onUpdateSuccess({undoAction}) { + toast.show( + + + + Your live event preferences have been updated. + + {undoAction && ( + { + if (undoAction) { + update(undoAction) + } + }}> + Undo + + )} + , + { + type: 'success', + }, + ) + + /* + * If there is no `undoAction`, it means that the action was already + * undone, and therefore the menu would have been closed prior to the + * undo happening. + */ + if (undoAction) { + control.close() + } + }, + }) + const cleanError = useCleanError() + const error = rawError ? cleanError(rawError) : undefined + + const isHidingFeed = variables?.type === 'hideFeed' && isPending + const isHidingAllFeeds = variables?.type === 'toggleHideAllFeeds' && isPending + + return ( + + + + Live event options + + + + + Live events appear occasionally when something exciting is + happening. If you'd like, you can hide this particular event, or all + events for this placement in your app interface. + + + + + + If you choose to hide all events, you can always re-enable them from{' '} + Settings → Content & Media. + + + + + + + + {isNative && ( + + )} + + + {error && ( + + {error.clean || error.raw || _(msg`An unknown error occurred.`)} + + )} + + ) +} diff --git a/src/features/liveEvents/components/LiveEventFeedsSettingsToggle.tsx b/src/features/liveEvents/components/LiveEventFeedsSettingsToggle.tsx new file mode 100644 index 0000000000..35d4039ce1 --- /dev/null +++ b/src/features/liveEvents/components/LiveEventFeedsSettingsToggle.tsx @@ -0,0 +1,43 @@ +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import * as SettingsList from '#/screens/Settings/components/SettingsList' +import * as Toggle from '#/components/forms/Toggle' +import {Live_Stroke2_Corner0_Rounded as LiveIcon} from '#/components/icons/Live' +import { + useLiveEventPreferences, + useUpdateLiveEventPreferences, +} from '#/features/liveEvents/preferences' + +export function LiveEventFeedsSettingsToggle() { + const {_} = useLingui() + const {data: prefs} = useLiveEventPreferences() + const { + isPending, + data: updatedPrefs, + mutate: update, + } = useUpdateLiveEventPreferences({ + metricContext: 'settings', + }) + const hideAllFeeds = !!(updatedPrefs || prefs)?.hideAllFeeds + + return ( + { + if (!isPending) { + update({type: 'toggleHideAllFeeds'}) + } + }}> + + + + Show live events in your Discover Feed + + + + + ) +} diff --git a/src/features/liveEvents/components/SidebarLiveEventFeedsBanner.tsx b/src/features/liveEvents/components/SidebarLiveEventFeedsBanner.tsx new file mode 100644 index 0000000000..25a5db8c8c --- /dev/null +++ b/src/features/liveEvents/components/SidebarLiveEventFeedsBanner.tsx @@ -0,0 +1,13 @@ +import {LiveEventFeedCardCompact} from '#/features/liveEvents/components/LiveEventFeedCardCompact' +import {useLiveEvents} from '#/features/liveEvents/context' + +export function SidebarLiveEventFeedsBanner() { + const events = useLiveEvents() + return events.feeds.map(feed => ( + + )) +} diff --git a/src/features/liveEvents/context.tsx b/src/features/liveEvents/context.tsx new file mode 100644 index 0000000000..fa2aba2260 --- /dev/null +++ b/src/features/liveEvents/context.tsx @@ -0,0 +1,110 @@ +import {createContext, useContext, useMemo} from 'react' +import {QueryClient, useQuery} from '@tanstack/react-query' + +import {useIsBskyTeam} from '#/lib/hooks/useIsBskyTeam' +import { + convertBskyAppUrlIfNeeded, + isBskyCustomFeedUrl, + makeRecordUri, +} from '#/lib/strings/url-helpers' +import {IS_DEV, LIVE_EVENTS_URL} from '#/env' +import {useLiveEventPreferences} from '#/features/liveEvents/preferences' +import {type LiveEventsWorkerResponse} from '#/features/liveEvents/types' +import {useDevMode} from '#/storage/hooks/dev-mode' + +const qc = new QueryClient() +const liveEventsQueryKey = ['live-events'] + +export const DEFAULT_LIVE_EVENTS = { + feeds: [], +} + +async function fetchLiveEvents(): Promise { + try { + const res = await fetch(`${LIVE_EVENTS_URL}/config`) + if (!res.ok) return null + const data = await res.json() + return data + } catch { + return null + } +} + +const Context = createContext(DEFAULT_LIVE_EVENTS) + +export function Provider({children}: React.PropsWithChildren<{}>) { + const [isDevMode] = useDevMode() + const isBskyTeam = useIsBskyTeam() + const {data} = useQuery( + { + staleTime: IS_DEV ? 5e3 : 1000 * 60, + queryKey: liveEventsQueryKey, + async queryFn() { + return fetchLiveEvents() + }, + }, + qc, + ) + + const ctx = useMemo(() => { + if (!data) return DEFAULT_LIVE_EVENTS + const feeds = data.feeds.filter(f => { + if (f.preview && !isBskyTeam) return false + return true + }) + return { + ...data, + // only one at a time for now, unless bsky team and dev mode + feeds: isBskyTeam && isDevMode ? feeds : feeds.slice(0, 1), + } + }, [data, isBskyTeam, isDevMode]) + + return {children} +} + +export async function prefetchLiveEvents() { + const data = await fetchLiveEvents() + if (data) { + qc.setQueryData(liveEventsQueryKey, data) + } +} + +export function useLiveEvents() { + const ctx = useContext(Context) + if (!ctx) { + throw new Error('useLiveEventsContext must be used within a Provider') + } + return ctx +} + +export function useUserPreferencedLiveEvents() { + const events = useLiveEvents() + const {data, isLoading} = useLiveEventPreferences() + if (isLoading) return DEFAULT_LIVE_EVENTS + const {hideAllFeeds, hiddenFeedIds} = data + return { + ...events, + feeds: hideAllFeeds + ? [] + : events.feeds.filter(f => { + const hidden = f?.id ? hiddenFeedIds.includes(f?.id || '') : false + return !hidden + }), + } +} + +export function useActiveLiveEventFeedUris() { + const {feeds} = useLiveEvents() + + return new Set( + feeds + // insurance + .filter(f => isBskyCustomFeedUrl(f.url)) + .map(f => { + const uri = convertBskyAppUrlIfNeeded(f.url) + const [_0, did, _1, rkey] = uri.split('/').filter(Boolean) + const urip = makeRecordUri(did, 'app.bsky.feed.generator', rkey) + return urip.toString() + }), + ) +} diff --git a/src/features/liveEvents/preferences.ts b/src/features/liveEvents/preferences.ts new file mode 100644 index 0000000000..2f7af094a8 --- /dev/null +++ b/src/features/liveEvents/preferences.ts @@ -0,0 +1,161 @@ +import {useEffect} from 'react' +import {type Agent, AppBskyActorDefs, asPredicate} from '@atproto/api' +import {useMutation, useQueryClient} from '@tanstack/react-query' + +import {logger} from '#/logger' +import {isWeb} from '#/platform/detection' +import { + preferencesQueryKey, + usePreferencesQuery, +} from '#/state/queries/preferences' +import {useAgent} from '#/state/session' +import * as env from '#/env' +import { + type LiveEventFeed, + type LiveEventFeedMetricContext, +} from '#/features/liveEvents/types' + +export type LiveEventPreferencesAction = Parameters< + Agent['updateLiveEventPreferences'] +>[0] & { + /** + * Flag that is internal to this hook, do not set when updating prefs + */ + __canUndo?: boolean +} + +export function useLiveEventPreferences() { + const query = usePreferencesQuery() + useWebOnlyDebugLiveEventPreferences() + return { + ...query, + data: query.data?.liveEventPreferences || { + hideAllFeeds: false, + hiddenFeedIds: [], + }, + } +} + +function useWebOnlyDebugLiveEventPreferences() { + const queryClient = useQueryClient() + const agent = useAgent() + + useEffect(() => { + if (env.IS_DEV && isWeb && typeof window !== 'undefined') { + // @ts-ignore + window.__updateLiveEventPreferences = async ( + action: LiveEventPreferencesAction, + ) => { + await agent.updateLiveEventPreferences(action) + // triggers a refetch + await queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + }) + } + } + }, [agent, queryClient]) +} + +export function useUpdateLiveEventPreferences(props: { + feed?: LiveEventFeed + metricContext: LiveEventFeedMetricContext + onUpdateSuccess?: (props: { + undoAction: LiveEventPreferencesAction | null + }) => void +}) { + const queryClient = useQueryClient() + const agent = useAgent() + + return useMutation< + AppBskyActorDefs.LiveEventPreferences, + Error, + LiveEventPreferencesAction, + {undoAction: LiveEventPreferencesAction | null} + >({ + onSettled(data, error, variables) { + /* + * `onSettled` runs after the mutation completes, success or no. The idea + * here is that we want to invert the action that was just passed in, and + * provide it as an `undoAction` to the `onUpdateSuccess` callback. + * + * If the operation was not a success, we don't provide the `undoAction`. + * + * Upon the first call of the mutation, the `__canUndo` flag is undefined, + * so we allow the undo. However, when we create the `undoAction`, we + * set its `__canUndo` flag to false, so that if the user were to call + * the undo action, we would not provide another undo for that. + */ + const canUndo = variables.__canUndo === undefined ? true : false + let undoAction: LiveEventPreferencesAction | null = null + + switch (variables.type) { + case 'hideFeed': + undoAction = {type: 'unhideFeed', id: variables.id, __canUndo: false} + break + case 'unhideFeed': + undoAction = {type: 'hideFeed', id: variables.id, __canUndo: false} + break + case 'toggleHideAllFeeds': + undoAction = {type: 'toggleHideAllFeeds', __canUndo: false} + break + } + + if (data && !error) { + props?.onUpdateSuccess?.({ + undoAction: canUndo ? undoAction : null, + }) + } + }, + mutationFn: async action => { + const updated = await agent.updateLiveEventPreferences(action) + const prefs = updated.find(p => + asPredicate(AppBskyActorDefs.validateLiveEventPreferences)(p), + ) + + switch (action.type) { + case 'hideFeed': + case 'unhideFeed': { + if (!props.feed) { + logger.error( + `useUpdateLiveEventPreferences: feed is missing, but required for hiding/unhiding`, + { + action, + }, + ) + break + } + + logger.metric( + action.type === 'hideFeed' + ? 'liveEvents:feedBanner:hide' + : 'liveEvents:feedBanner:unhide', + { + feed: props.feed.url, + context: props.metricContext, + }, + ) + break + } + case 'toggleHideAllFeeds': { + if (prefs!.hideAllFeeds) { + logger.metric('liveEvents:hideAllFeedBanners', { + context: props.metricContext, + }) + } else { + logger.metric('liveEvents:unhideAllFeedBanners', { + context: props.metricContext, + }) + } + break + } + } + + // triggers a refetch + queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + }) + + return prefs! + }, + }) +} diff --git a/src/features/liveEvents/types.ts b/src/features/liveEvents/types.ts new file mode 100644 index 0000000000..8bc88c448c --- /dev/null +++ b/src/features/liveEvents/types.ts @@ -0,0 +1,27 @@ +export type LiveEventFeedImageLayout = 'wide' | 'compact' // maybe more in the future + +export type LiveEventFeedLayout = { + title: string + overlayColor: string + textColor: string + image: string + blurhash: string +} + +export type LiveEventFeed = { + id: string + preview: boolean + title: string + url: string + layouts: Record +} + +export type LiveEventsWorkerResponse = { + feeds: LiveEventFeed[] +} + +export type LiveEventFeedMetricContext = + | 'explore' + | 'discover' + | 'sidebar' + | 'settings' diff --git a/src/geolocation/const.ts b/src/geolocation/const.ts index b12f37140f..653e829bad 100644 --- a/src/geolocation/const.ts +++ b/src/geolocation/const.ts @@ -1,7 +1,7 @@ -import {BAPP_CONFIG_URL} from '#/env' +import {GEOLOCATION_URL} from '#/env' import {type Geolocation} from '#/geolocation/types' -export const GEOLOCATION_SERVICE_URL = `${BAPP_CONFIG_URL}/geolocation` +export const GEOLOCATION_SERVICE_URL = `${GEOLOCATION_URL}/geolocation` /** * Default geolocation config. diff --git a/src/lib/actor-status.ts b/src/lib/actor-status.ts index 39ab011875..31e532eaa0 100644 --- a/src/lib/actor-status.ts +++ b/src/lib/actor-status.ts @@ -7,7 +7,7 @@ import { import {isAfter, parseISO} from 'date-fns' import {useMaybeProfileShadow} from '#/state/cache/profile-shadow' -import {type LiveNowConfig, useLiveNowConfig} from '#/state/service-config' +import {useLiveNowConfig} from '#/state/service-config' import {useTickEveryMinute} from '#/state/shell' import type * as bsky from '#/types/bsky' @@ -20,7 +20,7 @@ export function useActorStatus(actor?: bsky.profile.AnyProfileView) { tick! // revalidate every minute if (shadowed && 'status' in shadowed && shadowed.status) { - const isValid = validateStatus(shadowed.status, config) + const isValid = validateStatus(shadowed.did, shadowed.status, config) const isDisabled = shadowed.status.isDisabled || false const isActive = isStatusStillActive(shadowed.status.expiresAt) if (isValid && !isDisabled && isActive) { @@ -65,14 +65,19 @@ export function isStatusStillActive(timeStr: string | undefined) { } export function validateStatus( + did: string, status: AppBskyActorDefs.StatusView, - config: LiveNowConfig, + config: {did: string; domains: string[]}[], ) { if (status.status !== 'app.bsky.actor.status#live') return false + const sources = config.find(cfg => cfg.did === did) + if (!sources) { + return false + } try { if (AppBskyEmbedExternal.isView(status.embed)) { const url = new URL(status.embed.external.uri) - return config.allowedDomains.includes(url.hostname) + return sources.domains.includes(url.hostname) } else { return false } diff --git a/src/lib/hooks/useIsBskyTeam.ts b/src/lib/hooks/useIsBskyTeam.ts new file mode 100644 index 0000000000..d653e7fd5e --- /dev/null +++ b/src/lib/hooks/useIsBskyTeam.ts @@ -0,0 +1,8 @@ +import {useMemo} from 'react' + +import {useGate} from '#/lib/statsig/statsig' + +export function useIsBskyTeam() { + const gate = useGate() + return useMemo(() => gate('is_bsky_team_member'), [gate]) +} diff --git a/src/lib/react-query.tsx b/src/lib/react-query.tsx index fe3ec6f4c7..e788bf410c 100644 --- a/src/lib/react-query.tsx +++ b/src/lib/react-query.tsx @@ -1,4 +1,4 @@ -import {useRef, useState} from 'react' +import {useEffect, useRef, useState} from 'react' import {AppState, type AppStateStatus} from 'react-native' import AsyncStorage from '@react-native-async-storage/async-storage' import {createAsyncStoragePersister} from '@tanstack/query-async-storage-persister' @@ -9,9 +9,15 @@ import { } from '@tanstack/react-query-persist-client' import type React from 'react' -import {isNative} from '#/platform/detection' +import {isNative, isWeb} from '#/platform/detection' import {listenNetworkConfirmed, listenNetworkLost} from '#/state/events' +declare global { + interface Window { + __TANSTACK_QUERY_CLIENT__: import('@tanstack/query-core').QueryClient + } +} + // any query keys in this array will be persisted to AsyncStorage export const labelersDetailedInfoQueryKeyRoot = 'labelers-detailed-info' const STORED_CACHE_QUERY_KEY_ROOTS = [labelersDetailedInfoQueryKeyRoot] @@ -180,6 +186,11 @@ function QueryProviderInner({ dehydrateOptions, } }) + useEffect(() => { + if (isWeb) { + window.__TANSTACK_QUERY_CLIENT__ = queryClient + } + }, [queryClient]) return ( Learn more" msgstr "" -#: src/screens/Signup/StepInfo/Policies.tsx:79 +#: src/screens/Signup/StepInfo/Policies.tsx:75 msgid "By creating an account you agree to the <0>Privacy Policy." msgstr "" -#: src/screens/Signup/StepInfo/Policies.tsx:46 +#: src/screens/Signup/StepInfo/Policies.tsx:42 msgid "By creating an account you agree to the <0>Terms of Service and <1>Privacy Policy." msgstr "" -#: src/screens/Signup/StepInfo/Policies.tsx:66 +#: src/screens/Signup/StepInfo/Policies.tsx:62 msgid "By creating an account you agree to the <0>Terms of Service." msgstr "" @@ -1799,9 +1803,9 @@ msgstr "" msgid "Cancel search" msgstr "" -#: src/components/PostControls/index.tsx:106 -#: src/components/PostControls/index.tsx:137 -#: src/components/PostControls/index.tsx:165 +#: src/components/PostControls/index.tsx:107 +#: src/components/PostControls/index.tsx:138 +#: src/components/PostControls/index.tsx:166 #: src/state/shell/composer/index.tsx:95 msgid "Cannot interact with a blocked user" msgstr "" @@ -1837,7 +1841,7 @@ msgstr "" msgid "Change Handle" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:423 +#: src/components/moderation/ReportDialog/index.tsx:437 msgid "Change moderation service" msgstr "" @@ -1850,11 +1854,11 @@ msgstr "" msgid "Change password dialog" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:288 +#: src/components/moderation/ReportDialog/index.tsx:302 msgid "Change report category" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:368 +#: src/components/moderation/ReportDialog/index.tsx:382 msgid "Change report reason" msgstr "" @@ -1993,7 +1997,7 @@ msgstr "" msgid "Choose your own timeline! Feeds built by the community help you find content you love." msgstr "" -#: src/screens/Signup/StepInfo/index.tsx:244 +#: src/screens/Signup/StepInfo/index.tsx:273 msgid "Choose your password" msgstr "" @@ -2030,7 +2034,7 @@ msgstr "" msgid "Click here to contact our support team" msgstr "" -#: src/ageAssurance/components/NoAccessScreen.tsx:236 +#: src/ageAssurance/components/NoAccessScreen.tsx:245 msgid "Click here to log out" msgstr "" @@ -2039,7 +2043,7 @@ msgid "Click here to restart the verification process." msgstr "" #: src/ageAssurance/components/NoAccessScreen.tsx:103 -#: src/ageAssurance/components/NoAccessScreen.tsx:219 +#: src/ageAssurance/components/NoAccessScreen.tsx:228 msgid "Click here to update your birthdate" msgstr "" @@ -2086,7 +2090,7 @@ msgstr "" #: src/components/NewskieDialog.tsx:167 #: src/components/NewskieDialog.tsx:173 #: src/components/Post/Embed/ExternalEmbed/Gif.tsx:208 -#: src/components/ProgressGuide/FollowDialog.tsx:440 +#: src/components/ProgressGuide/FollowDialog.tsx:445 #: src/components/StarterPack/Wizard/WizardEditListDialog.tsx:118 #: src/components/StarterPack/Wizard/WizardEditListDialog.tsx:124 #: src/components/verification/VerificationsDialog.tsx:144 @@ -2125,7 +2129,7 @@ msgstr "" msgid "Close dialog" msgstr "" -#: src/view/shell/index.web.tsx:128 +#: src/view/shell/index.web.tsx:130 msgid "Close drawer menu" msgstr "" @@ -2256,8 +2260,8 @@ msgstr "" msgid "Confirm delete account" msgstr "" -#: src/ageAssurance/components/NoAccessScreen.tsx:359 -#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:87 +#: src/ageAssurance/components/NoAccessScreen.tsx:368 +#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:95 #: src/components/dialogs/DeviceLocationRequestDialog.tsx:40 #: src/components/dialogs/DeviceLocationRequestDialog.tsx:105 msgid "Confirm your location" @@ -2283,8 +2287,8 @@ msgstr "" msgid "Connection issue" msgstr "" -#: src/ageAssurance/components/NoAccessScreen.tsx:296 -#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:130 +#: src/ageAssurance/components/NoAccessScreen.tsx:305 +#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:138 #: src/components/ageAssurance/AgeAssuranceAppealDialog.tsx:29 msgid "Contact our moderation team" msgstr "" @@ -2415,7 +2419,7 @@ msgstr "" #: src/components/dms/MessageContextMenu.tsx:57 #: src/components/PostControls/DiscoverDebug.tsx:36 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:235 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:249 #: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:72 #: src/lib/sharing.ts:25 #: src/lib/sharing.ts:41 @@ -2439,8 +2443,8 @@ msgstr "" msgid "Copy App Password" msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:438 -#: src/view/com/profile/ProfileMenu.tsx:441 +#: src/view/com/profile/ProfileMenu.tsx:447 +#: src/view/com/profile/ProfileMenu.tsx:450 msgid "Copy at:// URI" msgstr "" @@ -2455,8 +2459,8 @@ msgid "Copy code" msgstr "" #: src/screens/Settings/components/ChangeHandleDialog.tsx:502 -#: src/view/com/profile/ProfileMenu.tsx:447 -#: src/view/com/profile/ProfileMenu.tsx:450 +#: src/view/com/profile/ProfileMenu.tsx:456 +#: src/view/com/profile/ProfileMenu.tsx:459 msgid "Copy DID" msgstr "" @@ -2485,8 +2489,8 @@ msgstr "" msgid "Copy link to post" msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:251 -#: src/view/com/profile/ProfileMenu.tsx:262 +#: src/view/com/profile/ProfileMenu.tsx:252 +#: src/view/com/profile/ProfileMenu.tsx:263 msgid "Copy link to profile" msgstr "" @@ -2504,8 +2508,8 @@ msgstr "" msgid "Copy post at:// URI" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:484 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:486 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:510 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:512 msgid "Copy post text" msgstr "" @@ -2668,8 +2672,8 @@ msgid "Create new account" msgstr "" #. Accessibility label for button to create a moderation report for the selected option -#: src/components/moderation/ReportDialog/index.tsx:688 -#: src/components/moderation/ReportDialog/index.tsx:734 +#: src/components/moderation/ReportDialog/index.tsx:702 +#: src/components/moderation/ReportDialog/index.tsx:748 msgid "Create report for {0}" msgstr "" @@ -2735,7 +2739,7 @@ msgstr "" msgid "Dark theme" msgstr "" -#: src/screens/Signup/StepInfo/index.tsx:272 +#: src/screens/Signup/StepInfo/index.tsx:301 msgid "Date of birth" msgstr "" @@ -2766,7 +2770,7 @@ msgid "Default icons" msgstr "" #: src/components/dms/MessageContextMenu.tsx:202 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:736 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:762 #: src/screens/Messages/components/ChatStatusInfo.tsx:55 #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:280 #: src/screens/Settings/AppPasswords.tsx:212 @@ -2838,8 +2842,8 @@ msgstr "" msgid "Delete my account" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:717 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:719 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:743 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:745 #: src/view/com/composer/Composer.tsx:969 msgid "Delete post" msgstr "" @@ -2857,7 +2861,7 @@ msgstr "" msgid "Delete this list?" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:731 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:757 msgid "Delete this post?" msgstr "" @@ -2892,12 +2896,12 @@ msgstr "" msgid "Descriptive alt text" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:621 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:631 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:647 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:657 msgid "Detach quote" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:767 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:793 msgid "Detach quote post?" msgstr "" @@ -3010,7 +3014,7 @@ msgstr "" msgid "Dismiss error" msgstr "" -#: src/components/ProgressGuide/List.tsx:42 +#: src/components/ProgressGuide/List.tsx:67 msgid "Dismiss getting started guide" msgstr "" @@ -3065,8 +3069,8 @@ msgstr "" msgid "Don't see an email? <0>Click here to resend." msgstr "" -#: src/components/ageAssurance/AgeAssuranceDismissibleFeedBanner.tsx:103 -#: src/components/ageAssurance/AgeAssuranceDismissibleNotice.tsx:33 +#: src/components/ageAssurance/AgeAssuranceDismissibleFeedBanner.tsx:104 +#: src/components/ageAssurance/AgeAssuranceDismissibleNotice.tsx:37 msgid "Don't show again" msgstr "" @@ -3210,8 +3214,8 @@ msgstr "" msgid "Edit image" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:698 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:711 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:724 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:737 msgid "Edit interaction settings" msgstr "" @@ -3225,8 +3229,8 @@ msgstr "" msgid "Edit list details" msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:334 -#: src/view/com/profile/ProfileMenu.tsx:340 +#: src/view/com/profile/ProfileMenu.tsx:337 +#: src/view/com/profile/ProfileMenu.tsx:349 msgid "Edit live status" msgstr "" @@ -3289,7 +3293,7 @@ msgid "Either the creator of this list has blocked you or you have blocked the c msgstr "" #: src/screens/Settings/AccountSettings.tsx:66 -#: src/screens/Signup/StepInfo/index.tsx:196 +#: src/screens/Signup/StepInfo/index.tsx:225 msgid "Email" msgstr "" @@ -3458,7 +3462,7 @@ msgid "Enter your birthdate" msgstr "" #: src/screens/Login/ForgotPasswordForm.tsx:99 -#: src/screens/Signup/StepInfo/index.tsx:216 +#: src/screens/Signup/StepInfo/index.tsx:245 msgid "Enter your email address" msgstr "" @@ -3691,7 +3695,7 @@ msgstr "" msgid "Failed to delete message" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:204 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:206 msgid "Failed to delete post, please try again" msgstr "" @@ -3821,12 +3825,13 @@ msgstr "" msgid "Failed to send email, please try again." msgstr "" +#: src/components/live/GoLiveDisabledDialog.tsx:76 #: src/components/moderation/LabelsOnMeDialog.tsx:265 #: src/screens/Messages/components/ChatDisabled.tsx:99 msgid "Failed to submit appeal, please try again." msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:224 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:238 msgid "Failed to toggle thread mute, please try again" msgstr "" @@ -3907,14 +3912,14 @@ msgstr "" msgid "Feed unavailable" msgstr "" +#: src/view/shell/desktop/RightNav.tsx:104 #: src/view/shell/desktop/RightNav.tsx:105 -#: src/view/shell/desktop/RightNav.tsx:106 #: src/view/shell/Drawer.tsx:368 msgid "Feedback" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:276 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:294 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:296 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:320 msgctxt "toast" msgid "Feedback sent to feed operator" msgstr "" @@ -4010,9 +4015,9 @@ msgid "Find my friends" msgstr "" #. Starter packs suggested to the user for them to follow -#: src/components/ProgressGuide/FollowDialog.tsx:72 -#: src/components/ProgressGuide/FollowDialog.tsx:82 -#: src/components/ProgressGuide/FollowDialog.tsx:426 +#: src/components/ProgressGuide/FollowDialog.tsx:78 +#: src/components/ProgressGuide/FollowDialog.tsx:86 +#: src/components/ProgressGuide/FollowDialog.tsx:431 #: src/screens/Onboarding/StepSuggestedStarterpacks/index.tsx:42 msgid "Find people to follow" msgstr "" @@ -4063,8 +4068,8 @@ msgstr "" #. User is not following this account, click to follow #: src/components/ProfileCard.tsx:559 -#: src/components/ProfileHoverCard/index.web.tsx:496 -#: src/components/ProfileHoverCard/index.web.tsx:507 +#: src/components/ProfileHoverCard/index.web.tsx:497 +#: src/components/ProfileHoverCard/index.web.tsx:508 #: src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx:131 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:378 #: src/screens/VideoFeed/index.tsx:879 @@ -4082,17 +4087,20 @@ msgstr "" msgid "Follow {handle}" msgstr "" -#: src/components/ProgressGuide/List.tsx:52 #: src/state/shell/progress-guide.tsx:231 msgid "Follow 10 accounts" msgstr "" -#: src/components/ProgressGuide/List.tsx:69 +#: src/components/ProgressGuide/List.tsx:60 +msgid "Follow 10 people to get started" +msgstr "" + +#: src/components/ProgressGuide/List.tsx:102 msgid "Follow 7 accounts" msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:292 -#: src/view/com/profile/ProfileMenu.tsx:303 +#: src/view/com/profile/ProfileMenu.tsx:293 +#: src/view/com/profile/ProfileMenu.tsx:304 msgid "Follow account" msgstr "" @@ -4157,8 +4165,8 @@ msgstr "" #. User is following this account, click to unfollow #. User is following this account, click to unfollow #: src/components/ProfileCard.tsx:546 -#: src/components/ProfileHoverCard/index.web.tsx:495 -#: src/components/ProfileHoverCard/index.web.tsx:506 +#: src/components/ProfileHoverCard/index.web.tsx:496 +#: src/components/ProfileHoverCard/index.web.tsx:507 #: src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx:134 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:374 #: src/screens/VideoFeed/index.tsx:877 @@ -4341,10 +4349,6 @@ msgstr "" msgid "Get started" msgstr "" -#: src/components/ProgressGuide/List.tsx:35 -msgid "Getting started" -msgstr "" - #: src/components/MediaPreview.tsx:114 msgid "GIF" msgstr "" @@ -4407,8 +4411,8 @@ msgstr "" msgid "Go Home" msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:335 -#: src/view/com/profile/ProfileMenu.tsx:342 +#: src/view/com/profile/ProfileMenu.tsx:338 +#: src/view/com/profile/ProfileMenu.tsx:351 msgid "Go live" msgstr "" @@ -4419,6 +4423,11 @@ msgstr "" msgid "Go Live" msgstr "" +#: src/view/com/profile/ProfileMenu.tsx:335 +#: src/view/com/profile/ProfileMenu.tsx:347 +msgid "Go live (disabled)" +msgstr "" + #: src/components/live/GoLiveDialog.tsx:171 msgid "Go live for" msgstr "" @@ -4427,9 +4436,9 @@ msgstr "" msgid "Go to {firstAuthorName}'s profile" msgstr "" -#: src/components/ageAssurance/AgeAssuranceAdmonition.tsx:87 -#: src/components/ageAssurance/AgeRestrictedScreen.tsx:64 -#: src/components/ageAssurance/AgeRestrictedScreen.tsx:73 +#: src/components/ageAssurance/AgeAssuranceAdmonition.tsx:91 +#: src/components/ageAssurance/AgeRestrictedScreen.tsx:71 +#: src/components/ageAssurance/AgeRestrictedScreen.tsx:80 #: src/screens/Moderation/index.tsx:219 msgid "Go to account settings" msgstr "" @@ -4456,6 +4465,10 @@ msgstr "" msgid "Go to user's profile" msgstr "" +#: src/components/live/GoLiveDisabledDialog.tsx:104 +msgid "Going live is currently disabled for your account" +msgstr "" + #: src/lib/moderation/useGlobalLabelStrings.ts:46 #: src/lib/moderation/useGlobalLabelStrings.ts:50 #: src/view/com/composer/labels/LabelsBtn.tsx:196 @@ -4531,6 +4544,10 @@ msgstr "" msgid "Have a code? <0>Click here." msgstr "" +#: src/screens/Signup/StepInfo/index.tsx:332 +msgid "Have we got your location wrong? <0>Tap here to confirm your location with GPS." +msgstr "" + #: src/screens/Signup/index.tsx:220 msgid "Having trouble?" msgstr "" @@ -4541,8 +4558,8 @@ msgstr "" #: src/screens/Settings/Settings.tsx:254 #: src/screens/Settings/Settings.tsx:258 -#: src/view/shell/desktop/RightNav.tsx:123 -#: src/view/shell/desktop/RightNav.tsx:124 +#: src/view/shell/desktop/RightNav.tsx:125 +#: src/view/shell/desktop/RightNav.tsx:128 #: src/view/shell/Drawer.tsx:381 msgid "Help" msgstr "" @@ -4564,7 +4581,7 @@ msgstr "" msgid "Hey there!" msgstr "" -#: src/ageAssurance/components/NoAccessScreen.tsx:201 +#: src/ageAssurance/components/NoAccessScreen.tsx:210 msgid "Hi there!" msgstr "" @@ -4581,17 +4598,17 @@ msgstr "" msgid "Hidden list" msgstr "" -#: src/components/interstitials/Trending.tsx:131 +#: src/components/interstitials/Trending.tsx:130 #: src/components/interstitials/TrendingVideos.tsx:138 #: src/components/moderation/ContentHider.tsx:208 #: src/components/moderation/LabelPreference.tsx:140 #: src/components/moderation/PostHider.tsx:137 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:747 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:773 #: src/lib/moderation/useLabelBehaviorDescription.ts:18 #: src/lib/moderation/useLabelBehaviorDescription.ts:23 #: src/lib/moderation/useLabelBehaviorDescription.ts:28 #: src/lib/moderation/useLabelBehaviorDescription.ts:33 -#: src/view/shell/desktop/SidebarTrendingTopics.tsx:111 +#: src/view/shell/desktop/SidebarTrendingTopics.tsx:129 msgid "Hide" msgstr "" @@ -4608,18 +4625,18 @@ msgstr "" msgid "Hide lists" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:578 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:584 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:604 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:610 msgid "Hide post for me" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:595 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:605 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:621 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:631 msgid "Hide reply for everyone" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:577 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:583 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:603 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:609 msgid "Hide reply for me" msgstr "" @@ -4627,22 +4644,21 @@ msgstr "" msgid "Hide this card" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:742 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:768 msgid "Hide this post?" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:742 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:777 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:768 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:803 msgid "Hide this reply?" msgstr "" -#: src/components/interstitials/Trending.tsx:113 -#: src/view/shell/desktop/SidebarTrendingTopics.tsx:62 +#: src/components/interstitials/Trending.tsx:112 msgid "Hide trending topics" msgstr "" -#: src/components/interstitials/Trending.tsx:129 -#: src/view/shell/desktop/SidebarTrendingTopics.tsx:109 +#: src/components/interstitials/Trending.tsx:128 +#: src/view/shell/desktop/SidebarTrendingTopics.tsx:127 msgid "Hide trending topics?" msgstr "" @@ -4765,7 +4781,7 @@ msgstr "" msgid "If alt text is long, toggles alt text expanded state" msgstr "" -#: src/screens/Signup/StepInfo/Policies.tsx:110 +#: src/screens/Signup/StepInfo/index.tsx:351 msgid "If you are not yet an adult according to the laws of your country, your parent or legal guardian must read these Terms on your behalf." msgstr "" @@ -4789,7 +4805,7 @@ msgstr "" msgid "If you need to update your email, <0>click here." msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:733 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:759 msgid "If you remove this post, you won't be able to recover it." msgstr "" @@ -4861,7 +4877,7 @@ msgstr "" msgid "Imported on {0}" msgstr "" -#: src/ageAssurance/components/NoAccessScreen.tsx:204 +#: src/ageAssurance/components/NoAccessScreen.tsx:213 msgid "In order to provide an age-appropriate experience, we need to know your birthdate. This is a one-time thing, and your data will be kept private." msgstr "" @@ -4951,7 +4967,7 @@ msgstr "" msgid "Invalid handle. Please try a different one." msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:365 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:391 msgctxt "toast" msgid "Invalid interaction settings." msgstr "" @@ -4961,7 +4977,7 @@ msgstr "" msgid "Invalid phone number" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:80 +#: src/components/moderation/ReportDialog/index.tsx:92 msgid "Invalid report subject" msgstr "" @@ -4977,7 +4993,7 @@ msgstr "" msgid "Invite {name} to join Bluesky" msgstr "" -#: src/screens/Signup/StepInfo/index.tsx:166 +#: src/screens/Signup/StepInfo/index.tsx:195 msgid "Invite code" msgstr "" @@ -5006,8 +5022,8 @@ msgstr "" msgid "Invites, but personal" msgstr "" -#: src/ageAssurance/components/NoAccessScreen.tsx:356 -#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:84 +#: src/ageAssurance/components/NoAccessScreen.tsx:365 +#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:92 msgid "Is your location not accurate? <0>Tap here to confirm your location." msgstr "" @@ -5015,7 +5031,7 @@ msgstr "" msgid "It looks like some of your contacts have not tried to find you here yet. You can personally invite them by customizing a draft message we will provide." msgstr "" -#: src/screens/Signup/StepInfo/index.tsx:292 +#: src/screens/Signup/StepInfo/index.tsx:384 msgid "It's correct" msgstr "" @@ -5103,13 +5119,13 @@ msgstr "" msgid "Larger" msgstr "" -#: src/ageAssurance/components/NoAccessScreen.tsx:339 -#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:185 +#: src/ageAssurance/components/NoAccessScreen.tsx:348 +#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:193 msgid "Last initiated {timeAgo} ago" msgstr "" -#: src/ageAssurance/components/NoAccessScreen.tsx:337 -#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:183 +#: src/ageAssurance/components/NoAccessScreen.tsx:346 +#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:191 msgid "Last initiated just now" msgstr "" @@ -5133,7 +5149,7 @@ msgstr "" msgid "Learn More" msgstr "" -#: src/components/ageAssurance/AgeAssuranceDismissibleFeedBanner.tsx:64 +#: src/components/ageAssurance/AgeAssuranceDismissibleFeedBanner.tsx:65 msgid "Learn more about age assurance" msgstr "" @@ -5185,7 +5201,7 @@ msgstr "" msgid "Learn more about what is public on Bluesky." msgstr "" -#: src/components/ageAssurance/AgeAssuranceAdmonition.tsx:84 +#: src/components/ageAssurance/AgeAssuranceAdmonition.tsx:88 msgid "Learn more in your <0>account settings." msgstr "" @@ -5253,11 +5269,11 @@ msgid "Like" msgstr "" #. Accessibility label for the like button when the post has not been liked, verb form followed by number of likes and noun form -#: src/components/PostControls/index.tsx:270 +#: src/components/PostControls/index.tsx:286 msgid "Like ({0, plural, one {# like} other {# likes}})" msgstr "" -#: src/components/ProgressGuide/List.tsx:63 +#: src/components/ProgressGuide/List.tsx:96 msgid "Like 10 posts" msgstr "" @@ -5318,7 +5334,7 @@ msgstr "" msgid "Likes of your reposts notifications" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:470 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:482 msgid "Likes on this post" msgstr "" @@ -5429,8 +5445,8 @@ msgstr "" msgid "LIVE" msgstr "" -#: src/components/live/LiveStatusDialog.tsx:215 -msgid "Live feature is in beta testing" +#: src/components/live/LiveStatusDialog.tsx:235 +msgid "Live feature is in beta" msgstr "" #: src/components/live/EditLiveDialog.tsx:148 @@ -5484,11 +5500,11 @@ msgstr "" msgid "Logged-out visibility" msgstr "" -#: src/view/shell/desktop/RightNav.tsx:133 +#: src/view/shell/desktop/RightNav.tsx:137 msgid "Logo by @sawaratsuki.bsky.social" msgstr "" -#: src/view/shell/desktop/RightNav.tsx:130 +#: src/view/shell/desktop/RightNav.tsx:134 #: src/view/shell/Drawer.tsx:709 msgid "Logo by <0>@sawaratsuki.bsky.social" msgstr "" @@ -5702,8 +5718,8 @@ msgstr "" msgid "Moderator has chosen to set a general warning on the content." msgstr "" -#: src/view/shell/desktop/Feeds.tsx:113 -#: src/view/shell/desktop/Feeds.tsx:123 +#: src/view/shell/desktop/Feeds.tsx:107 +#: src/view/shell/desktop/Feeds.tsx:154 msgid "More feeds" msgstr "" @@ -5713,8 +5729,8 @@ msgid "More languages..." msgstr "" #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:149 -#: src/view/com/profile/ProfileMenu.tsx:228 -#: src/view/com/profile/ProfileMenu.tsx:234 +#: src/view/com/profile/ProfileMenu.tsx:229 +#: src/view/com/profile/ProfileMenu.tsx:235 msgid "More options" msgstr "" @@ -5745,10 +5761,10 @@ msgstr "" msgid "Mute {tag}" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:660 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:666 -#: src/view/com/profile/ProfileMenu.tsx:380 -#: src/view/com/profile/ProfileMenu.tsx:387 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:686 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:692 +#: src/view/com/profile/ProfileMenu.tsx:389 +#: src/view/com/profile/ProfileMenu.tsx:396 msgid "Mute account" msgstr "" @@ -5798,13 +5814,13 @@ msgstr "" msgid "Mute this word until you unmute it" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:544 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:548 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:570 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:574 msgid "Mute thread" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:558 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:560 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:584 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:586 msgid "Mute words & tags" msgstr "" @@ -5868,8 +5884,8 @@ msgstr "" msgid "Navigates to your profile" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:319 -#: src/components/moderation/ReportDialog/index.tsx:336 +#: src/components/moderation/ReportDialog/index.tsx:333 +#: src/components/moderation/ReportDialog/index.tsx:350 msgid "Need to report a copyright violation, legal request, or regulatory compliance issue?" msgstr "" @@ -6133,7 +6149,7 @@ msgid "No result" msgstr "" #: src/components/dialogs/SearchablePeopleList.tsx:223 -#: src/components/ProgressGuide/FollowDialog.tsx:224 +#: src/components/ProgressGuide/FollowDialog.tsx:229 msgid "No results" msgstr "" @@ -6222,7 +6238,7 @@ msgstr "" msgid "Not Found" msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:502 +#: src/view/com/profile/ProfileMenu.tsx:511 msgid "Note about sharing" msgstr "" @@ -6322,7 +6338,7 @@ msgid "OK" msgstr "" #: src/screens/Login/PasswordUpdatedForm.tsx:37 -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:693 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:705 msgid "Okay" msgstr "" @@ -6451,16 +6467,16 @@ msgstr "" msgid "Open pack" msgstr "" -#: src/components/PostControls/PostMenu/index.tsx:64 +#: src/components/PostControls/PostMenu/index.tsx:66 msgid "Open post options menu" msgstr "" -#: src/components/live/LiveStatusDialog.tsx:189 -#: src/components/live/LiveStatusDialog.tsx:203 +#: src/components/live/LiveStatusDialog.tsx:205 +#: src/components/live/LiveStatusDialog.tsx:219 msgid "Open profile" msgstr "" -#: src/components/PostControls/ShareMenu/index.tsx:89 +#: src/components/PostControls/ShareMenu/index.tsx:107 msgid "Open share menu" msgstr "" @@ -6477,6 +6493,10 @@ msgstr "" msgid "Open system log" msgstr "" +#: src/view/shell/desktop/Feeds.tsx:186 +msgid "Opens {0} feed" +msgstr "" + #: src/view/com/composer/labels/LabelsBtn.tsx:62 msgid "Opens a dialog to add a content warning to your post" msgstr "" @@ -6661,7 +6681,7 @@ msgstr "" #: src/screens/Login/LoginForm.tsx:229 #: src/screens/Settings/AccountSettings.tsx:121 #: src/screens/Settings/AccountSettings.tsx:125 -#: src/screens/Signup/StepInfo/index.tsx:231 +#: src/screens/Signup/StepInfo/index.tsx:260 #: src/view/com/modals/DeleteAccount.tsx:239 #: src/view/com/modals/DeleteAccount.tsx:246 msgid "Password" @@ -6772,8 +6792,8 @@ msgstr "" msgid "Pin to Home" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:452 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:459 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:478 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:485 msgid "Pin to your profile" msgstr "" @@ -6842,7 +6862,7 @@ msgid "Please choose your handle." msgstr "" #: src/screens/Signup/state.ts:281 -#: src/screens/Signup/StepInfo/index.tsx:122 +#: src/screens/Signup/StepInfo/index.tsx:151 msgid "Please choose your password." msgstr "" @@ -6855,7 +6875,7 @@ msgid "Please complete the verification captcha." msgstr "" #: src/components/ageAssurance/AgeAssuranceInitDialog.tsx:101 -#: src/screens/Signup/StepInfo/index.tsx:111 +#: src/screens/Signup/StepInfo/index.tsx:140 msgid "Please double-check that you have entered your email address correctly." msgstr "" @@ -6903,11 +6923,11 @@ msgid "Please enter the security code we sent to your previous email address." msgstr "" #: src/screens/Signup/state.ts:265 -#: src/screens/Signup/StepInfo/index.tsx:93 +#: src/screens/Signup/StepInfo/index.tsx:122 msgid "Please enter your email." msgstr "" -#: src/screens/Signup/StepInfo/index.tsx:86 +#: src/screens/Signup/StepInfo/index.tsx:115 msgid "Please enter your invite code." msgstr "" @@ -6935,6 +6955,10 @@ msgstr "" msgid "Please explain why you think your chats were incorrectly disabled" msgstr "" +#: src/components/live/GoLiveDisabledDialog.tsx:123 +msgid "Please explain why you think your Go Live access was incorrectly disabled." +msgstr "" + #: src/components/FocusScope/index.tsx:91 #: src/components/FocusScope/index.tsx:115 msgid "Please go back, or activate this element to return to the start of the active content." @@ -7022,7 +7046,7 @@ msgstr "" msgid "Post by @{0}" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:184 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:186 msgctxt "toast" msgid "Post deleted" msgstr "" @@ -7072,7 +7096,7 @@ msgctxt "toast" msgid "Post pinned" msgstr "" -#: src/components/PostControls/BookmarkButton.tsx:59 +#: src/components/PostControls/BookmarkButton.tsx:66 msgid "Post saved" msgstr "" @@ -7275,11 +7299,11 @@ msgstr "" msgid "Quote post" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:314 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:340 msgid "Quote post was re-attached" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:313 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:339 msgid "Quote post was successfully detached" msgstr "" @@ -7297,7 +7321,7 @@ msgstr "" msgid "Quotes" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:454 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:466 msgid "Quotes of this post" msgstr "" @@ -7309,8 +7333,8 @@ msgstr "" msgid "Rate limit exceeded. Please try again later." msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:620 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:630 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:646 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:656 msgid "Re-attach quote" msgstr "" @@ -7356,13 +7380,13 @@ msgstr "" msgid "Read the Bluesky blog" msgstr "" -#: src/screens/Signup/StepInfo/Policies.tsx:56 -#: src/screens/Signup/StepInfo/Policies.tsx:82 +#: src/screens/Signup/StepInfo/Policies.tsx:52 +#: src/screens/Signup/StepInfo/Policies.tsx:78 msgid "Read the Bluesky Privacy Policy" msgstr "" -#: src/screens/Signup/StepInfo/Policies.tsx:49 -#: src/screens/Signup/StepInfo/Policies.tsx:69 +#: src/screens/Signup/StepInfo/Policies.tsx:45 +#: src/screens/Signup/StepInfo/Policies.tsx:65 msgid "Read the Bluesky Terms of Service" msgstr "" @@ -7494,7 +7518,7 @@ msgstr "" msgid "Remove from saved feeds" msgstr "" -#: src/components/PostControls/BookmarkButton.tsx:128 +#: src/components/PostControls/BookmarkButton.tsx:140 #: src/screens/Bookmarks/index.tsx:260 msgid "Remove from saved posts" msgstr "" @@ -7544,8 +7568,8 @@ msgstr "" #: src/components/verification/VerificationRemovePrompt.tsx:46 #: src/components/verification/VerificationsDialog.tsx:252 -#: src/view/com/profile/ProfileMenu.tsx:353 -#: src/view/com/profile/ProfileMenu.tsx:356 +#: src/view/com/profile/ProfileMenu.tsx:362 +#: src/view/com/profile/ProfileMenu.tsx:365 msgid "Remove verification" msgstr "" @@ -7570,7 +7594,7 @@ msgstr "" msgid "Removed from saved feeds" msgstr "" -#: src/components/PostControls/BookmarkButton.tsx:94 +#: src/components/PostControls/BookmarkButton.tsx:106 #: src/screens/Bookmarks/index.tsx:218 msgid "Removed from saved posts" msgstr "" @@ -7638,7 +7662,7 @@ msgid "Reply" msgstr "" #. Accessibility label for the reply button, verb form followed by number of replies and noun form -#: src/components/PostControls/index.tsx:224 +#: src/components/PostControls/index.tsx:240 msgid "Reply ({0, plural, one {# reply} other {# replies}})" msgstr "" @@ -7664,23 +7688,24 @@ msgstr "" msgid "Reply sorting" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:351 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:377 msgctxt "toast" msgid "Reply visibility updated" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:350 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:376 msgid "Reply was successfully hidden" msgstr "" #: src/components/dms/MessageContextMenu.tsx:168 #: src/components/dms/MessagesListBlockedFooter.tsx:85 #: src/components/dms/MessagesListBlockedFooter.tsx:92 +#: src/components/live/LiveStatusDialog.tsx:257 msgid "Report" msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:420 -#: src/view/com/profile/ProfileMenu.tsx:423 +#: src/view/com/profile/ProfileMenu.tsx:429 +#: src/view/com/profile/ProfileMenu.tsx:432 msgid "Report account" msgstr "" @@ -7692,8 +7717,8 @@ msgstr "" msgid "Report conversation" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:78 -#: src/components/moderation/ReportDialog/index.tsx:239 +#: src/components/moderation/ReportDialog/index.tsx:90 +#: src/components/moderation/ReportDialog/index.tsx:253 msgid "Report dialog" msgstr "" @@ -7711,8 +7736,8 @@ msgstr "" msgid "Report message" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:686 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:688 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:712 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:714 msgid "Report post" msgstr "" @@ -7726,27 +7751,32 @@ msgstr "" msgid "Report submitted" msgstr "" -#: src/components/moderation/ReportDialog/copy.ts:45 +#: src/components/moderation/ReportDialog/copy.ts:51 msgid "Report this conversation" msgstr "" -#: src/components/moderation/ReportDialog/copy.ts:31 +#: src/components/moderation/ReportDialog/copy.ts:37 msgid "Report this feed" msgstr "" -#: src/components/moderation/ReportDialog/copy.ts:25 +#: src/components/moderation/ReportDialog/copy.ts:31 msgid "Report this list" msgstr "" -#: src/components/moderation/ReportDialog/copy.ts:51 +#: src/components/live/LiveStatusDialog.tsx:240 +#: src/components/moderation/ReportDialog/copy.ts:19 +msgid "Report this livestream" +msgstr "" + +#: src/components/moderation/ReportDialog/copy.ts:57 msgid "Report this message" msgstr "" -#: src/components/moderation/ReportDialog/copy.ts:19 +#: src/components/moderation/ReportDialog/copy.ts:25 msgid "Report this post" msgstr "" -#: src/components/moderation/ReportDialog/copy.ts:37 +#: src/components/moderation/ReportDialog/copy.ts:43 msgid "Report this starter pack" msgstr "" @@ -7798,7 +7828,7 @@ msgstr "" msgid "Reposts" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:436 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:448 msgid "Reposts of this post" msgstr "" @@ -7826,7 +7856,7 @@ msgstr "" msgid "Require an email code to sign in to your account." msgstr "" -#: src/screens/Signup/StepInfo/index.tsx:180 +#: src/screens/Signup/StepInfo/index.tsx:209 msgid "Required for this provider" msgstr "" @@ -7892,12 +7922,14 @@ msgstr "" msgid "Retries the last action, which errored out" msgstr "" +#: src/components/ageAssurance/AgeAssuranceErrors.tsx:27 +#: src/components/ageAssurance/AgeAssuranceErrors.tsx:30 #: src/components/contacts/screens/VerifyNumber.tsx:347 #: src/components/contacts/screens/VerifyNumber.tsx:352 #: src/components/dms/MessageItem.tsx:322 #: src/components/Error.tsx:65 #: src/components/Lists.tsx:114 -#: src/components/moderation/ReportDialog/index.tsx:273 +#: src/components/moderation/ReportDialog/index.tsx:287 #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx:55 #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx:58 #: src/components/StarterPack/ProfileStarterPacks.tsx:374 @@ -7919,7 +7951,7 @@ msgstr "" msgid "Retry" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:270 +#: src/components/moderation/ReportDialog/index.tsx:284 #: src/view/screens/Storybook/Admonitions.tsx:60 msgid "Retry loading report options" msgstr "" @@ -8068,7 +8100,7 @@ msgstr "" msgid "Search @{0}'s posts" msgstr "" -#: src/components/ProgressGuide/FollowDialog.tsx:657 +#: src/components/ProgressGuide/FollowDialog.tsx:662 msgid "Search by name or interest" msgstr "" @@ -8081,12 +8113,12 @@ msgid "Search feeds" msgstr "" #. Accessibility label for a tab that searches for accounts in a category (e.g. Art, Video Games, Sports, etc.) that are suggested for the user to follow. The tab is not currently active and can be selected. -#: src/components/ProgressGuide/FollowDialog.tsx:486 +#: src/components/ProgressGuide/FollowDialog.tsx:491 msgid "Search for \"{interestsDisplayName}\"" msgstr "" #. Accessibility label for a tab that searches for accounts in a category (e.g. Art, Video Games, Sports, etc.) that are suggested for the user to follow. The tab is currently selected. -#: src/components/ProgressGuide/FollowDialog.tsx:479 +#: src/components/ProgressGuide/FollowDialog.tsx:484 msgid "Search for \"{interestsDisplayName}\" (active)" msgstr "" @@ -8132,13 +8164,13 @@ msgstr "" msgid "Search my posts" msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:271 -#: src/view/com/profile/ProfileMenu.tsx:274 +#: src/view/com/profile/ProfileMenu.tsx:272 +#: src/view/com/profile/ProfileMenu.tsx:275 msgid "Search posts" msgstr "" #: src/components/dialogs/SearchablePeopleList.tsx:534 -#: src/components/ProgressGuide/FollowDialog.tsx:676 +#: src/components/ProgressGuide/FollowDialog.tsx:681 msgid "Search profiles" msgstr "" @@ -8151,7 +8183,7 @@ msgid "Search..." msgstr "" #: src/components/dialogs/SearchablePeopleList.tsx:535 -#: src/components/ProgressGuide/FollowDialog.tsx:677 +#: src/components/ProgressGuide/FollowDialog.tsx:682 msgid "Searches for profiles" msgstr "" @@ -8219,7 +8251,7 @@ msgstr "" msgid "Select a color" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:358 +#: src/components/moderation/ReportDialog/index.tsx:372 msgid "Select a reason" msgstr "" @@ -8294,7 +8326,7 @@ msgstr "" msgid "Select languages" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:408 +#: src/components/moderation/ReportDialog/index.tsx:422 msgid "Select moderation service" msgstr "" @@ -8335,7 +8367,7 @@ msgstr "" msgid "Select which languages you want your subscribed feeds to include. If none are selected, all languages will be shown." msgstr "" -#: src/screens/Signup/StepInfo/index.tsx:273 +#: src/screens/Signup/StepInfo/index.tsx:302 msgid "Select your date of birth" msgstr "" @@ -8398,7 +8430,7 @@ msgstr "" msgid "Send post to..." msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:799 +#: src/components/moderation/ReportDialog/index.tsx:813 msgid "Send report to {title}" msgstr "" @@ -8451,7 +8483,7 @@ msgstr "" msgid "Set who can reply to your post" msgstr "" -#: src/ageAssurance/components/NoAccessScreen.tsx:211 +#: src/ageAssurance/components/NoAccessScreen.tsx:220 msgid "Set your birthdate below and we'll get you back to posting and exploring in no time!" msgstr "" @@ -8543,7 +8575,7 @@ msgstr "" msgid "Share a fun fact!" msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:507 +#: src/view/com/profile/ProfileMenu.tsx:516 msgid "Share anyway" msgstr "" @@ -8591,8 +8623,8 @@ msgstr "" #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:171 #: src/screens/StarterPack/StarterPackScreen.tsx:613 #: src/screens/StarterPack/StarterPackScreen.tsx:621 -#: src/view/com/profile/ProfileMenu.tsx:251 -#: src/view/com/profile/ProfileMenu.tsx:264 +#: src/view/com/profile/ProfileMenu.tsx:252 +#: src/view/com/profile/ProfileMenu.tsx:265 msgid "Share via..." msgstr "" @@ -8639,8 +8671,8 @@ msgstr "" msgid "Show customization options" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:515 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:517 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:541 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:543 msgid "Show less like this" msgstr "" @@ -8656,8 +8688,8 @@ msgstr "" msgid "Show More" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:507 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:509 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:533 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:535 msgid "Show more like this" msgstr "" @@ -8683,8 +8715,8 @@ msgstr "" msgid "Show replies as" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:594 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:604 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:620 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:630 msgid "Show reply for everyone" msgstr "" @@ -8706,7 +8738,7 @@ msgstr "" msgid "Show warning and filter from feeds" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:629 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:641 msgid "Shows information about when this post was created" msgstr "" @@ -8766,8 +8798,8 @@ msgstr "" msgid "Sign in to Bluesky or create a new account" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:493 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:495 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:519 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:521 msgid "Sign in to view post" msgstr "" @@ -8869,7 +8901,7 @@ msgstr "" msgid "Someone reacted {0} to {1}" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:83 +#: src/components/moderation/ReportDialog/index.tsx:95 msgid "Something wasn't quite right with the data you're trying to report. Please contact support." msgstr "" @@ -8878,7 +8910,7 @@ msgid "Something went wrong" msgstr "" #: src/components/ageAssurance/AgeAssuranceInitDialog.tsx:138 -#: src/components/moderation/ReportDialog/index.tsx:265 +#: src/components/moderation/ReportDialog/index.tsx:279 #: src/screens/Deactivated.tsx:85 #: src/screens/Settings/components/DeactivateAccountDialog.tsx:59 #: src/view/screens/Storybook/Admonitions.tsx:55 @@ -8898,7 +8930,7 @@ msgstr "" msgid "Something went wrong. Please try again in a moment." msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:219 +#: src/components/moderation/ReportDialog/index.tsx:233 msgid "Something went wrong. Please try again." msgstr "" @@ -9028,6 +9060,8 @@ msgstr "" #: src/components/ageAssurance/AgeAssuranceAppealDialog.tsx:117 #: src/components/ageAssurance/AgeAssuranceAppealDialog.tsx:123 +#: src/components/live/GoLiveDisabledDialog.tsx:138 +#: src/components/live/GoLiveDisabledDialog.tsx:139 #: src/components/moderation/LabelsOnMeDialog.tsx:342 #: src/components/moderation/LabelsOnMeDialog.tsx:343 #: src/screens/Messages/components/ChatDisabled.tsx:154 @@ -9043,9 +9077,9 @@ msgstr "" msgid "Submit Appeal" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:486 -#: src/components/moderation/ReportDialog/index.tsx:547 -#: src/components/moderation/ReportDialog/index.tsx:554 +#: src/components/moderation/ReportDialog/index.tsx:500 +#: src/components/moderation/ReportDialog/index.tsx:561 +#: src/components/moderation/ReportDialog/index.tsx:568 msgid "Submit report" msgstr "" @@ -9172,6 +9206,10 @@ msgstr "" msgid "Tap for more information" msgstr "" +#: src/screens/Signup/StepInfo/index.tsx:336 +msgid "Tap here to confirm your location with GPS." +msgstr "" + #: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:164 msgid "Tap to acknowledge that you understand and agree to these updates and continue using Bluesky" msgstr "" @@ -9198,7 +9236,7 @@ msgstr "" msgid "Task complete - 10 likes!" msgstr "" -#: src/components/ProgressGuide/List.tsx:64 +#: src/components/ProgressGuide/List.tsx:97 msgid "Teach our algorithm what you like" msgstr "" @@ -9218,8 +9256,8 @@ msgstr "" msgid "Tell us a little more" msgstr "" -#: src/view/shell/desktop/RightNav.tsx:119 #: src/view/shell/desktop/RightNav.tsx:120 +#: src/view/shell/desktop/RightNav.tsx:121 msgid "Terms" msgstr "" @@ -9239,6 +9277,7 @@ msgstr "" msgid "Text & tags" msgstr "" +#: src/components/live/GoLiveDisabledDialog.tsx:121 #: src/components/moderation/LabelsOnMeDialog.tsx:306 #: src/screens/Messages/components/ChatDisabled.tsx:120 msgid "Text input field" @@ -9252,8 +9291,8 @@ msgstr "" msgid "Thanks, you have successfully verified your email address. You can close this dialog." msgstr "" -#: src/ageAssurance/components/NoAccessScreen.tsx:385 -#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:113 +#: src/ageAssurance/components/NoAccessScreen.tsx:394 +#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:121 msgid "Thanks! You're all set." msgstr "" @@ -9284,7 +9323,7 @@ msgstr "" #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:186 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:391 -#: src/view/com/profile/ProfileMenu.tsx:483 +#: src/view/com/profile/ProfileMenu.tsx:492 msgid "The account will be able to interact with you after unblocking." msgstr "" @@ -9458,21 +9497,21 @@ msgstr "" msgid "There was an issue updating your feeds, please check your internet connection and try again." msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:397 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:410 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:420 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:423 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:436 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:446 #: src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx:90 #: src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx:101 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:88 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:253 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:279 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:294 -#: src/view/com/profile/ProfileMenu.tsx:136 -#: src/view/com/profile/ProfileMenu.tsx:146 -#: src/view/com/profile/ProfileMenu.tsx:160 -#: src/view/com/profile/ProfileMenu.tsx:170 -#: src/view/com/profile/ProfileMenu.tsx:183 -#: src/view/com/profile/ProfileMenu.tsx:195 +#: src/view/com/profile/ProfileMenu.tsx:139 +#: src/view/com/profile/ProfileMenu.tsx:149 +#: src/view/com/profile/ProfileMenu.tsx:163 +#: src/view/com/profile/ProfileMenu.tsx:173 +#: src/view/com/profile/ProfileMenu.tsx:186 +#: src/view/com/profile/ProfileMenu.tsx:198 msgid "There was an issue! {0}" msgstr "" @@ -9535,6 +9574,7 @@ msgstr "" msgid "This appeal will be sent to <0>{sourceName}." msgstr "" +#: src/components/live/GoLiveDisabledDialog.tsx:113 #: src/screens/Messages/components/ChatDisabled.tsx:116 msgid "This appeal will be sent to Bluesky's moderation service." msgstr "" @@ -9668,7 +9708,7 @@ msgstr "" msgid "This moderation service is unavailable. See below for more details. If this issue persists, contact us." msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:669 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:681 msgid "This post claims to have been created on <0>{0}, but was first seen by Bluesky on <1>{1}." msgstr "" @@ -9684,7 +9724,7 @@ msgstr "" msgid "This post was deleted by its author" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:744 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:770 msgid "This post will be hidden from feeds and threads. This cannot be undone." msgstr "" @@ -9692,15 +9732,15 @@ msgstr "" msgid "This post's author has disabled quote posts." msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:504 +#: src/view/com/profile/ProfileMenu.tsx:513 msgid "This profile is only visible to logged-in users. It won't be visible to people who aren't signed in." msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:779 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:805 msgid "This reply will be sorted into a hidden section at the bottom of your thread and will mute notifications for subsequent replies - both for yourself and others." msgstr "" -#: src/screens/Signup/StepInfo/Policies.tsx:35 +#: src/screens/Signup/StepInfo/Policies.tsx:31 msgid "This service has not provided terms of service or a privacy policy." msgstr "" @@ -9757,7 +9797,7 @@ msgstr "" msgid "This will remove @{0} from the quick access list." msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:769 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:795 msgid "This will remove your post from this quote post for all users, and replace it with a placeholder." msgstr "" @@ -9801,7 +9841,7 @@ msgstr "" msgid "To disable your email 2FA method, please verify your access to <0>{0}" msgstr "" -#: src/ageAssurance/components/NoAccessScreen.tsx:233 +#: src/ageAssurance/components/NoAccessScreen.tsx:242 msgid "To log out, <0>click here." msgstr "" @@ -9852,10 +9892,10 @@ msgstr "" #: src/components/dms/MessageContextMenu.tsx:139 #: src/components/dms/MessageContextMenu.tsx:141 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:476 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:478 -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:591 -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:594 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:502 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:504 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:603 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:606 msgid "Translate" msgstr "" @@ -9864,10 +9904,14 @@ msgstr "" msgid "Tree view" msgstr "" -#: src/view/shell/desktop/SidebarTrendingTopics.tsx:59 +#: src/view/shell/desktop/SidebarTrendingTopics.tsx:48 msgid "Trending" msgstr "" +#: src/view/shell/desktop/SidebarTrendingTopics.tsx:55 +msgid "Trending options" +msgstr "" + #: src/components/interstitials/TrendingVideos.tsx:86 msgid "Trending Videos" msgstr "" @@ -9940,7 +9984,7 @@ msgstr "" msgid "Unapply Pull Request {currentChannel}" msgstr "" -#: src/components/ageAssurance/AgeRestrictedScreen.tsx:40 +#: src/components/ageAssurance/AgeRestrictedScreen.tsx:41 msgid "Unavailable" msgstr "" @@ -9957,7 +10001,7 @@ msgstr "" #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:394 #: src/screens/ProfileList/components/Header.tsx:171 #: src/screens/ProfileList/components/Header.tsx:178 -#: src/view/com/profile/ProfileMenu.tsx:495 +#: src/view/com/profile/ProfileMenu.tsx:504 msgid "Unblock" msgstr "" @@ -9968,14 +10012,14 @@ msgstr "" #: src/components/dms/ConvoMenu.tsx:261 #: src/components/dms/ConvoMenu.tsx:264 -#: src/view/com/profile/ProfileMenu.tsx:400 -#: src/view/com/profile/ProfileMenu.tsx:406 +#: src/view/com/profile/ProfileMenu.tsx:409 +#: src/view/com/profile/ProfileMenu.tsx:415 msgid "Unblock account" msgstr "" #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:184 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:389 -#: src/view/com/profile/ProfileMenu.tsx:477 +#: src/view/com/profile/ProfileMenu.tsx:486 msgid "Unblock Account?" msgstr "" @@ -9984,7 +10028,7 @@ msgstr "" msgid "Unblock list" msgstr "" -#: src/components/PostControls/BookmarkButton.tsx:40 +#: src/components/PostControls/BookmarkButton.tsx:42 msgctxt "Button label to undo saving/removing a post from saved posts." msgid "Undo" msgstr "" @@ -10003,8 +10047,8 @@ msgstr "" msgid "Unfollow {0}" msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:291 -#: src/view/com/profile/ProfileMenu.tsx:301 +#: src/view/com/profile/ProfileMenu.tsx:292 +#: src/view/com/profile/ProfileMenu.tsx:302 msgid "Unfollow account" msgstr "" @@ -10012,14 +10056,18 @@ msgstr "" msgid "Unfollows the user" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:470 +#: src/components/moderation/ReportDialog/index.tsx:484 msgid "Unfortunately, none of your subscribed labelers supports this report type." msgstr "" -#: src/ageAssurance/components/NoAccessScreen.tsx:188 +#: src/ageAssurance/components/NoAccessScreen.tsx:197 msgid "Unfortunately, the birthdate you have saved to your profile makes you too young to access Bluesky." msgstr "" +#: src/ageAssurance/components/NoAccessScreen.tsx:182 +msgid "Unfortunately, your declared age indicates that you are not old enough to access Bluesky in your region." +msgstr "" + #: src/components/verification/VerificationsDialog.tsx:211 msgid "Unknown verifier" msgstr "" @@ -10037,7 +10085,7 @@ msgid "Unlike" msgstr "" #. Accessibility label for the like button when the post has been liked, verb followed by number of likes and noun -#: src/components/PostControls/index.tsx:260 +#: src/components/PostControls/index.tsx:276 msgid "Unlike ({0, plural, one {# like} other {# likes}})" msgstr "" @@ -10057,10 +10105,10 @@ msgstr "" msgid "Unmute {tag}" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:659 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:665 -#: src/view/com/profile/ProfileMenu.tsx:379 -#: src/view/com/profile/ProfileMenu.tsx:385 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:685 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:691 +#: src/view/com/profile/ProfileMenu.tsx:388 +#: src/view/com/profile/ProfileMenu.tsx:394 msgid "Unmute account" msgstr "" @@ -10073,8 +10121,8 @@ msgstr "" msgid "Unmute list" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:544 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:548 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:570 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:574 msgid "Unmute thread" msgstr "" @@ -10102,8 +10150,8 @@ msgstr "" msgid "Unpin from home" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:451 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:458 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:477 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:484 msgid "Unpin from profile" msgstr "" @@ -10175,12 +10223,12 @@ msgstr "" msgid "Update your email" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:318 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:344 msgctxt "toast" msgid "Updating quote attachment failed" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:370 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:396 msgctxt "toast" msgid "Updating reply visibility failed" msgstr "" @@ -10368,13 +10416,13 @@ msgstr "" #: src/components/verification/VerificationCreatePrompt.tsx:84 #: src/components/verification/VerificationCreatePrompt.tsx:86 -#: src/view/com/profile/ProfileMenu.tsx:363 -#: src/view/com/profile/ProfileMenu.tsx:366 +#: src/view/com/profile/ProfileMenu.tsx:372 +#: src/view/com/profile/ProfileMenu.tsx:375 msgid "Verify account" msgstr "" -#: src/ageAssurance/components/NoAccessScreen.tsx:322 -#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:168 +#: src/ageAssurance/components/NoAccessScreen.tsx:331 +#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:176 msgid "Verify again" msgstr "" @@ -10400,10 +10448,10 @@ msgstr "" msgid "Verify email dialog" msgstr "" -#: src/ageAssurance/components/NoAccessScreen.tsx:310 -#: src/ageAssurance/components/NoAccessScreen.tsx:324 -#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:156 -#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:170 +#: src/ageAssurance/components/NoAccessScreen.tsx:319 +#: src/ageAssurance/components/NoAccessScreen.tsx:333 +#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:164 +#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:178 msgid "Verify now" msgstr "" @@ -10525,7 +10573,7 @@ msgstr "" msgid "View {displayName}'s profile" msgstr "" -#: src/components/ProfileHoverCard/index.web.tsx:480 +#: src/components/ProfileHoverCard/index.web.tsx:481 msgid "View blocked user's profile" msgstr "" @@ -10565,9 +10613,9 @@ msgstr "" msgid "View post" msgstr "" -#: src/components/ProfileHoverCard/index.web.tsx:466 -#: src/components/ProfileHoverCard/index.web.tsx:486 -#: src/components/ProfileHoverCard/index.web.tsx:513 +#: src/components/ProfileHoverCard/index.web.tsx:467 +#: src/components/ProfileHoverCard/index.web.tsx:487 +#: src/components/ProfileHoverCard/index.web.tsx:514 #: src/view/com/posts/PostFeedErrorMessage.tsx:182 #: src/view/com/util/PostMeta.tsx:90 #: src/view/com/util/PostMeta.tsx:127 @@ -10663,8 +10711,8 @@ msgstr "" msgid "Warn content and filter from feeds" msgstr "" -#: src/components/live/LiveStatusDialog.tsx:156 -#: src/components/live/LiveStatusDialog.tsx:169 +#: src/components/live/LiveStatusDialog.tsx:172 +#: src/components/live/LiveStatusDialog.tsx:185 msgid "Watch now" msgstr "" @@ -10740,6 +10788,10 @@ msgstr "" msgid "We were unable to determine if you are allowed to upload videos. Please try again." msgstr "" +#: src/components/ageAssurance/AgeAssuranceErrors.tsx:18 +msgid "We were unable to load the age assurance configuration for your region, probably due to a network error. Some content and features may be unavailable temporarily. Please try again later." +msgstr "" + #: src/components/dialogs/BirthDateSettings.tsx:67 msgid "We were unable to load your birthdate preferences. Please try again." msgstr "" @@ -10787,7 +10839,7 @@ msgid "We're having issues initializing the age assurance process for your accou msgstr "" #: src/components/dialogs/SearchablePeopleList.tsx:107 -#: src/components/ProgressGuide/FollowDialog.tsx:184 +#: src/components/ProgressGuide/FollowDialog.tsx:189 msgid "We're having network issues, try again" msgstr "" @@ -10799,8 +10851,8 @@ msgstr "" msgid "We're so excited to have you join us!" msgstr "" -#: src/ageAssurance/components/NoAccessScreen.tsx:378 -#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:106 +#: src/ageAssurance/components/NoAccessScreen.tsx:387 +#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:114 msgid "We're sorry, but based on your device's location, you are currently located in a region that requires age assurance." msgstr "" @@ -10910,27 +10962,31 @@ msgstr "" msgid "Why are you appealing?" msgstr "" -#: src/components/moderation/ReportDialog/copy.ts:46 +#: src/components/moderation/ReportDialog/copy.ts:52 msgid "Why should this conversation be reviewed?" msgstr "" -#: src/components/moderation/ReportDialog/copy.ts:32 +#: src/components/moderation/ReportDialog/copy.ts:38 msgid "Why should this feed be reviewed?" msgstr "" -#: src/components/moderation/ReportDialog/copy.ts:26 +#: src/components/moderation/ReportDialog/copy.ts:32 msgid "Why should this list be reviewed?" msgstr "" -#: src/components/moderation/ReportDialog/copy.ts:52 +#: src/components/moderation/ReportDialog/copy.ts:20 +msgid "Why should this livestream be reviewed?" +msgstr "" + +#: src/components/moderation/ReportDialog/copy.ts:58 msgid "Why should this message be reviewed?" msgstr "" -#: src/components/moderation/ReportDialog/copy.ts:20 +#: src/components/moderation/ReportDialog/copy.ts:26 msgid "Why should this post be reviewed?" msgstr "" -#: src/components/moderation/ReportDialog/copy.ts:38 +#: src/components/moderation/ReportDialog/copy.ts:44 msgid "Why should this starter pack be reviewed?" msgstr "" @@ -10987,11 +11043,11 @@ msgstr "" msgid "Yes, delete this starter pack" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:772 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:798 msgid "Yes, detach" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:782 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:808 msgid "Yes, hide" msgstr "" @@ -11015,8 +11071,12 @@ msgstr "" msgid "You are creating an account on" msgstr "" -#: src/ageAssurance/components/NoAccessScreen.tsx:292 -#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:126 +#: src/components/live/GoLiveDisabledDialog.tsx:107 +msgid "You are currently blocked from using the Go Live feature. To appeal this moderation decision, please submit the form below." +msgstr "" + +#: src/ageAssurance/components/NoAccessScreen.tsx:301 +#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:134 msgid "You are currently unable to access Bluesky's Age Assurance flow. Please <0>contact our moderation team if you believe this is an error." msgstr "" @@ -11086,7 +11146,7 @@ msgstr "" msgid "You can continue ongoing conversations regardless of which setting you choose." msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:357 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:383 msgctxt "toast" msgid "You can hide a maximum of {MAX_HIDDEN_REPLIES} replies." msgstr "" @@ -11117,9 +11177,9 @@ msgstr "" msgid "You can select up to {MAX_IMAGES, plural, other {# images}} in total." msgstr "" -#: src/components/interstitials/Trending.tsx:130 +#: src/components/interstitials/Trending.tsx:129 #: src/components/interstitials/TrendingVideos.tsx:137 -#: src/view/shell/desktop/SidebarTrendingTopics.tsx:110 +#: src/view/shell/desktop/SidebarTrendingTopics.tsx:128 msgid "You can update this later from your settings." msgstr "" @@ -11270,8 +11330,12 @@ msgstr "" msgid "You may only add up to 3 feeds" msgstr "" -#: src/screens/Signup/StepInfo/Policies.tsx:104 -msgid "You must be 13 years of age or older to create an account." +#: src/screens/Signup/StepInfo/index.tsx:322 +msgid "You must be {0} years of age or older to create an account in your region." +msgstr "" + +#: src/screens/Signup/StepInfo/index.tsx:317 +msgid "You must be {MIN_ACCESS_AGE} years of age or older to create an account." msgstr "" #: src/components/dialogs/BirthDateSettings.tsx:178 @@ -11282,7 +11346,7 @@ msgstr "" msgid "You must be following at least seven other people to generate a starter pack." msgstr "" -#: src/components/ageAssurance/AgeRestrictedScreen.tsx:53 +#: src/components/ageAssurance/AgeRestrictedScreen.tsx:60 msgid "You must complete age assurance in order to access this screen." msgstr "" @@ -11328,11 +11392,11 @@ msgstr "" msgid "You will no longer receive notifications for {0}" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:217 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:231 msgid "You will no longer receive notifications for this thread" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:213 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:221 msgid "You will now receive notifications for this thread" msgstr "" @@ -11462,7 +11526,7 @@ msgstr "" msgid "Your appeal has been submitted. If your appeal succeeds, you will receive an email." msgstr "" -#: src/screens/Signup/StepInfo/index.tsx:260 +#: src/screens/Signup/StepInfo/index.tsx:289 msgid "Your birth date" msgstr "" @@ -11507,7 +11571,7 @@ msgstr "" #: src/screens/Login/ForgotPasswordForm.tsx:51 #: src/screens/Settings/components/ChangePasswordDialog.tsx:81 #: src/screens/Signup/state.ts:273 -#: src/screens/Signup/StepInfo/index.tsx:100 +#: src/screens/Signup/StepInfo/index.tsx:129 msgid "Your email appears to be invalid." msgstr "" @@ -11552,6 +11616,10 @@ msgstr "" msgid "Your location data is not tracked and does not leave your device." msgstr "" +#: src/screens/Signup/StepInfo/index.tsx:367 +msgid "Your location has been updated." +msgstr "" + #: src/components/dialogs/MutedWords.tsx:374 msgid "Your muted words" msgstr "" @@ -11560,7 +11628,7 @@ msgstr "" msgid "Your password has been changed successfully! Please use your new password when you sign in to Bluesky from now on." msgstr "" -#: src/screens/Signup/StepInfo/index.tsx:129 +#: src/screens/Signup/StepInfo/index.tsx:158 msgid "Your password must be at least 8 characters long." msgstr "" @@ -11593,7 +11661,7 @@ msgstr "" msgid "Your reply was sent" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:497 +#: src/components/moderation/ReportDialog/index.tsx:511 msgid "Your report will be sent to <0>{0}." msgstr "" diff --git a/src/logger/index.ts b/src/logger/index.ts index 998d025818..57d430b5dd 100644 --- a/src/logger/index.ts +++ b/src/logger/index.ts @@ -16,6 +16,8 @@ import {enabledLogLevels} from '#/logger/util' import {isNative} from '#/platform/detection' import {ENV} from '#/env' +export {type MetricEvents as Metrics} from '#/logger/metrics' + const TRANSPORTS: Transport[] = (function configureTransports() { switch (ENV) { case 'production': { diff --git a/src/logger/metrics.ts b/src/logger/metrics.ts index 949c883b7c..7afe7f3bbe 100644 --- a/src/logger/metrics.ts +++ b/src/logger/metrics.ts @@ -1,5 +1,6 @@ import {type NotificationReason} from '#/lib/hooks/useNotificationHandler' import {type FeedDescriptor} from '#/state/queries/post-feed' +import {type LiveEventFeedMetricContext} from '#/features/liveEvents/types' export type MetricEvents = { // App events @@ -176,13 +177,19 @@ export type MetricEvents = { 'feed:suggestion:press': { feedUrl: string } - 'feed:showMore': { - feed: string - feedContext: string + 'post:showMore': { + uri: string + authorDid: string + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + feedDescriptor?: string + position?: number } - 'feed:showLess': { - feed: string - feedContext: string + 'post:showLess': { + uri: string + authorDid: string + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + feedDescriptor?: string + position?: number } 'feed:clickthrough': { feed: string @@ -238,6 +245,8 @@ export type MetricEvents = { isReply: boolean } 'post:like': { + uri: string + authorDid: string doesLikerFollowPoster: boolean | undefined doesPosterFollowLiker: boolean | undefined likerClout: number | undefined @@ -246,26 +255,87 @@ export type MetricEvents = { feedDescriptor?: string } 'post:repost': { + uri: string + authorDid: string logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' feedDescriptor?: string } 'post:unlike': { + uri: string + authorDid: string logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' feedDescriptor?: string } 'post:unrepost': { + uri: string + authorDid: string logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' feedDescriptor?: string } - 'post:mute': {} - 'post:unmute': {} + 'post:mute': { + uri: string + authorDid: string + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + feedDescriptor?: string + position?: number + } + 'post:unmute': { + uri: string + authorDid: string + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + feedDescriptor?: string + position?: number + } 'post:pin': {} 'post:unpin': {} 'post:bookmark': { + uri: string + authorDid: string logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + feedDescriptor?: string + position?: number } 'post:unbookmark': { + uri: string + authorDid: string logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + feedDescriptor?: string + position?: number + } + 'post:clickReply': { + uri: string + authorDid: string + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + feedDescriptor?: string + position?: number + } + 'post:clickQuotePost': { + uri: string + authorDid: string + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + feedDescriptor?: string + position?: number + } + 'post:clickthroughAuthor': { + uri: string + authorDid: string + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + feedDescriptor?: string + position?: number + } + 'post:clickthroughItem': { + uri: string + authorDid: string + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + feedDescriptor?: string + position?: number + } + 'post:clickthroughEmbed': { + uri: string + authorDid: string + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + feedDescriptor?: string + position?: number } 'post:view': { uri: string @@ -565,7 +635,14 @@ export type MetricEvents = { 'live:view:profile': {subject: string} 'live:view:post': {subject: string; feed?: string} - 'share:open': {context: 'feed' | 'thread'} + 'post:share': { + uri: string + authorDid: string + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + feedDescriptor?: string + postContext: 'feed' | 'thread' + position?: number + } 'share:press:copyLink': {} 'share:press:nativeShare': {} 'share:press:openDmSearch': {} @@ -720,4 +797,27 @@ export type MetricEvents = { } // user pressed the remove all data button 'contacts:settings:removeData': {} + + 'liveEvents:feedBanner:seen': { + feed: string + context: LiveEventFeedMetricContext + } + 'liveEvents:feedBanner:click': { + feed: string + context: LiveEventFeedMetricContext + } + 'liveEvents:feedBanner:hide': { + feed: string + context: LiveEventFeedMetricContext + } + 'liveEvents:feedBanner:unhide': { + feed: string + context: LiveEventFeedMetricContext + } + 'liveEvents:hideAllFeedBanners': { + context: LiveEventFeedMetricContext + } + 'liveEvents:unhideAllFeedBanners': { + context: LiveEventFeedMetricContext + } } diff --git a/src/screens/PostThread/components/ThreadItemAnchor.tsx b/src/screens/PostThread/components/ThreadItemAnchor.tsx index 7785afe2ca..fbed7965b1 100644 --- a/src/screens/PostThread/components/ThreadItemAnchor.tsx +++ b/src/screens/PostThread/components/ThreadItemAnchor.tsx @@ -281,6 +281,12 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({ ]) const onOpenAuthor = () => { + logger.metric('post:clickthroughAuthor', { + uri: post.uri, + authorDid: post.author.did, + logContext: 'PostThreadItem', + feedDescriptor: feedFeedback.feedDescriptor, + }) if (postSource) { feedFeedback.sendInteraction({ item: post.uri, @@ -292,6 +298,12 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({ } const onOpenEmbed = () => { + logger.metric('post:clickthroughEmbed', { + uri: post.uri, + authorDid: post.author.did, + logContext: 'PostThreadItem', + feedDescriptor: feedFeedback.feedDescriptor, + }) if (postSource) { feedFeedback.sendInteraction({ item: post.uri, diff --git a/src/screens/Search/Explore.tsx b/src/screens/Search/Explore.tsx index b3e26dcfd3..6b49588db9 100644 --- a/src/screens/Search/Explore.tsx +++ b/src/screens/Search/Explore.tsx @@ -68,6 +68,7 @@ import {Loader} from '#/components/Loader' import * as ProfileCard from '#/components/ProfileCard' import {SubtleHover} from '#/components/SubtleHover' import {Text} from '#/components/Typography' +import {ExploreScreenLiveEventFeedsBanner} from '#/features/liveEvents/components/ExploreScreenLiveEventFeedsBanner' import * as ModuleHeader from './components/ModuleHeader' import { SuggestedAccountsTabBar, @@ -201,6 +202,10 @@ type ExploreScreenItems = type: 'interests-card' key: 'interests-card' } + | { + type: 'liveEventFeedsBanner' + key: string + } export function Explore({ focusSearchInput, @@ -684,6 +689,8 @@ export function Explore({ i.push(topBorder) i.push(...interestsNuxModule) + i.push({type: 'liveEventFeedsBanner', key: 'liveEventFeedsBanner'}) + if (useFullExperience) { i.push(trendingTopicsModule) i.push(...suggestedFeedsModule) @@ -998,6 +1005,9 @@ export function Explore({ case 'interests-card': { return } + case 'liveEventFeedsBanner': { + return + } } }, [ diff --git a/src/screens/Settings/ContentAndMediaSettings.tsx b/src/screens/Settings/ContentAndMediaSettings.tsx index e1144d0645..ebcf95a7af 100644 --- a/src/screens/Settings/ContentAndMediaSettings.tsx +++ b/src/screens/Settings/ContentAndMediaSettings.tsx @@ -26,6 +26,7 @@ import {Play_Stroke2_Corner2_Rounded as PlayIcon} from '#/components/icons/Play' import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending' import {Window_Stroke2_Corner2_Rounded as WindowIcon} from '#/components/icons/Window' import * as Layout from '#/components/Layout' +import {LiveEventFeedsSettingsToggle} from '#/features/liveEvents/components/LiveEventFeedsSettingsToggle' type Props = NativeStackScreenProps< CommonNavigatorParams, @@ -124,7 +125,7 @@ export function ContentAndMediaSettingsScreen({}: Props) { - {trendingEnabled && ( + {trendingEnabled ? ( <> + + ) : ( + <> + + + )} diff --git a/src/state/feed-feedback.tsx b/src/state/feed-feedback.tsx index 1d719978a5..28f13fb5f2 100644 --- a/src/state/feed-feedback.tsx +++ b/src/state/feed-feedback.tsx @@ -137,7 +137,6 @@ export function useFeedFeedback( sendOrAggregateInteractionsForStats( aggregatedStats.current, interactionsToSend, - feed?.feedDescriptor ?? 'unknown', ) throttledFlushAggregatedStats() logger.debug('flushed') @@ -274,28 +273,10 @@ function createAggregatedStats(): AggregatedStats { function sendOrAggregateInteractionsForStats( stats: AggregatedStats, interactions: AppBskyFeedDefs.Interaction[], - feed: string, ) { for (let interaction of interactions) { switch (interaction.event) { - // Pressing "Show more" / "Show less" is relatively uncommon so we won't aggregate them. - // This lets us send the feed context together with them. - case 'app.bsky.feed.defs#requestLess': { - logger.metric('feed:showLess', { - feed, - feedContext: interaction.feedContext ?? '', - }) - break - } - case 'app.bsky.feed.defs#requestMore': { - logger.metric('feed:showMore', { - feed, - feedContext: interaction.feedContext ?? '', - }) - break - } - - // The rest of the events are aggregated and sent later in batches. + // The events are aggregated and sent later in batches. case 'app.bsky.feed.defs#clickthroughAuthor': case 'app.bsky.feed.defs#clickthroughEmbed': case 'app.bsky.feed.defs#clickthroughItem': diff --git a/src/state/queries/post.ts b/src/state/queries/post.ts index 09ced9874e..3043485b3e 100644 --- a/src/state/queries/post.ts +++ b/src/state/queries/post.ts @@ -111,7 +111,7 @@ export function usePostLikeMutationQueue( const postCid = post.cid const initialLikeUri = post.viewer?.like const likeMutation = usePostLikeMutation(feedDescriptor, logContext, post) - const unlikeMutation = usePostUnlikeMutation(feedDescriptor, logContext) + const unlikeMutation = usePostUnlikeMutation(feedDescriptor, logContext, post) const queueToggle = useToggleMutationQueue({ initialState: initialLikeUri, @@ -182,6 +182,8 @@ function usePostLikeMutation( ownProfile = findProfileQueryData(queryClient, currentAccount.did) } logger.metric('post:like', { + uri, + authorDid: postAuthor.did, logContext, doesPosterFollowLiker: postAuthor.viewer ? Boolean(postAuthor.viewer.followedBy) @@ -206,11 +208,17 @@ function usePostLikeMutation( function usePostUnlikeMutation( feedDescriptor: string | undefined, logContext: LogEvents['post:unlike']['logContext'], + post: Shadow, ) { const agent = useAgent() return useMutation({ - mutationFn: ({likeUri}) => { - logger.metric('post:unlike', {logContext, feedDescriptor}) + mutationFn: ({postUri, likeUri}) => { + logger.metric('post:unlike', { + uri: postUri, + authorDid: post.author.did, + logContext, + feedDescriptor, + }) return agent.deleteLike(likeUri) }, }) @@ -227,8 +235,12 @@ export function usePostRepostMutationQueue( const postUri = post.uri const postCid = post.cid const initialRepostUri = post.viewer?.repost - const repostMutation = usePostRepostMutation(feedDescriptor, logContext) - const unrepostMutation = usePostUnrepostMutation(feedDescriptor, logContext) + const repostMutation = usePostRepostMutation(feedDescriptor, logContext, post) + const unrepostMutation = usePostUnrepostMutation( + feedDescriptor, + logContext, + post, + ) const queueToggle = useToggleMutationQueue({ initialState: initialRepostUri, @@ -280,6 +292,7 @@ export function usePostRepostMutationQueue( function usePostRepostMutation( feedDescriptor: string | undefined, logContext: LogEvents['post:repost']['logContext'], + post: Shadow, ) { const agent = useAgent() return useMutation< @@ -288,7 +301,12 @@ function usePostRepostMutation( {uri: string; cid: string; via?: {uri: string; cid: string}} // the post's uri and cid, and the repost uri/cid if present >({ mutationFn: ({uri, cid, via}) => { - logger.metric('post:repost', {logContext, feedDescriptor}) + logger.metric('post:repost', { + uri, + authorDid: post.author.did, + logContext, + feedDescriptor, + }) return agent.repost(uri, cid, via) }, }) @@ -297,11 +315,17 @@ function usePostRepostMutation( function usePostUnrepostMutation( feedDescriptor: string | undefined, logContext: LogEvents['post:unrepost']['logContext'], + post: Shadow, ) { const agent = useAgent() return useMutation({ - mutationFn: ({repostUri}) => { - logger.metric('post:unrepost', {logContext, feedDescriptor}) + mutationFn: ({postUri, repostUri}) => { + logger.metric('post:unrepost', { + uri: postUri, + authorDid: post.author.did, + logContext, + feedDescriptor, + }) return agent.deleteRepost(repostUri) }, }) @@ -373,7 +397,6 @@ function useThreadMuteMutation() { {uri: string} // the root post's uri >({ mutationFn: ({uri}) => { - logger.metric('post:mute', {}) return agent.api.app.bsky.graph.muteThread({root: uri}) }, }) @@ -383,7 +406,6 @@ function useThreadUnmuteMutation() { const agent = useAgent() return useMutation<{}, Error, {uri: string}>({ mutationFn: ({uri}) => { - logger.metric('post:unmute', {}) return agent.api.app.bsky.graph.unmuteThread({root: uri}) }, }) diff --git a/src/state/queries/preferences/const.ts b/src/state/queries/preferences/const.ts index 54d063a502..7508fa4f6d 100644 --- a/src/state/queries/preferences/const.ts +++ b/src/state/queries/preferences/const.ts @@ -45,4 +45,8 @@ export const DEFAULT_LOGGED_OUT_PREFERENCES: UsePreferencesQueryResponse = { verificationPrefs: { hideBadges: false, }, + liveEventPreferences: { + hideAllFeeds: false, + hiddenFeedIds: [], + }, } diff --git a/src/state/queries/profile.ts b/src/state/queries/profile.ts index 9d30288d40..94b362657b 100644 --- a/src/state/queries/profile.ts +++ b/src/state/queries/profile.ts @@ -4,12 +4,14 @@ import { type AppBskyActorGetProfile, type AppBskyActorGetProfiles, type AppBskyActorProfile, + type AppBskyGraphGetFollows, AtUri, type BskyAgent, type ComAtprotoRepoUploadBlob, type Un$Typed, } from '@atproto/api' import { + type InfiniteData, keepPreviousData, type QueryClient, useMutation, @@ -26,6 +28,7 @@ import {type Shadow} from '#/state/cache/types' import {type ImageMeta} from '#/state/gallery' import {STALE} from '#/state/queries' import {resetProfilePostsQueries} from '#/state/queries/post-feed' +import {RQKEY as PROFILE_FOLLOWS_RQKEY} from '#/state/queries/profile-follows' import { unstableCacheProfileView, useUnstableProfileViewCache, @@ -247,6 +250,7 @@ export function useProfileFollowMutationQueue( ) { const agent = useAgent() const queryClient = useQueryClient() + const {currentAccount} = useSession() const did = profile.did const initialFollowingUri = profile.viewer?.following const followMutation = useProfileFollowMutation( @@ -283,6 +287,47 @@ export function useProfileFollowMutationQueue( followingUri: finalFollowingUri, }) + // Optimistically update profile follows cache for avatar displays + if (currentAccount?.did) { + type FollowsQueryData = + InfiniteData + queryClient.setQueryData( + PROFILE_FOLLOWS_RQKEY(currentAccount.did), + old => { + if (!old?.pages?.[0]) return old + if (finalFollowingUri) { + // Add the followed profile to the beginning + const alreadyExists = old.pages[0].follows.some( + f => f.did === profile.did, + ) + if (alreadyExists) return old + return { + ...old, + pages: [ + { + ...old.pages[0], + follows: [ + profile as AppBskyActorDefs.ProfileView, + ...old.pages[0].follows, + ], + }, + ...old.pages.slice(1), + ], + } + } else { + // Remove the unfollowed profile + return { + ...old, + pages: old.pages.map(page => ({ + ...page, + follows: page.follows.filter(f => f.did !== profile.did), + })), + } + } + }, + ) + } + if (finalFollowingUri) { agent.app.bsky.graph .getSuggestedFollowsByActor({ diff --git a/src/view/com/feeds/ComposerPrompt.tsx b/src/view/com/feeds/ComposerPrompt.tsx index be88ee2dc1..5d4685a4e6 100644 --- a/src/view/com/feeds/ComposerPrompt.tsx +++ b/src/view/com/feeds/ComposerPrompt.tsx @@ -148,8 +148,6 @@ export function ComposerPrompt() { a.relative, a.flex_row, a.align_start, - a.border_t, - t.atoms.border_contrast_low, { paddingLeft: 18, paddingRight: 15, diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index 4f25468c95..9e6fcdcccb 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -70,6 +70,7 @@ import { } from '#/components/feeds/PostFeedVideoGridRow' import {TrendingInterstitial} from '#/components/interstitials/Trending' import {TrendingVideos as TrendingVideosInterstitial} from '#/components/interstitials/TrendingVideos' +import {DiscoverFeedLiveEventFeedsAndTrendingBanner} from '#/features/liveEvents/components/DiscoverFeedLiveEventFeedsAndTrendingBanner' import {ComposerPrompt} from '../feeds/ComposerPrompt' import {DiscoverFallbackHeader} from './DiscoverFallbackHeader' import {FeedShutdownMsg} from './FeedShutdownMsg' @@ -155,6 +156,10 @@ type FeedRow = type: 'composerPrompt' key: string } + | { + type: 'liveEventFeedsAndTrendingBanner' + key: string + } export function getItemsForFeedback(feedRow: FeedRow): { item: FeedPostSliceItem @@ -360,7 +365,7 @@ let PostFeed = ({ const showProgressIntersitial = (followProgressGuide || followAndLikeProgressGuide) && !rightNavVisible - const {trendingDisabled, trendingVideoDisabled} = useTrendingSettings() + const {trendingVideoDisabled} = useTrendingSettings() const ageAssuranceBannerState = useAgeAssuranceBannerState() const selectedFeed = useSelectedFeed() @@ -510,13 +515,10 @@ let PostFeed = ({ }) } } - if (!rightNavVisible && !trendingDisabled) { - arr.push({ - type: 'interstitialTrending', - key: - 'interstitial2-' + sliceIndex + '-' + lastFetchedAt, - }) - } + arr.push({ + type: 'liveEventFeedsAndTrendingBanner', + key: 'liveEventFeedsAndTrendingBanner-' + sliceIndex, + }) // Show composer prompt for Discover and Following feeds if ( hasSession && @@ -672,9 +674,7 @@ let PostFeed = ({ feedTab, hasSession, showProgressIntersitial, - trendingDisabled, trendingVideoDisabled, - rightNavVisible, gtMobile, isVideoFeed, areVideoFeedsEnabled, @@ -773,6 +773,8 @@ let PostFeed = ({ return } else if (row.type === 'interstitialTrending') { return + } else if (row.type === 'liveEventFeedsAndTrendingBanner') { + return } else if (row.type === 'composerPrompt') { return } else if (row.type === 'interstitialTrendingVideos') { diff --git a/src/view/com/posts/PostFeedItem.tsx b/src/view/com/posts/PostFeedItem.tsx index 244ebde05f..5b6ce7841a 100644 --- a/src/view/com/posts/PostFeedItem.tsx +++ b/src/view/com/posts/PostFeedItem.tsx @@ -21,6 +21,7 @@ import {makeProfileLink} from '#/lib/routes/links' import {type NavigationProp} from '#/lib/routes/types' import {useGate} from '#/lib/statsig/statsig' import {countLines} from '#/lib/strings/helpers' +import {logger} from '#/logger' import { POST_TOMBSTONE, type Shadow, @@ -173,7 +174,8 @@ let FeedItemInner = ({ const urip = new AtUri(post.uri) return [makeProfileLink(post.author, 'post', urip.rkey), urip.rkey] }, [post.uri, post.author]) - const {sendInteraction, feedSourceInfo} = useFeedFeedbackContext() + const {sendInteraction, feedSourceInfo, feedDescriptor} = + useFeedFeedbackContext() const onPressReply = () => { sendInteraction({ @@ -209,6 +211,12 @@ let FeedItemInner = ({ feedContext, reqId, }) + logger.metric('post:clickthroughAuthor', { + uri: post.uri, + authorDid: post.author.did, + logContext: 'FeedItem', + feedDescriptor, + }) } const onOpenReposter = () => { @@ -227,6 +235,12 @@ let FeedItemInner = ({ feedContext, reqId, }) + logger.metric('post:clickthroughEmbed', { + uri: post.uri, + authorDid: post.author.did, + logContext: 'FeedItem', + feedDescriptor, + }) } const onBeforePress = () => { @@ -236,6 +250,12 @@ let FeedItemInner = ({ feedContext, reqId, }) + logger.metric('post:clickthroughItem', { + uri: post.uri, + authorDid: post.author.did, + logContext: 'FeedItem', + feedDescriptor, + }) unstableCacheProfileView(queryClient, post.author) setUnstablePostSource(buildPostSourceKey(post.uri, post.author.handle), { feedSourceInfo, diff --git a/src/view/shell/desktop/Feeds.tsx b/src/view/shell/desktop/Feeds.tsx index 641b90f3e3..57b078e0c1 100644 --- a/src/view/shell/desktop/Feeds.tsx +++ b/src/view/shell/desktop/Feeds.tsx @@ -1,4 +1,4 @@ -import {View} from 'react-native' +import {Pressable, View} from 'react-native' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigation, useNavigationState} from '@react-navigation/native' @@ -7,10 +7,18 @@ import {getCurrentRoute} from '#/lib/routes/helpers' import {type NavigationProp} from '#/lib/routes/types' import {logger} from '#/logger' import {emitSoftReset} from '#/state/events' -import {usePinnedFeedsInfos} from '#/state/queries/feed' +import { + type SavedFeedSourceInfo, + usePinnedFeedsInfos, +} from '#/state/queries/feed' import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed' +import {UserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, useTheme, web} from '#/alf' -import {createStaticClick, InlineLinkText} from '#/components/Link' +import {useInteractionState} from '#/components/hooks/useInteractionState' +import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline' +import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' +import {Link} from '#/components/Link' +import {Text} from '#/components/Typography' export function DesktopFeeds() { const t = useTheme() @@ -57,13 +65,12 @@ export function DesktopFeeds() { style={[ a.flex_1, web({ - gap: 10, + gap: 2, /* * Small padding prevents overflow prior to actually overflowing the * height of the screen with lots of feeds. */ - paddingVertical: 2, - marginHorizontal: -2, + paddingTop: 2, overflowY: 'auto', }), ]}> @@ -72,10 +79,11 @@ export function DesktopFeeds() { const current = route.name === 'Home' && feed === selectedFeed return ( - { + feedInfo={feedInfo} + current={current} + onPress={() => { logger.metric( 'desktopFeeds:feed:click', { @@ -89,39 +97,143 @@ export function DesktopFeeds() { if (route.name === 'Home' && feed === selectedFeed) { emitSoftReset() } - })} - style={[ - a.text_md, - a.leading_snug, - a.flex_shrink_0, - current - ? [a.font_semi_bold, t.atoms.text] - : [t.atoms.text_contrast_medium], - web({ - marginHorizontal: 2, - width: 'calc(100% - 4px)', - }), - ]} - numberOfLines={1}> - {feedInfo.displayName} - + }} + /> ) })} - - {_(msg`More feeds`)} - + a.flex_row, + a.align_center, + a.gap_sm, + a.self_start, + a.rounded_sm, + {paddingVertical: 6, paddingHorizontal: 8}, + route.name === 'Feeds' && {backgroundColor: t.palette.primary_50}, + ]}> + {({hovered}) => { + const isActive = route.name === 'Feeds' + return ( + <> + + + + + {_(msg`More feeds`)} + + + ) + }} + ) } + +function FeedItem({ + feedInfo, + current, + onPress, +}: { + feedInfo: SavedFeedSourceInfo + current: boolean + onPress: () => void +}) { + const t = useTheme() + const {_} = useLingui() + const { + state: hovered, + onIn: onHoverIn, + onOut: onHoverOut, + } = useInteractionState() + const isFollowing = feedInfo.feedDescriptor === 'following' + + return ( + + {isFollowing ? ( + + + + ) : ( + + )} + + {feedInfo.displayName} + + + ) +} diff --git a/src/view/shell/desktop/RightNav.tsx b/src/view/shell/desktop/RightNav.tsx index 1d097fc9a8..04dddaf739 100644 --- a/src/view/shell/desktop/RightNav.tsx +++ b/src/view/shell/desktop/RightNav.tsx @@ -18,11 +18,11 @@ import { web, } from '#/alf' import {AppLanguageDropdown} from '#/components/AppLanguageDropdown' -import {Divider} from '#/components/Divider' import {CENTER_COLUMN_OFFSET} from '#/components/Layout' import {InlineLinkText} from '#/components/Link' import {ProgressGuideList} from '#/components/ProgressGuide/List' import {Text} from '#/components/Typography' +import {SidebarLiveEventFeedsBanner} from '#/features/liveEvents/components/SidebarLiveEventFeedsBanner' function useWebQueryParams() { const navigation = useNavigation() @@ -50,7 +50,8 @@ export function DesktopRightNav({routeName}: {routeName: string}) { const isSearchScreen = routeName === 'Search' const webqueryParams = useWebQueryParams() const searchQuery = webqueryParams?.q - const showTrending = !isSearchScreen || (isSearchScreen && !!searchQuery) + const showExploreScreenDuplicatedContent = + !isSearchScreen || (isSearchScreen && !!searchQuery) const {rightNavVisible, centerColumnOffset, leftNavMinimal} = useLayoutBreakpoints() @@ -86,13 +87,13 @@ export function DesktopRightNav({routeName}: {routeName: string}) { {hasSession && ( <> - - + )} - {showTrending && } + {showExploreScreenDuplicatedContent && } + {showExploreScreenDuplicatedContent && } {hasSession && ( @@ -102,25 +103,31 @@ export function DesktopRightNav({routeName}: {routeName: string}) { email: currentAccount?.email, handle: currentAccount?.handle, })} + style={[t.atoms.text_contrast_medium]} label={_(msg`Feedback`)}> {_(msg`Feedback`)} - {' • '} + {' ∙ '} )} {_(msg`Privacy`)} - {' • '} + {' ∙ '} {_(msg`Terms`)} - {' • '} - + {' ∙ '} + {_(msg`Help`)} diff --git a/src/view/shell/desktop/SidebarTrendingTopics.tsx b/src/view/shell/desktop/SidebarTrendingTopics.tsx index 11dcff3a4d..913b9ccab9 100644 --- a/src/view/shell/desktop/SidebarTrendingTopics.tsx +++ b/src/view/shell/desktop/SidebarTrendingTopics.tsx @@ -1,9 +1,8 @@ -import React from 'react' import {View} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {logEvent} from '#/lib/statsig/statsig' +import {logger} from '#/logger' import { useTrendingSettings, useTrendingSettingsApi, @@ -12,18 +11,13 @@ import {useTrendingTopics} from '#/state/queries/trending/useTrendingTopics' import {useTrendingConfig} from '#/state/service-config' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonIcon} from '#/components/Button' -import {Divider} from '#/components/Divider' -import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' -import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending' +import {DotGrid_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid' +import {Trending3_Stroke2_Corner1_Rounded as TrendingIcon} from '#/components/icons/Trending' import * as Prompt from '#/components/Prompt' -import { - TrendingTopic, - TrendingTopicLink, - TrendingTopicSkeleton, -} from '#/components/TrendingTopics' +import {TrendingTopicLink} from '#/components/TrendingTopics' import {Text} from '#/components/Typography' -const TRENDING_LIMIT = 6 +const TRENDING_LIMIT = 5 export function SidebarTrendingTopics() { const {enabled} = useTrendingConfig() @@ -39,64 +33,88 @@ function Inner() { const {data: trending, error, isLoading} = useTrendingTopics() const noTopics = !isLoading && !error && !trending?.topics?.length - const onConfirmHide = React.useCallback(() => { - logEvent('trendingTopics:hide', {context: 'sidebar'}) + const onConfirmHide = () => { + logger.metric('trendingTopics:hide', {context: 'sidebar'}) setTrendingDisabled(true) - }, [setTrendingDisabled]) + } return error || noTopics ? null : ( <> - - - - + + + + Trending - + {isLoading ? ( Array(TRENDING_LIMIT) .fill(0) .map((_n, i) => ( - + + + {i + 1}. + + + )) ) : !trending?.topics ? null : ( <> - {trending.topics.slice(0, TRENDING_LIMIT).map(topic => ( + {trending.topics.slice(0, TRENDING_LIMIT).map((topic, i) => ( { - logEvent('trendingTopic:click', {context: 'sidebar'}) + logger.metric('trendingTopic:click', {context: 'sidebar'}) }}> {({hovered}) => ( - + + + {i + 1}. + + + {topic.displayName ?? topic.topic} + + )} ))} @@ -111,7 +129,6 @@ function Inner() { confirmButtonCta={_(msg`Hide`)} onConfirm={onConfirmHide} /> - ) } diff --git a/yarn.lock b/yarn.lock index a010c615be..ff0610ac4b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -82,12 +82,12 @@ "@atproto/xrpc" "^0.7.6" "@atproto/xrpc-server" "^0.10.0" -"@atproto/api@^0.18.13": - version "0.18.13" - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.18.13.tgz#63eee310e6715752eb87748323cf9ab57dd91e4b" - integrity sha512-CULZ01pSJDltLS/Gc9MMrhFzB6OM3ezyZw7KoeLT/sBfwgA1ddA4mWdTh7DIRosPRigXtA05bnoiCutZbQDo+Q== +"@atproto/api@^0.18.15": + version "0.18.15" + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.18.15.tgz#25ce82081216bdefbf5397220de76ac3ba9e3f5d" + integrity sha512-GeaTP7HMRZa8jD6trMuTACa8t2jkFtRmcwWgrB0FT7l9jVCXrKpYupWeIeauEgWHNwWUUiaq3LmCox+HBy8ZMQ== dependencies: - "@atproto/common-web" "^0.4.11" + "@atproto/common-web" "^0.4.12" "@atproto/lexicon" "^0.6.0" "@atproto/syntax" "^0.4.2" "@atproto/xrpc" "^0.7.7" @@ -208,13 +208,13 @@ pino-http "^8.2.1" typed-emitter "^2.1.0" -"@atproto/common-web@^0.4.11": - version "0.4.11" - resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.4.11.tgz#eb41dc02c1ea4221388630e193d181fb098186e0" - integrity sha512-VHejNmSABU8/03VrQ3e36AmT5U3UIeio+qSUqCrO1oNgrJcWfGy1rpj0FVtUugWF8Un29+yzkukzWGZfXL70rQ== +"@atproto/common-web@^0.4.12": + version "0.4.12" + resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.4.12.tgz#04135bef480d9e12cfef124ee45d8236764e7509" + integrity sha512-3aCJemqM/fkHQrVPbTCHCdiVstKFI+2LkFLvUhO6XZP0EqUZa/rg/CIZBKTFUWu9I5iYiaEiXL9VwcDRpEevSw== dependencies: - "@atproto/lex-data" "0.0.7" - "@atproto/lex-json" "0.0.7" + "@atproto/lex-data" "0.0.8" + "@atproto/lex-json" "0.0.8" zod "^3.23.8" "@atproto/common-web@^0.4.4", "@atproto/common-web@^0.4.6": @@ -415,10 +415,10 @@ uint8arrays "3.0.0" unicode-segmenter "^0.14.0" -"@atproto/lex-data@0.0.7": - version "0.0.7" - resolved "https://registry.yarnpkg.com/@atproto/lex-data/-/lex-data-0.0.7.tgz#6aa87423f6d47849bec8ff3ca0b00ce93964adc8" - integrity sha512-W/Q5o9o7n2Sv3UywckChu01X5lwQUtaiiOkGJLnRsdkQTyC6813nPgY+p2sG7NwwM+82lu+FUV9fE/Ul3VqaJw== +"@atproto/lex-data@0.0.8": + version "0.0.8" + resolved "https://registry.yarnpkg.com/@atproto/lex-data/-/lex-data-0.0.8.tgz#46cc261efbfa6cc05bf04439d2d73cd8386b467d" + integrity sha512-1Y5tz7BkS7380QuLNXaE8GW8Xba+mRWugt8BKM4BUFYjjUZdmirU8lr72iM4XlEBrzRu8Cfvj+MbsbYaZv+IgA== dependencies: "@atproto/syntax" "0.4.2" multiformats "^9.9.0" @@ -451,12 +451,12 @@ "@atproto/lex-data" "0.0.3" tslib "^2.8.1" -"@atproto/lex-json@0.0.7": - version "0.0.7" - resolved "https://registry.yarnpkg.com/@atproto/lex-json/-/lex-json-0.0.7.tgz#c06e1fc3e06d739bbb74694f5d846055bed37866" - integrity sha512-bjNPD5M/MhLfjNM7tcxuls80UgXpHqxdOxDXEUouAtZQV/nIDhGjmNUvKxOmOgnDsiZRnT2g5y3onrnjH3a44g== +"@atproto/lex-json@0.0.8": + version "0.0.8" + resolved "https://registry.yarnpkg.com/@atproto/lex-json/-/lex-json-0.0.8.tgz#03290762d9368b029488ee0a0766d1a34063255c" + integrity sha512-w1Qmkae1QhmNz+i1Zm3xr3jp0UPPRENmdlpU0qIrdxWDo9W4Mzkeyc3eSoa+Zs+zN8xkRSQw7RLZte/B7Ipdwg== dependencies: - "@atproto/lex-data" "0.0.7" + "@atproto/lex-data" "0.0.8" tslib "^2.8.1" "@atproto/lex-resolver@0.0.5": @@ -6144,20 +6144,13 @@ resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.1.1.tgz#78244efe12930c56fd255d7923865857c41ac8cb" integrity sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw== -"@react-native-async-storage/async-storage@2.2.0": +"@react-native-async-storage/async-storage@2.2.0", "@react-native-async-storage/async-storage@^1.15.2": version "2.2.0" resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz#a3aa565253e46286655560172f4e366e8969f5ad" integrity sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw== dependencies: merge-options "^3.0.4" -"@react-native-async-storage/async-storage@^1.15.2": - version "1.22.0" - resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-1.22.0.tgz#202a9afd15a5b829c39b709d0ca3942612441efc" - integrity sha512-b5KD010iiZnot86RbAaHpLuHwmPW2qA3SSN/OSZhd1kBoINEQEVBuv+uFtcaTxAhX27bT0wd13GOb2IOSDUXSA== - dependencies: - merge-options "^3.0.4" - "@react-native/assets-registry@0.81.5": version "0.81.5" resolved "https://registry.yarnpkg.com/@react-native/assets-registry/-/assets-registry-0.81.5.tgz#d22c924fa6f6d4a463c5af34ce91f38756c0fa7d"