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/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/assets/images/live_now_beta.webp b/assets/images/live_now_beta.webp
new file mode 100644
index 0000000000..7b5b074835
Binary files /dev/null and b/assets/images/live_now_beta.webp differ
diff --git a/package.json b/package.json
index 0030297734..d94a49874f 100644
--- a/package.json
+++ b/package.json
@@ -73,7 +73,7 @@
"icons:optimize": "svgo -f ./assets/icons"
},
"dependencies": {
- "@atproto/api": "^0.18.14",
+ "@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/Dialog/index.tsx b/src/components/Dialog/index.tsx
index ee2c0f76c7..b6d47fd3b4 100644
--- a/src/components/Dialog/index.tsx
+++ b/src/components/Dialog/index.tsx
@@ -359,7 +359,13 @@ export function FlatListFooter({children}: {children: React.ReactNode}) {
)
}
-export function Handle({difference = false}: {difference?: boolean}) {
+export function Handle({
+ difference = false,
+ fill,
+}: {
+ difference?: boolean
+ fill?: string
+}) {
const t = useTheme()
const {_} = useLingui()
const {screenReaderEnabled} = useA11y()
@@ -390,7 +396,7 @@ export function Handle({difference = false}: {difference?: boolean}) {
opacity: 0.75,
}
: {
- backgroundColor: t.palette.contrast_975,
+ backgroundColor: fill || t.palette.contrast_975,
opacity: 0.5,
},
]}
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
+
+
+ )}
@@ -202,16 +202,18 @@ export function ItemText({children, style}: ItemTextProps) {
)
}
-export function ItemIcon({icon: Comp}: ItemIconProps) {
+export function ItemIcon({icon: Comp, fill}: ItemIconProps) {
const t = useTheme()
const {disabled} = useMenuItemContext()
return (
)
diff --git a/src/components/Menu/index.web.tsx b/src/components/Menu/index.web.tsx
index 242449bf49..339b26350c 100644
--- a/src/components/Menu/index.web.tsx
+++ b/src/components/Menu/index.web.tsx
@@ -262,6 +262,7 @@ export function Item({children, label, onPress, style, ...rest}: ItemProps) {
a.gap_lg,
a.py_sm,
a.rounded_xs,
+ a.overflow_hidden,
{minHeight: 32, paddingHorizontal: 10},
web({outline: 0}),
(hovered || focused) &&
@@ -302,7 +303,7 @@ export function ItemText({children, style}: ItemTextProps) {
)
}
-export function ItemIcon({icon: Comp, position = 'left'}: ItemIconProps) {
+export function ItemIcon({icon: Comp, position = 'left', fill}: ItemIconProps) {
const t = useTheme()
const {disabled} = useMenuItemContext()
return (
@@ -319,9 +320,11 @@ export function ItemIcon({icon: Comp, position = 'left'}: ItemIconProps) {
diff --git a/src/components/Menu/types.ts b/src/components/Menu/types.ts
index e9ee082f4c..a5fce102ba 100644
--- a/src/components/Menu/types.ts
+++ b/src/components/Menu/types.ts
@@ -107,6 +107,7 @@ export type ItemTextProps = React.PropsWithChildren
export type ItemIconProps = React.PropsWithChildren<{
icon: React.ComponentType
position?: 'left' | 'right'
+ fill?: (props: {disabled: boolean}) => string
}>
export type GroupProps = React.PropsWithChildren
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/RichText.tsx b/src/components/RichText.tsx
index 9908679c57..eca0d7c635 100644
--- a/src/components/RichText.tsx
+++ b/src/components/RichText.tsx
@@ -11,6 +11,9 @@ import {RichTextTag} from '#/components/RichTextTag'
import {Text, type TextProps} from '#/components/Typography'
const WORD_WRAP = {wordWrap: 1}
+// lifted from facet detection in `RichText` impl, _without_ `gm` flags
+const URL_REGEX =
+ /(^|\s|\()((https?:\/\/[\S]+)|((?[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/dialogs/nuxs/LiveNowBetaDialog.tsx b/src/components/dialogs/nuxs/LiveNowBetaDialog.tsx
new file mode 100644
index 0000000000..b081322c44
--- /dev/null
+++ b/src/components/dialogs/nuxs/LiveNowBetaDialog.tsx
@@ -0,0 +1,209 @@
+import {useCallback, 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 {isWeb} from '#/platform/detection'
+import {atoms as a, select, useTheme, utils, web} from '#/alf'
+import {Button, ButtonText} from '#/components/Button'
+import * as Dialog from '#/components/Dialog'
+import {useNuxDialogContext} from '#/components/dialogs/nuxs'
+import {
+ createIsEnabledCheck,
+ isExistingUserAsOf,
+} from '#/components/dialogs/nuxs/utils'
+import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker'
+import {Text} from '#/components/Typography'
+import {IS_E2E} from '#/env'
+
+export const enabled = createIsEnabledCheck(props => {
+ return (
+ !IS_E2E &&
+ isExistingUserAsOf(
+ '2026-01-16T00:00:00.000Z',
+ props.currentProfile.createdAt,
+ ) &&
+ props.gate('live_now_beta')
+ )
+})
+
+export function LiveNowBetaDialog() {
+ const t = useTheme()
+ const {_} = useLingui()
+ const nuxDialogs = useNuxDialogContext()
+ const control = Dialog.useDialogControl()
+
+ Dialog.useAutoOpen(control)
+
+ const onClose = useCallback(() => {
+ nuxDialogs.dismissActiveNux()
+ }, [nuxDialogs])
+
+ const shadowColor = useMemo(() => {
+ return select(t.name, {
+ light: utils.alpha(t.palette.primary_900, 0.4),
+ dark: utils.alpha(t.palette.primary_25, 0.4),
+ dim: utils.alpha(t.palette.primary_25, 0.4),
+ })
+ }, [t])
+
+ return (
+
+
+
+
+
+
+
+
+
+ Beta Feature
+
+
+
+
+
+
+
+
+
+
+
+
+ Show when you’re live
+
+
+
+ Streaming on Twitch? Set your live status on Bluesky to add a
+ badge to your avatar. Tapping it takes people straight to your
+ stream.
+
+
+
+
+ {!isWeb && (
+
+ )}
+
+
+
+
+
+ )
+}
diff --git a/src/components/dialogs/nuxs/index.tsx b/src/components/dialogs/nuxs/index.tsx
index 2ab319fafd..eadadeb8eb 100644
--- a/src/components/dialogs/nuxs/index.tsx
+++ b/src/components/dialogs/nuxs/index.tsx
@@ -20,9 +20,9 @@ import {useProfileQuery} from '#/state/queries/profile'
import {type SessionAccount, useSession} from '#/state/session'
import {useOnboardingState} from '#/state/shell'
import {
- enabled as isFindContactsAnnouncementEnabled,
- FindContactsAnnouncement,
-} from '#/components/dialogs/nuxs/FindContactsAnnouncement'
+ enabled as isLiveNowBetaDialogEnabled,
+ LiveNowBetaDialog,
+} from '#/components/dialogs/nuxs/LiveNowBetaDialog'
import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing'
import {type EnabledCheckProps} from '#/components/dialogs/nuxs/utils'
import {useGeolocation} from '#/geolocation'
@@ -37,8 +37,8 @@ const queuedNuxs: {
enabled?: (props: EnabledCheckProps) => boolean
}[] = [
{
- id: Nux.FindContactsAnnouncement,
- enabled: isFindContactsAnnouncementEnabled,
+ id: Nux.LiveNowBetaDialog,
+ enabled: isLiveNowBetaDialogEnabled,
},
]
@@ -186,9 +186,7 @@ function Inner({
return (
{/*For example, activeNux === Nux.NeueTypography && */}
- {activeNux === Nux.FindContactsAnnouncement && (
-
- )}
+ {activeNux === Nux.LiveNowBetaDialog && }
)
}
diff --git a/src/components/interstitials/Trending.tsx b/src/components/interstitials/Trending.tsx
index 3d47385ee8..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 : (
-
+
{(liveLinkError || linkMetaError) && (
-
-
-
- {liveLinkError ? (
- This is not a valid link
- ) : (
- cleanError(linkMetaError)
- )}
-
-
+
+ {liveLinkError ? (
+ This is not a valid link
+ ) : (
+ cleanError(linkMetaError)
+ )}
+
)}
diff --git a/src/components/live/GoLiveDialog.tsx b/src/components/live/GoLiveDialog.tsx
index 44c604cde7..9d866fd6a5 100644
--- a/src/components/live/GoLiveDialog.tsx
+++ b/src/components/live/GoLiveDialog.tsx
@@ -6,13 +6,14 @@ import {useLingui} from '@lingui/react'
import {cleanError} from '#/lib/strings/errors'
import {definitelyUrl} from '#/lib/strings/url-helpers'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
+import {useLiveNowConfig} from '#/state/service-config'
import {useTickEveryMinute} from '#/state/shell'
import {atoms as a, ios, native, platform, useTheme, web} from '#/alf'
import {Admonition} from '#/components/Admonition'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import * as TextField from '#/components/forms/TextField'
-import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
+import {getLiveServiceNames} from '#/components/live/utils'
import {Loader} from '#/components/Loader'
import * as ProfileCard from '#/components/ProfileCard'
import * as Select from '#/components/Select'
@@ -49,6 +50,10 @@ function DialogInner({profile}: {profile: bsky.profile.AnyProfileView}) {
const [duration, setDuration] = useState(60)
const moderationOpts = useModerationOpts()
const tick = useTickEveryMinute()
+ const liveNowConfig = useLiveNowConfig()
+ const {formatted: allowedServices} = getLiveServiceNames(
+ liveNowConfig.allowedDomains,
+ )
const time = useCallback(
(offset: number) => {
@@ -139,27 +144,21 @@ function DialogInner({profile}: {profile: bsky.profile.AnyProfileView}) {
/>
- {(liveLinkError || linkMetaError) && (
-
-
-
- {liveLinkError ? (
- This is not a valid link
- ) : (
- cleanError(linkMetaError)
- )}
-
-
+ {liveLinkError || linkMetaError ? (
+
+ {liveLinkError ? (
+ This is not a valid link
+ ) : (
+ cleanError(linkMetaError)
+ )}
+
+ ) : (
+
+
+ The following services are enabled for your account:{' '}
+ {allowedServices}
+
+
)}
diff --git a/src/components/live/queries.ts b/src/components/live/queries.ts
index 19cb54ebf2..7db944d06d 100644
--- a/src/components/live/queries.ts
+++ b/src/components/live/queries.ts
@@ -18,10 +18,10 @@ import {useLiveNowConfig} from '#/state/service-config'
import {useAgent, useSession} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import {useDialogContext} from '#/components/Dialog'
+import {getLiveServiceNames} from '#/components/live/utils'
export function useLiveLinkMetaQuery(url: string | null) {
const liveNowConfig = useLiveNowConfig()
- const {currentAccount} = useSession()
const {_} = useLingui()
const agent = useAgent()
@@ -30,13 +30,14 @@ export function useLiveLinkMetaQuery(url: string | null) {
queryKey: ['link-meta', url],
queryFn: async () => {
if (!url) return undefined
- const config = liveNowConfig.find(cfg => cfg.did === currentAccount?.did)
-
- if (!config) throw new Error(_(msg`You are not allowed to go live`))
-
const urlp = new URL(url)
- if (!config.domains.includes(urlp.hostname)) {
- throw new Error(_(msg`${urlp.hostname} is not a valid URL`))
+ if (!liveNowConfig.allowedDomains.has(urlp.hostname)) {
+ const {formatted} = getLiveServiceNames(liveNowConfig.allowedDomains)
+ throw new Error(
+ _(
+ msg`This service is not supported while the Live feature is in beta. Allowed services: ${formatted}.`,
+ ),
+ )
}
return await getLinkMeta(agent, url)
diff --git a/src/components/live/utils.ts b/src/components/live/utils.ts
index 6b4267cb01..10b0ba8f90 100644
--- a/src/components/live/utils.ts
+++ b/src/components/live/utils.ts
@@ -35,3 +35,28 @@ export function useDebouncedValue(val: T, delayMs: number): T {
return prev
}
+
+const serviceUrlToNameMap: Record = {
+ 'twitch.tv': 'Twitch',
+ 'www.twitch.tv': 'Twitch',
+ 'youtube.com': 'YouTube',
+ 'www.youtube.com': 'YouTube',
+ 'youtu.be': 'YouTube',
+ 'nba.com': 'NBA',
+ 'www.nba.com': 'NBA',
+ 'nba.smart.link': 'nba.smart.link',
+ 'espn.com': 'ESPN',
+ 'www.espn.com': 'ESPN',
+ 'stream.place': 'Streamplace',
+ 'skylight.social': 'Skylight',
+}
+
+export function getLiveServiceNames(domains: Set) {
+ const names = Array.from(
+ new Set(Array.from(domains.values()).map(d => serviceUrlToNameMap[d] || d)),
+ )
+ return {
+ names,
+ formatted: names.join(', '),
+ }
+}
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/features/nuxs/components/Dot.tsx b/src/features/nuxs/components/Dot.tsx
new file mode 100644
index 0000000000..7eb82bf60f
--- /dev/null
+++ b/src/features/nuxs/components/Dot.tsx
@@ -0,0 +1,31 @@
+import {View, type ViewStyle} from 'react-native'
+
+import {atoms as a, useTheme} from '#/alf'
+
+/**
+ * The little blue dot used to nudge a user towards a certain feature. The dot
+ * is absolutely positioned, and is intended to be configured by passing in
+ * positional styles via `top`, `bottom`, `left`, and `right` props.
+ */
+export function Dot({
+ top,
+ bottom,
+ left,
+ right,
+}: Pick) {
+ const t = useTheme()
+ return (
+
+
+
+ )
+}
diff --git a/src/features/nuxs/components/Gradient.tsx b/src/features/nuxs/components/Gradient.tsx
new file mode 100644
index 0000000000..5da8b4aca3
--- /dev/null
+++ b/src/features/nuxs/components/Gradient.tsx
@@ -0,0 +1,24 @@
+import {LinearGradient} from 'expo-linear-gradient'
+
+import {atoms as a, useTheme, utils, type ViewStyleProp} from '#/alf'
+
+/**
+ * A gradient overlay using the primary color at low opacity. This component is
+ * absolutely positioned and intended to be composed within other components,
+ * with optional styling allowed, such as adjusting border radius.
+ */
+export function Gradient({style}: ViewStyleProp) {
+ const t = useTheme()
+ return (
+
+ )
+}
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 31e532eaa0..7c18b5dc66 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 {useLiveNowConfig} from '#/state/service-config'
+import {type LiveNowConfig, 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.did, shadowed.status, config)
+ const isValid = validateStatus(shadowed.status, config)
const isDisabled = shadowed.status.isDisabled || false
const isActive = isStatusStillActive(shadowed.status.expiresAt)
if (isValid && !isDisabled && isActive) {
@@ -65,19 +65,14 @@ export function isStatusStillActive(timeStr: string | undefined) {
}
export function validateStatus(
- did: string,
status: AppBskyActorDefs.StatusView,
- config: {did: string; domains: string[]}[],
+ config: LiveNowConfig,
) {
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 sources.domains.includes(url.hostname)
+ return config.allowedDomains.has(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 (
moderation settings0>."
msgstr ""
@@ -2316,7 +2334,7 @@ msgstr ""
msgid "Contacts removed"
msgstr ""
-#: src/screens/Settings/ContentAndMediaSettings.tsx:51
+#: src/screens/Settings/ContentAndMediaSettings.tsx:52
msgid "Content & Media"
msgstr ""
@@ -2419,7 +2437,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
@@ -2443,8 +2461,8 @@ msgstr ""
msgid "Copy App Password"
msgstr ""
-#: src/view/com/profile/ProfileMenu.tsx:447
-#: src/view/com/profile/ProfileMenu.tsx:450
+#: src/view/com/profile/ProfileMenu.tsx:491
+#: src/view/com/profile/ProfileMenu.tsx:494
msgid "Copy at:// URI"
msgstr ""
@@ -2459,8 +2477,8 @@ msgid "Copy code"
msgstr ""
#: src/screens/Settings/components/ChangeHandleDialog.tsx:502
-#: src/view/com/profile/ProfileMenu.tsx:456
-#: src/view/com/profile/ProfileMenu.tsx:459
+#: src/view/com/profile/ProfileMenu.tsx:500
+#: src/view/com/profile/ProfileMenu.tsx:503
msgid "Copy DID"
msgstr ""
@@ -2489,8 +2507,8 @@ msgstr ""
msgid "Copy link to post"
msgstr ""
-#: src/view/com/profile/ProfileMenu.tsx:252
-#: src/view/com/profile/ProfileMenu.tsx:263
+#: src/view/com/profile/ProfileMenu.tsx:269
+#: src/view/com/profile/ProfileMenu.tsx:280
msgid "Copy link to profile"
msgstr ""
@@ -2508,8 +2526,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 ""
@@ -2770,7 +2788,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
@@ -2842,8 +2860,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 ""
@@ -2861,7 +2879,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 ""
@@ -2896,12 +2914,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 ""
@@ -2997,12 +3015,12 @@ msgstr ""
msgid "Discover new custom feeds"
msgstr ""
-#: src/screens/Search/Explore.tsx:431
+#: src/screens/Search/Explore.tsx:436
#: src/view/screens/Feeds.tsx:730
msgid "Discover New Feeds"
msgstr ""
-#: src/components/Dialog/index.tsx:373
+#: src/components/Dialog/index.tsx:379
msgid "Dismiss"
msgstr ""
@@ -3116,7 +3134,7 @@ msgstr ""
msgid "Double tap or long press the message to add a reaction"
msgstr ""
-#: src/components/Dialog/index.tsx:374
+#: src/components/Dialog/index.tsx:380
msgid "Double tap to close the dialog"
msgstr ""
@@ -3214,8 +3232,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 ""
@@ -3229,8 +3247,8 @@ msgstr ""
msgid "Edit list details"
msgstr ""
-#: src/view/com/profile/ProfileMenu.tsx:337
-#: src/view/com/profile/ProfileMenu.tsx:349
+#: src/view/com/profile/ProfileMenu.tsx:354
+#: src/view/com/profile/ProfileMenu.tsx:374
msgid "Edit live status"
msgstr ""
@@ -3394,13 +3412,13 @@ msgstr ""
msgid "Enable this source only"
msgstr ""
-#: src/screens/Settings/ContentAndMediaSettings.tsx:132
-#: src/screens/Settings/ContentAndMediaSettings.tsx:146
+#: src/screens/Settings/ContentAndMediaSettings.tsx:133
+#: src/screens/Settings/ContentAndMediaSettings.tsx:147
msgid "Enable trending topics"
msgstr ""
-#: src/screens/Settings/ContentAndMediaSettings.tsx:153
-#: src/screens/Settings/ContentAndMediaSettings.tsx:167
+#: src/screens/Settings/ContentAndMediaSettings.tsx:155
+#: src/screens/Settings/ContentAndMediaSettings.tsx:169
msgid "Enable trending videos in your Discover feed"
msgstr ""
@@ -3596,7 +3614,7 @@ msgstr ""
msgid "Expires in {0}"
msgstr ""
-#: src/components/live/EditLiveDialog.tsx:131
+#: src/components/live/EditLiveDialog.tsx:130
msgid "Expires in {0} at {1}"
msgstr ""
@@ -3630,8 +3648,8 @@ msgstr ""
msgid "Export My Data"
msgstr ""
-#: src/screens/Settings/ContentAndMediaSettings.tsx:84
-#: src/screens/Settings/ContentAndMediaSettings.tsx:87
+#: src/screens/Settings/ContentAndMediaSettings.tsx:85
+#: src/screens/Settings/ContentAndMediaSettings.tsx:88
msgid "External media"
msgstr ""
@@ -3695,7 +3713,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 ""
@@ -3724,10 +3742,10 @@ msgstr ""
msgid "Failed to load conversations"
msgstr ""
-#: src/screens/Search/Explore.tsx:467
-#: src/screens/Search/Explore.tsx:519
-#: src/screens/Search/Explore.tsx:557
-#: src/screens/Search/Explore.tsx:596
+#: src/screens/Search/Explore.tsx:472
+#: src/screens/Search/Explore.tsx:524
+#: src/screens/Search/Explore.tsx:562
+#: src/screens/Search/Explore.tsx:601
msgid "Failed to load feeds preferences"
msgstr ""
@@ -3757,14 +3775,14 @@ msgstr ""
msgid "Failed to load preference."
msgstr ""
-#: src/screens/Search/Explore.tsx:460
-#: src/screens/Search/Explore.tsx:512
-#: src/screens/Search/Explore.tsx:550
-#: src/screens/Search/Explore.tsx:589
+#: src/screens/Search/Explore.tsx:465
+#: src/screens/Search/Explore.tsx:517
+#: src/screens/Search/Explore.tsx:555
+#: src/screens/Search/Explore.tsx:594
msgid "Failed to load suggested feeds"
msgstr ""
-#: src/screens/Search/Explore.tsx:370
+#: src/screens/Search/Explore.tsx:375
msgid "Failed to load suggested follows"
msgstr ""
@@ -3831,7 +3849,7 @@ msgstr ""
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 ""
@@ -3848,7 +3866,7 @@ msgstr ""
msgid "Failed to update email, please try again."
msgstr ""
-#: src/components/FeedCard.tsx:285
+#: src/components/FeedCard.tsx:317
msgid "Failed to update feeds"
msgstr ""
@@ -3887,7 +3905,7 @@ msgstr ""
msgid "Feed"
msgstr ""
-#: src/components/FeedCard.tsx:139
+#: src/components/FeedCard.tsx:171
#: src/view/com/feeds/FeedSourceCard.tsx:150
msgid "Feed by {0}"
msgstr ""
@@ -3912,14 +3930,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:107
+#: src/view/shell/desktop/RightNav.tsx:108
#: 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 ""
@@ -3939,7 +3957,7 @@ msgstr ""
msgid "Feeds are custom algorithms that users build with a little coding expertise. <0>See this guide0> for more information."
msgstr ""
-#: src/components/FeedCard.tsx:282
+#: src/components/FeedCard.tsx:314
#: src/screens/SavedFeeds.tsx:90
msgctxt "toast"
msgid "Feeds updated!"
@@ -4099,8 +4117,8 @@ msgstr ""
msgid "Follow 7 accounts"
msgstr ""
-#: src/view/com/profile/ProfileMenu.tsx:293
-#: src/view/com/profile/ProfileMenu.tsx:304
+#: src/view/com/profile/ProfileMenu.tsx:310
+#: src/view/com/profile/ProfileMenu.tsx:321
msgid "Follow account"
msgstr ""
@@ -4191,8 +4209,8 @@ msgstr ""
msgid "Following {handle}"
msgstr ""
-#: src/screens/Settings/ContentAndMediaSettings.tsx:76
-#: src/screens/Settings/ContentAndMediaSettings.tsx:79
+#: src/screens/Settings/ContentAndMediaSettings.tsx:77
+#: src/screens/Settings/ContentAndMediaSettings.tsx:80
msgid "Following feed preferences"
msgstr ""
@@ -4411,24 +4429,24 @@ msgstr ""
msgid "Go Home"
msgstr ""
-#: src/view/com/profile/ProfileMenu.tsx:338
-#: src/view/com/profile/ProfileMenu.tsx:351
+#: src/view/com/profile/ProfileMenu.tsx:355
+#: src/view/com/profile/ProfileMenu.tsx:376
msgid "Go live"
msgstr ""
-#: src/components/live/GoLiveDialog.tsx:90
#: src/components/live/GoLiveDialog.tsx:95
-#: src/components/live/GoLiveDialog.tsx:229
-#: src/components/live/GoLiveDialog.tsx:238
+#: src/components/live/GoLiveDialog.tsx:100
+#: src/components/live/GoLiveDialog.tsx:228
+#: src/components/live/GoLiveDialog.tsx:237
msgid "Go Live"
msgstr ""
-#: src/view/com/profile/ProfileMenu.tsx:335
-#: src/view/com/profile/ProfileMenu.tsx:347
+#: src/view/com/profile/ProfileMenu.tsx:352
+#: src/view/com/profile/ProfileMenu.tsx:372
msgid "Go live (disabled)"
msgstr ""
-#: src/components/live/GoLiveDialog.tsx:171
+#: src/components/live/GoLiveDialog.tsx:170
msgid "Go live for"
msgstr ""
@@ -4511,6 +4529,11 @@ msgstr ""
msgid "Handle too long. Please try a shorter one."
msgstr ""
+#: src/components/FeedCard.tsx:157
+#: src/features/liveEvents/components/LiveEventFeedCardWide.tsx:120
+msgid "Happening now"
+msgstr ""
+
#: src/screens/Settings/AccessibilitySettings.tsx:85
msgid "Haptics"
msgstr ""
@@ -4558,8 +4581,8 @@ msgstr ""
#: src/screens/Settings/Settings.tsx:254
#: src/screens/Settings/Settings.tsx:258
-#: src/view/shell/desktop/RightNav.tsx:125
#: src/view/shell/desktop/RightNav.tsx:128
+#: src/view/shell/desktop/RightNav.tsx:131
#: src/view/shell/Drawer.tsx:381
msgid "Help"
msgstr ""
@@ -4603,7 +4626,7 @@ msgstr ""
#: 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
@@ -4617,6 +4640,11 @@ msgctxt "action"
msgid "Hide"
msgstr ""
+#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:140
+#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:147
+msgid "Hide all events"
+msgstr ""
+
#: src/components/dialogs/Embed.tsx:128
msgid "Hide customization options"
msgstr ""
@@ -4625,18 +4653,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 ""
@@ -4644,12 +4672,17 @@ msgstr ""
msgid "Hide this card"
msgstr ""
-#: src/components/PostControls/PostMenu/PostMenuItems.tsx:742
+#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:128
+#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:135
+msgid "Hide this event"
+msgstr ""
+
+#: 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 ""
@@ -4793,6 +4826,10 @@ msgstr ""
msgid "If you believe your birthdate is incorrect, you can update it by <0>clicking here0>."
msgstr ""
+#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:119
+msgid "If you choose to hide all events, you can always re-enable them from <0>Settings → Content & Media0>."
+msgstr ""
+
#: src/screens/ProfileList/components/MoreOptionsMenu.tsx:277
msgid "If you delete this list, you won't be able to recover it."
msgstr ""
@@ -4805,7 +4842,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 ""
@@ -4967,7 +5004,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 ""
@@ -5269,7 +5306,7 @@ 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 ""
@@ -5305,7 +5342,7 @@ msgstr ""
msgid "Liked By"
msgstr ""
-#: src/components/FeedCard.tsx:218
+#: src/components/FeedCard.tsx:250
#: src/view/com/feeds/FeedSourceCard.tsx:172
msgid "Liked by {0, plural, one {# user} other {# users}}"
msgstr ""
@@ -5334,7 +5371,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 ""
@@ -5445,23 +5482,36 @@ msgstr ""
msgid "LIVE"
msgstr ""
+#: src/features/liveEvents/components/LiveEventFeedCardCompact.tsx:52
+#: src/features/liveEvents/components/LiveEventFeedCardWide.tsx:52
+msgid "Live event happening now: {0}"
+msgstr ""
+
+#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:107
+msgid "Live event options"
+msgstr ""
+
+#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:111
+msgid "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."
+msgstr ""
+
#: src/components/live/LiveStatusDialog.tsx:235
msgid "Live feature is in beta"
msgstr ""
-#: src/components/live/EditLiveDialog.tsx:148
-#: src/components/live/EditLiveDialog.tsx:152
-#: src/components/live/GoLiveDialog.tsx:121
-#: src/components/live/GoLiveDialog.tsx:125
+#: src/components/live/EditLiveDialog.tsx:147
+#: src/components/live/EditLiveDialog.tsx:151
+#: src/components/live/GoLiveDialog.tsx:126
+#: src/components/live/GoLiveDialog.tsx:130
msgid "Live link"
msgstr ""
-#: src/screens/Search/Explore.tsx:83
+#: src/screens/Search/Explore.tsx:84
msgid "Load more"
msgstr ""
-#: src/screens/Search/Explore.tsx:501
-#: src/screens/Search/Explore.tsx:578
+#: src/screens/Search/Explore.tsx:506
+#: src/screens/Search/Explore.tsx:583
msgid "Load more suggested feeds"
msgstr ""
@@ -5500,11 +5550,11 @@ msgstr ""
msgid "Logged-out visibility"
msgstr ""
-#: src/view/shell/desktop/RightNav.tsx:137
+#: src/view/shell/desktop/RightNav.tsx:140
msgid "Logo by @sawaratsuki.bsky.social"
msgstr ""
-#: src/view/shell/desktop/RightNav.tsx:134
+#: src/view/shell/desktop/RightNav.tsx:137
#: src/view/shell/Drawer.tsx:709
msgid "Logo by <0>@sawaratsuki.bsky.social0>"
msgstr ""
@@ -5541,8 +5591,8 @@ msgstr ""
msgid "Make sure this is where you intend to go!"
msgstr ""
-#: src/screens/Settings/ContentAndMediaSettings.tsx:60
-#: src/screens/Settings/ContentAndMediaSettings.tsx:63
+#: src/screens/Settings/ContentAndMediaSettings.tsx:61
+#: src/screens/Settings/ContentAndMediaSettings.tsx:64
msgid "Manage saved feeds"
msgstr ""
@@ -5729,8 +5779,8 @@ msgid "More languages..."
msgstr ""
#: src/screens/ProfileList/components/MoreOptionsMenu.tsx:149
-#: src/view/com/profile/ProfileMenu.tsx:229
-#: src/view/com/profile/ProfileMenu.tsx:235
+#: src/view/com/profile/ProfileMenu.tsx:241
+#: src/view/com/profile/ProfileMenu.tsx:248
msgid "More options"
msgstr ""
@@ -5761,10 +5811,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:389
-#: src/view/com/profile/ProfileMenu.tsx:396
+#: src/components/PostControls/PostMenu/PostMenuItems.tsx:686
+#: src/components/PostControls/PostMenu/PostMenuItems.tsx:692
+#: src/view/com/profile/ProfileMenu.tsx:433
+#: src/view/com/profile/ProfileMenu.tsx:440
msgid "Mute account"
msgstr ""
@@ -5814,13 +5864,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 ""
@@ -5894,6 +5944,7 @@ msgid "Nevermind, create a handle for me"
msgstr ""
#: src/screens/Search/modules/ExploreTrendingTopics.tsx:196
+#: src/view/com/profile/ProfileMenu.tsx:388
msgid "New"
msgstr ""
@@ -6054,7 +6105,7 @@ msgstr ""
msgid "No DNS Panel"
msgstr ""
-#: src/components/live/EditLiveDialog.tsx:140
+#: src/components/live/EditLiveDialog.tsx:139
msgid "No expiry set"
msgstr ""
@@ -6153,7 +6204,7 @@ msgstr ""
msgid "No results"
msgstr ""
-#: src/screens/Search/Explore.tsx:793
+#: src/screens/Search/Explore.tsx:800
msgid "No results for \"{0}\"."
msgstr ""
@@ -6170,7 +6221,7 @@ msgstr ""
msgid "No results found for \"<0>{query}0>\"."
msgstr ""
-#: src/screens/Search/Explore.tsx:797
+#: src/screens/Search/Explore.tsx:804
msgid "No results."
msgstr ""
@@ -6238,7 +6289,7 @@ msgstr ""
msgid "Not Found"
msgstr ""
-#: src/view/com/profile/ProfileMenu.tsx:511
+#: src/view/com/profile/ProfileMenu.tsx:555
msgid "Note about sharing"
msgstr ""
@@ -6338,7 +6389,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 ""
@@ -6416,7 +6467,7 @@ msgstr ""
msgid "Open avatar creator"
msgstr ""
-#: src/view/com/feeds/ComposerPrompt.tsx:200
+#: src/view/com/feeds/ComposerPrompt.tsx:198
msgid "Open camera"
msgstr ""
@@ -6467,7 +6518,7 @@ 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 ""
@@ -6476,7 +6527,7 @@ msgstr ""
msgid "Open profile"
msgstr ""
-#: src/components/PostControls/ShareMenu/index.tsx:89
+#: src/components/PostControls/ShareMenu/index.tsx:107
msgid "Open share menu"
msgstr ""
@@ -6529,7 +6580,7 @@ msgstr ""
msgid "Opens composer"
msgstr ""
-#: src/view/com/feeds/ComposerPrompt.tsx:201
+#: src/view/com/feeds/ComposerPrompt.tsx:199
msgid "Opens device camera"
msgstr ""
@@ -6560,7 +6611,7 @@ msgstr ""
msgid "Opens helpdesk in browser"
msgstr ""
-#: src/view/com/feeds/ComposerPrompt.tsx:223
+#: src/view/com/feeds/ComposerPrompt.tsx:221
msgid "Opens image picker"
msgstr ""
@@ -6779,7 +6830,7 @@ msgstr ""
msgid "Pin feed"
msgstr ""
-#: src/components/FeedCard.tsx:329
+#: src/components/FeedCard.tsx:361
msgid "Pin Feed"
msgstr ""
@@ -6792,8 +6843,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 ""
@@ -7046,7 +7097,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 ""
@@ -7096,7 +7147,7 @@ msgctxt "toast"
msgid "Post pinned"
msgstr ""
-#: src/components/PostControls/BookmarkButton.tsx:59
+#: src/components/PostControls/BookmarkButton.tsx:66
msgid "Post saved"
msgstr ""
@@ -7153,6 +7204,10 @@ msgstr ""
msgid "Press to view followers of this account that you also follow"
msgstr ""
+#: src/features/liveEvents/components/LiveEventFeedCardWide.tsx:118
+msgid "Preview"
+msgstr ""
+
#: src/view/com/lightbox/Lightbox.web.tsx:151
msgid "Previous image"
msgstr ""
@@ -7165,8 +7220,8 @@ msgstr ""
msgid "Primary Language"
msgstr ""
-#: src/view/shell/desktop/RightNav.tsx:113
-#: src/view/shell/desktop/RightNav.tsx:114
+#: src/view/shell/desktop/RightNav.tsx:116
+#: src/view/shell/desktop/RightNav.tsx:117
msgid "Privacy"
msgstr ""
@@ -7299,11 +7354,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 ""
@@ -7321,7 +7376,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 ""
@@ -7333,8 +7388,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 ""
@@ -7445,7 +7500,7 @@ msgstr ""
#: src/components/dialogs/MutedWords.tsx:443
#: src/components/dialogs/StarterPackDialog.tsx:374
#: src/components/dialogs/StarterPackDialog.tsx:380
-#: src/components/FeedCard.tsx:343
+#: src/components/FeedCard.tsx:375
#: src/components/StarterPack/Wizard/WizardListCard.tsx:105
#: src/components/StarterPack/Wizard/WizardListCard.tsx:112
#: src/screens/Bookmarks/index.tsx:266
@@ -7518,12 +7573,12 @@ 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 ""
-#: src/components/FeedCard.tsx:338
+#: src/components/FeedCard.tsx:370
msgid "Remove from your feeds?"
msgstr ""
@@ -7531,8 +7586,8 @@ msgstr ""
msgid "Remove image"
msgstr ""
-#: src/components/live/EditLiveDialog.tsx:240
-#: src/components/live/EditLiveDialog.tsx:247
+#: src/components/live/EditLiveDialog.tsx:226
+#: src/components/live/EditLiveDialog.tsx:233
msgid "Remove live status"
msgstr ""
@@ -7568,8 +7623,8 @@ msgstr ""
#: src/components/verification/VerificationRemovePrompt.tsx:46
#: src/components/verification/VerificationsDialog.tsx:252
-#: src/view/com/profile/ProfileMenu.tsx:362
-#: src/view/com/profile/ProfileMenu.tsx:365
+#: src/view/com/profile/ProfileMenu.tsx:406
+#: src/view/com/profile/ProfileMenu.tsx:409
msgid "Remove verification"
msgstr ""
@@ -7594,7 +7649,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 ""
@@ -7662,7 +7717,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 ""
@@ -7688,12 +7743,12 @@ 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 ""
@@ -7704,8 +7759,8 @@ msgstr ""
msgid "Report"
msgstr ""
-#: src/view/com/profile/ProfileMenu.tsx:429
-#: src/view/com/profile/ProfileMenu.tsx:432
+#: src/view/com/profile/ProfileMenu.tsx:473
+#: src/view/com/profile/ProfileMenu.tsx:476
msgid "Report account"
msgstr ""
@@ -7736,8 +7791,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 ""
@@ -7828,7 +7883,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 ""
@@ -7983,8 +8038,8 @@ msgstr ""
#: src/components/dialogs/lists/CreateOrEditListDialog.tsx:307
#: src/components/dialogs/PostInteractionSettingsDialog.tsx:662
#: src/components/dialogs/PostInteractionSettingsDialog.tsx:667
-#: src/components/live/EditLiveDialog.tsx:216
-#: src/components/live/EditLiveDialog.tsx:223
+#: src/components/live/EditLiveDialog.tsx:202
+#: src/components/live/EditLiveDialog.tsx:209
#: src/components/StarterPack/QrCodeDialog.tsx:204
#: src/screens/Profile/Header/EditProfileDialog.tsx:236
#: src/screens/Profile/Header/EditProfileDialog.tsx:250
@@ -8134,11 +8189,11 @@ msgstr ""
msgid "Search for feeds that you want to suggest to others."
msgstr ""
-#: src/screens/Search/Explore.tsx:357
+#: src/screens/Search/Explore.tsx:362
msgid "Search for more accounts"
msgstr ""
-#: src/screens/Search/Explore.tsx:434
+#: src/screens/Search/Explore.tsx:439
msgid "Search for more feeds"
msgstr ""
@@ -8164,8 +8219,8 @@ msgstr ""
msgid "Search my posts"
msgstr ""
-#: src/view/com/profile/ProfileMenu.tsx:272
-#: src/view/com/profile/ProfileMenu.tsx:275
+#: src/view/com/profile/ProfileMenu.tsx:289
+#: src/view/com/profile/ProfileMenu.tsx:292
msgid "Search posts"
msgstr ""
@@ -8283,7 +8338,7 @@ msgstr ""
msgid "Select content languages"
msgstr ""
-#: src/components/live/GoLiveDialog.tsx:176
+#: src/components/live/GoLiveDialog.tsx:175
msgid "Select duration"
msgstr ""
@@ -8575,7 +8630,7 @@ msgstr ""
msgid "Share a fun fact!"
msgstr ""
-#: src/view/com/profile/ProfileMenu.tsx:516
+#: src/view/com/profile/ProfileMenu.tsx:560
msgid "Share anyway"
msgstr ""
@@ -8623,8 +8678,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:252
-#: src/view/com/profile/ProfileMenu.tsx:265
+#: src/view/com/profile/ProfileMenu.tsx:269
+#: src/view/com/profile/ProfileMenu.tsx:282
msgid "Share via..."
msgstr ""
@@ -8671,8 +8726,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 ""
@@ -8684,12 +8739,17 @@ msgstr ""
msgid "Show lists of users to select from"
msgstr ""
+#: src/features/liveEvents/components/LiveEventFeedsSettingsToggle.tsx:27
+#: src/features/liveEvents/components/LiveEventFeedsSettingsToggle.tsx:37
+msgid "Show live events in your Discover Feed"
+msgstr ""
+
#: src/components/Post/ShowMoreTextButton.tsx:51
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 ""
@@ -8715,8 +8775,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 ""
@@ -8738,7 +8798,12 @@ msgstr ""
msgid "Show warning and filter from feeds"
msgstr ""
-#: src/screens/PostThread/components/ThreadItemAnchor.tsx:629
+#: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:60
+#: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:170
+msgid "Show when you’re live"
+msgstr ""
+
+#: src/screens/PostThread/components/ThreadItemAnchor.tsx:641
msgid "Shows information about when this post was created"
msgstr ""
@@ -8798,8 +8863,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 ""
@@ -8885,7 +8950,7 @@ msgstr ""
msgid "Some of your verifications are invalid."
msgstr ""
-#: src/components/FeedInterstitials.tsx:870
+#: src/components/FeedInterstitials.tsx:871
msgid "Some other feeds you might like"
msgstr ""
@@ -8942,8 +9007,8 @@ msgstr ""
msgid "Sorry, we're unable to load account suggestions at this time."
msgstr ""
-#: src/App.native.tsx:126
-#: src/App.web.tsx:99
+#: src/App.native.tsx:131
+#: src/App.web.tsx:104
msgid "Sorry! Your session expired. Please sign in again."
msgstr ""
@@ -9022,7 +9087,7 @@ msgstr ""
msgid "Starter pack is invalid"
msgstr ""
-#: src/screens/Search/Explore.tsx:625
+#: src/screens/Search/Explore.tsx:630
#: src/view/screens/Profile.tsx:240
msgid "Starter Packs"
msgstr ""
@@ -9058,6 +9123,10 @@ msgstr ""
msgid "Storybook"
msgstr ""
+#: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:181
+msgid "Streaming on Twitch? Set your live status on Bluesky to add a badge to your avatar. Tapping it takes people straight to your stream."
+msgstr ""
+
#: src/components/ageAssurance/AgeAssuranceAppealDialog.tsx:117
#: src/components/ageAssurance/AgeAssuranceAppealDialog.tsx:123
#: src/components/live/GoLiveDisabledDialog.tsx:138
@@ -9121,7 +9190,7 @@ msgstr ""
msgid "Successfully verified"
msgstr ""
-#: src/screens/Search/Explore.tsx:354
+#: src/screens/Search/Explore.tsx:359
msgid "Suggested Accounts"
msgstr ""
@@ -9256,8 +9325,8 @@ msgstr ""
msgid "Tell us a little more"
msgstr ""
-#: src/view/shell/desktop/RightNav.tsx:120
-#: src/view/shell/desktop/RightNav.tsx:121
+#: src/view/shell/desktop/RightNav.tsx:123
+#: src/view/shell/desktop/RightNav.tsx:124
msgid "Terms"
msgstr ""
@@ -9323,7 +9392,7 @@ msgstr ""
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:186
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:391
-#: src/view/com/profile/ProfileMenu.tsx:492
+#: src/view/com/profile/ProfileMenu.tsx:536
msgid "The account will be able to interact with you after unblocking."
msgstr ""
@@ -9377,6 +9446,10 @@ msgstr ""
msgid "The following labels were applied to your content."
msgstr ""
+#: src/components/live/GoLiveDialog.tsx:157
+msgid "The following services are enabled for your account: {allowedServices}"
+msgstr ""
+
#: src/screens/ModerationInteractionSettings/index.tsx:42
msgid "The following settings will be used as your defaults when creating new posts. You can edit these for a specific post from the composer."
msgstr ""
@@ -9465,7 +9538,7 @@ msgstr ""
msgid "There was an issue fetching notifications. Tap here to try again."
msgstr ""
-#: src/screens/Search/Explore.tsx:992
+#: src/screens/Search/Explore.tsx:999
#: src/view/com/posts/PostFeed.tsx:759
msgid "There was an issue fetching posts. Tap here to try again."
msgstr ""
@@ -9497,21 +9570,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: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:151
+#: src/view/com/profile/ProfileMenu.tsx:161
+#: src/view/com/profile/ProfileMenu.tsx:175
+#: src/view/com/profile/ProfileMenu.tsx:185
#: src/view/com/profile/ProfileMenu.tsx:198
+#: src/view/com/profile/ProfileMenu.tsx:210
msgid "There was an issue! {0}"
msgstr ""
@@ -9671,8 +9744,8 @@ msgstr ""
msgid "This is an empty state"
msgstr ""
-#: src/components/live/EditLiveDialog.tsx:189
-#: src/components/live/GoLiveDialog.tsx:157
+#: src/components/live/EditLiveDialog.tsx:176
+#: src/components/live/GoLiveDialog.tsx:150
msgid "This is not a valid link"
msgstr ""
@@ -9708,7 +9781,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 ""
@@ -9724,7 +9797,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 ""
@@ -9732,11 +9805,11 @@ msgstr ""
msgid "This post's author has disabled quote posts."
msgstr ""
-#: src/view/com/profile/ProfileMenu.tsx:513
+#: src/view/com/profile/ProfileMenu.tsx:557
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 ""
@@ -9744,6 +9817,10 @@ msgstr ""
msgid "This service has not provided terms of service or a privacy policy."
msgstr ""
+#: src/components/live/queries.ts:38
+msgid "This service is not supported while the Live feature is in beta. Allowed services: {formatted}."
+msgstr ""
+
#: src/screens/Settings/components/ChangeHandleDialog.tsx:467
msgid "This should create a domain record at:"
msgstr ""
@@ -9797,7 +9874,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 ""
@@ -9806,8 +9883,8 @@ msgstr ""
msgid "Thread options"
msgstr ""
-#: src/screens/Settings/ContentAndMediaSettings.tsx:68
-#: src/screens/Settings/ContentAndMediaSettings.tsx:71
+#: src/screens/Settings/ContentAndMediaSettings.tsx:69
+#: src/screens/Settings/ContentAndMediaSettings.tsx:72
msgid "Thread preferences"
msgstr ""
@@ -9892,10 +9969,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 ""
@@ -10001,7 +10078,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:504
+#: src/view/com/profile/ProfileMenu.tsx:548
msgid "Unblock"
msgstr ""
@@ -10012,14 +10089,14 @@ msgstr ""
#: src/components/dms/ConvoMenu.tsx:261
#: src/components/dms/ConvoMenu.tsx:264
-#: src/view/com/profile/ProfileMenu.tsx:409
-#: src/view/com/profile/ProfileMenu.tsx:415
+#: src/view/com/profile/ProfileMenu.tsx:453
+#: src/view/com/profile/ProfileMenu.tsx:459
msgid "Unblock account"
msgstr ""
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:184
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:389
-#: src/view/com/profile/ProfileMenu.tsx:486
+#: src/view/com/profile/ProfileMenu.tsx:530
msgid "Unblock Account?"
msgstr ""
@@ -10028,7 +10105,12 @@ msgstr ""
msgid "Unblock list"
msgstr ""
-#: src/components/PostControls/BookmarkButton.tsx:40
+#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:72
+#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:78
+msgid "Undo"
+msgstr ""
+
+#: src/components/PostControls/BookmarkButton.tsx:42
msgctxt "Button label to undo saving/removing a post from saved posts."
msgid "Undo"
msgstr ""
@@ -10047,8 +10129,8 @@ msgstr ""
msgid "Unfollow {0}"
msgstr ""
-#: src/view/com/profile/ProfileMenu.tsx:292
-#: src/view/com/profile/ProfileMenu.tsx:302
+#: src/view/com/profile/ProfileMenu.tsx:309
+#: src/view/com/profile/ProfileMenu.tsx:319
msgid "Unfollow account"
msgstr ""
@@ -10085,7 +10167,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 ""
@@ -10105,10 +10187,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:388
-#: src/view/com/profile/ProfileMenu.tsx:394
+#: src/components/PostControls/PostMenu/PostMenuItems.tsx:685
+#: src/components/PostControls/PostMenu/PostMenuItems.tsx:691
+#: src/view/com/profile/ProfileMenu.tsx:432
+#: src/view/com/profile/ProfileMenu.tsx:438
msgid "Unmute account"
msgstr ""
@@ -10121,8 +10203,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 ""
@@ -10141,7 +10223,7 @@ msgstr ""
msgid "Unpin feed"
msgstr ""
-#: src/components/FeedCard.tsx:320
+#: src/components/FeedCard.tsx:352
msgid "Unpin Feed"
msgstr ""
@@ -10150,8 +10232,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 ""
@@ -10223,12 +10305,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 ""
@@ -10290,8 +10372,8 @@ msgstr ""
msgid "Use in-app browser"
msgstr ""
-#: src/screens/Settings/ContentAndMediaSettings.tsx:102
-#: src/screens/Settings/ContentAndMediaSettings.tsx:108
+#: src/screens/Settings/ContentAndMediaSettings.tsx:103
+#: src/screens/Settings/ContentAndMediaSettings.tsx:109
msgid "Use in-app browser to open links"
msgstr ""
@@ -10416,8 +10498,8 @@ msgstr ""
#: src/components/verification/VerificationCreatePrompt.tsx:84
#: src/components/verification/VerificationCreatePrompt.tsx:86
-#: src/view/com/profile/ProfileMenu.tsx:372
-#: src/view/com/profile/ProfileMenu.tsx:375
+#: src/view/com/profile/ProfileMenu.tsx:416
+#: src/view/com/profile/ProfileMenu.tsx:419
msgid "Verify account"
msgstr ""
@@ -10922,7 +11004,7 @@ msgstr ""
#: src/view/com/auth/SplashScreen.tsx:51
#: src/view/com/auth/SplashScreen.web.tsx:103
#: src/view/com/composer/Composer.tsx:857
-#: src/view/com/feeds/ComposerPrompt.tsx:191
+#: src/view/com/feeds/ComposerPrompt.tsx:189
msgid "What's up?"
msgstr ""
@@ -11025,8 +11107,8 @@ msgstr ""
msgid "Wrong DID returned from server. Received: {0}"
msgstr ""
-#: src/components/live/EditLiveDialog.tsx:153
-#: src/components/live/GoLiveDialog.tsx:126
+#: src/components/live/EditLiveDialog.tsx:152
+#: src/components/live/GoLiveDialog.tsx:131
msgid "www.mylivestream.tv"
msgstr ""
@@ -11043,11 +11125,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 ""
@@ -11084,19 +11166,15 @@ msgstr ""
msgid "You are in line."
msgstr ""
-#: src/components/live/EditLiveDialog.tsx:119
-#: src/components/live/EditLiveDialog.tsx:124
+#: src/components/live/EditLiveDialog.tsx:118
+#: src/components/live/EditLiveDialog.tsx:123
msgid "You are Live"
msgstr ""
-#: src/components/live/queries.ts:211
+#: src/components/live/queries.ts:212
msgid "You are no longer live"
msgstr ""
-#: src/components/live/queries.ts:35
-msgid "You are not allowed to go live"
-msgstr ""
-
#: src/view/com/composer/state/video.ts:411
msgid "You are not allowed to upload videos."
msgstr ""
@@ -11105,7 +11183,7 @@ msgstr ""
msgid "You are not following anyone yet"
msgstr ""
-#: src/components/live/queries.ts:156
+#: src/components/live/queries.ts:157
msgid "You are now live!"
msgstr ""
@@ -11146,7 +11224,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 ""
@@ -11392,11 +11470,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 ""
@@ -11597,8 +11675,8 @@ msgstr ""
#: src/Navigation.tsx:526
#: src/screens/Search/modules/ExploreInterestsCard.tsx:67
-#: src/screens/Settings/ContentAndMediaSettings.tsx:92
-#: src/screens/Settings/ContentAndMediaSettings.tsx:95
+#: src/screens/Settings/ContentAndMediaSettings.tsx:93
+#: src/screens/Settings/ContentAndMediaSettings.tsx:96
#: src/screens/Settings/InterestsSettings.tsx:46
msgid "Your interests"
msgstr ""
@@ -11612,6 +11690,10 @@ msgstr ""
msgid "Your interests help us find what you like!"
msgstr ""
+#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:68
+msgid "Your live event preferences have been updated."
+msgstr ""
+
#: src/components/dialogs/DeviceLocationRequestDialog.tsx:123
msgid "Your location data is not tracked and does not leave your device."
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/nuxs/definitions.ts b/src/state/queries/nuxs/definitions.ts
index 6c03c936a6..e23050c465 100644
--- a/src/state/queries/nuxs/definitions.ts
+++ b/src/state/queries/nuxs/definitions.ts
@@ -12,6 +12,8 @@ export enum Nux {
BookmarksAnnouncement = 'BookmarksAnnouncement',
FindContactsAnnouncement = 'FindContactsAnnouncement',
FindContactsDismissibleBanner = 'FindContactsDismissibleBanner',
+ LiveNowBetaDialog = 'LiveNowBetaDialog',
+ LiveNowBetaNudge = 'LiveNowBetaNudge',
/*
* Blocking announcements. New IDs are required for each new announcement.
@@ -62,6 +64,14 @@ export type AppNux = BaseNux<
id: Nux.FindContactsDismissibleBanner
data: undefined
}
+ | {
+ id: Nux.LiveNowBetaDialog
+ data: undefined
+ }
+ | {
+ id: Nux.LiveNowBetaNudge
+ data: undefined
+ }
>
export const NuxSchemas: Record | undefined> = {
@@ -75,4 +85,6 @@ export const NuxSchemas: Record | undefined> = {
[Nux.BookmarksAnnouncement]: undefined,
[Nux.FindContactsAnnouncement]: undefined,
[Nux.FindContactsDismissibleBanner]: undefined,
+ [Nux.LiveNowBetaDialog]: undefined,
+ [Nux.LiveNowBetaNudge]: undefined,
}
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/service-config.tsx b/src/state/service-config.tsx
index 242022b60d..a04cbb51ee 100644
--- a/src/state/service-config.tsx
+++ b/src/state/service-config.tsx
@@ -1,7 +1,10 @@
import {createContext, useContext, useMemo} from 'react'
+import {useGate} from '#/lib/statsig/statsig'
import {useLanguagePrefs} from '#/state/preferences/languages'
import {useServiceConfigQuery} from '#/state/queries/service-config'
+import {useSession} from '#/state/session'
+import {IS_DEV} from '#/env'
import {device} from '#/storage'
type TrendingContext = {
@@ -18,7 +21,7 @@ const TrendingContext = createContext({
})
TrendingContext.displayName = 'TrendingContext'
-const LiveNowContext = createContext(null)
+const LiveNowContext = createContext([])
LiveNowContext.displayName = 'LiveNowContext'
const CheckEmailConfirmedContext = createContext(null)
@@ -82,19 +85,28 @@ export function useTrendingConfig() {
return useContext(TrendingContext)
}
-export function useLiveNowConfig() {
+const DEFAULT_LIVE_ALLOWED_DOMAINS = ['twitch.tv', 'www.twitch.tv']
+export type LiveNowConfig = {
+ allowedDomains: Set
+}
+export function useLiveNowConfig(): LiveNowConfig {
const ctx = useContext(LiveNowContext)
- if (!ctx) {
- throw new Error(
- 'useLiveNowConfig must be used within a ServiceConfigManager',
- )
+ const canGoLive = useCanGoLive()
+ const {currentAccount} = useSession()
+ if (!currentAccount?.did || !canGoLive) return {allowedDomains: new Set()}
+ const vip = ctx.find(live => live.did === currentAccount.did)
+ return {
+ allowedDomains: new Set(
+ DEFAULT_LIVE_ALLOWED_DOMAINS.concat(vip ? vip.domains : []),
+ ),
}
- return ctx
}
-export function useCanGoLive(did?: string) {
- const config = useLiveNowConfig()
- return !!config.find(cfg => cfg.did === did)
+export function useCanGoLive() {
+ const gate = useGate()
+ const {hasSession} = useSession()
+ if (!hasSession) return false
+ return IS_DEV ? true : gate('live_now_beta')
}
export function useCheckEmailConfirmed() {
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..97e5e9731e 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') {
@@ -952,7 +954,7 @@ let PostFeed = ({
const actor = post.author
if (
actor.status &&
- validateStatus(actor.did, actor.status, liveNowConfig) &&
+ validateStatus(actor.status, liveNowConfig) &&
isStatusStillActive(actor.status.expiresAt)
) {
if (!seenActorWithStatusRef.current.has(actor.did)) {
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/com/profile/ProfileMenu.tsx b/src/view/com/profile/ProfileMenu.tsx
index 8cf6d2c968..f9c1bac007 100644
--- a/src/view/com/profile/ProfileMenu.tsx
+++ b/src/view/com/profile/ProfileMenu.tsx
@@ -15,6 +15,7 @@ import {logger} from '#/logger'
import {isWeb} from '#/platform/detection'
import {type Shadow} from '#/state/cache/types'
import {useModalControls} from '#/state/modals'
+import {Nux, useNux, useSaveNux} from '#/state/queries/nuxs'
import {
RQKEY as profileQueryKey,
useProfileBlockMutationQueue,
@@ -25,6 +26,7 @@ import {useCanGoLive} from '#/state/service-config'
import {useSession} from '#/state/session'
import {EventStopper} from '#/view/com/util/EventStopper'
import * as Toast from '#/view/com/util/Toast'
+import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog'
import {StarterPackDialog} from '#/components/dialogs/StarterPackDialog'
@@ -59,6 +61,8 @@ import * as Prompt from '#/components/Prompt'
import {useFullVerificationState} from '#/components/verification'
import {VerificationCreatePrompt} from '#/components/verification/VerificationCreatePrompt'
import {VerificationRemovePrompt} from '#/components/verification/VerificationRemovePrompt'
+import {Dot} from '#/features/nuxs/components/Dot'
+import {Gradient} from '#/features/nuxs/components/Gradient'
import {useDevMode} from '#/storage/hooks/dev-mode'
let ProfileMenu = ({
@@ -66,6 +70,7 @@ let ProfileMenu = ({
}: {
profile: Shadow
}): React.ReactNode => {
+ const t = useTheme()
const {_} = useLingui()
const {currentAccount, hasSession} = useSession()
const {openModal} = useModalControls()
@@ -79,8 +84,15 @@ let ProfileMenu = ({
const isLabelerAndNotBlocked = !!profile.associated?.labeler && !isBlocked
const [devModeEnabled] = useDevMode()
const verification = useFullVerificationState({profile})
- const canGoLive = useCanGoLive(currentAccount?.did)
+ const canGoLive = useCanGoLive()
const status = useActorStatus(profile)
+ const statusNudge = useNux(Nux.LiveNowBetaNudge)
+ const statusNudgeActive =
+ isSelf &&
+ canGoLive &&
+ statusNudge.status === 'ready' &&
+ !statusNudge.nux?.completed
+ const {mutate: saveNux} = useSaveNux()
const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile)
const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
@@ -229,17 +241,22 @@ let ProfileMenu = ({
{({props}) => {
return (
-
+ <>
+
+
+ {statusNudgeActive && }
+ >
)
}}
@@ -337,11 +354,19 @@ let ProfileMenu = ({
? _(msg`Edit live status`)
: _(msg`Go live`)
}
- onPress={
- status.isDisabled
- ? goLiveDisabledDialogControl.open
- : goLiveDialogControl.open
- }>
+ onPress={() => {
+ if (status.isDisabled) {
+ goLiveDisabledDialogControl.open()
+ } else {
+ goLiveDialogControl.open()
+ }
+ saveNux({
+ id: Nux.LiveNowBetaNudge,
+ data: undefined,
+ completed: true,
+ })
+ }}>
+ {statusNudgeActive && }
{status.isDisabled ? (
Go live (disabled)
@@ -351,7 +376,26 @@ let ProfileMenu = ({
Go live
)}
-
+ {statusNudgeActive && (
+
+ New
+
+ )}
+ t.palette.primary_500
+ : undefined
+ }
+ />
)}
{verification.viewer.role === 'verifier' &&
diff --git a/src/view/shell/desktop/RightNav.tsx b/src/view/shell/desktop/RightNav.tsx
index 788df6e64c..04dddaf739 100644
--- a/src/view/shell/desktop/RightNav.tsx
+++ b/src/view/shell/desktop/RightNav.tsx
@@ -22,6 +22,7 @@ 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()
@@ -49,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()
@@ -90,7 +92,8 @@ export function DesktopRightNav({routeName}: {routeName: string}) {
>
)}
- {showTrending && }
+ {showExploreScreenDuplicatedContent && }
+ {showExploreScreenDuplicatedContent && }
{hasSession && (
diff --git a/yarn.lock b/yarn.lock
index 16a78ababf..51b7d366c5 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.13", "@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"
@@ -194,16 +194,7 @@
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==
- dependencies:
- "@atproto/lex-data" "0.0.7"
- "@atproto/lex-json" "0.0.7"
- zod "^3.23.8"
-
-"@atproto/common-web@^0.4.12":
+"@atproto/common-web@^0.4.11", "@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==
@@ -437,17 +428,6 @@
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==
- dependencies:
- "@atproto/syntax" "0.4.2"
- multiformats "^9.9.0"
- tslib "^2.8.1"
- uint8arrays "3.0.0"
- unicode-segmenter "^0.14.0"
-
"@atproto/lex-data@0.0.8", "@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"
@@ -484,14 +464,6 @@
"@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==
- dependencies:
- "@atproto/lex-data" "0.0.7"
- tslib "^2.8.1"
-
"@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"
@@ -6212,20 +6184,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"