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 more0>"
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 Policy0>."
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 Service0> and <1>Privacy Policy1>."
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 Service0>."
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.0>"
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.0>"
msgstr ""
+#: src/screens/Signup/StepInfo/index.tsx:332
+msgid "Have we got your location wrong? <0>Tap here to confirm your location with GPS.0>"
+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 here0>."
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.0>"
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.0>"
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.social0>"
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}0>."
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}0>, but was first seen by Bluesky on <1>{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}0>"
msgstr ""
-#: src/ageAssurance/components/NoAccessScreen.tsx:233
+#: src/ageAssurance/components/NoAccessScreen.tsx:242
msgid "To log out, <0>click here0>."
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 team0> 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}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"