Compare commits

..

1 Commits

Author SHA1 Message Date
Eric Bailey 65bf8aefc8 Add some device location debug 2026-01-13 10:14:39 -06:00
90 changed files with 873 additions and 3578 deletions
+2 -5
View File
@@ -34,8 +34,5 @@ EXPO_PUBLIC_SENTRY_DSN=
# Bitdrift API key. If undefined, Bitdrift will be disabled.
EXPO_PUBLIC_BITDRIFT_API_KEY=
# geolocation web worker URL
GEOLOCATION_DEV_URL=
# live-events web worker URL
LIVE_EVENTS_DEV_URL=
# bapp-config web worker URL
BAPP_CONFIG_DEV_URL=
-1
View File
@@ -35,7 +35,6 @@ module.exports = {
'Admonition',
'Admonition.Admonition',
'Toast.Action',
'toast.Action',
'AgeAssuranceAdmonition',
'Span',
'StackedButton',
-591
View File
@@ -1,591 +0,0 @@
# 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 (
<View style={[a.flex_row, a.gap_md, a.p_lg, t.atoms.bg]}>
<Text style={[a.text_md, a.font_bold, t.atoms.text]}>
Hello
</Text>
</View>
)
}
```
### 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 (
<>
<Button label="Open" onPress={control.open}>
<ButtonText>Open Dialog</ButtonText>
</Button>
<Dialog.Outer control={control}>
<Dialog.Handle /> {/* Native drag handle */}
<Dialog.ScrollableInner label={_(msg`My Dialog`)}>
<Dialog.Header>
<Dialog.HeaderText>Title</Dialog.HeaderText>
</Dialog.Header>
<Text>Dialog content here</Text>
<Button label="Close" onPress={() => control.close()}>
<ButtonText>Close</ButtonText>
</Button>
</Dialog.ScrollableInner>
</Dialog.Outer>
</>
)
}
```
### 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 (
<Menu.Root>
<Menu.Trigger label="Open menu">
{({props}) => (
<Button {...props} label="Menu">
<ButtonIcon icon={DotsHorizontal} />
</Button>
)}
</Menu.Trigger>
<Menu.Outer>
<Menu.Group>
<Menu.Item label="Edit" onPress={handleEdit}>
<Menu.ItemIcon icon={Pencil} />
<Menu.ItemText>Edit</Menu.ItemText>
</Menu.Item>
<Menu.Item label="Delete" onPress={handleDelete}>
<Menu.ItemIcon icon={Trash} />
<Menu.ItemText>Delete</Menu.ItemText>
</Menu.Item>
</Menu.Group>
</Menu.Outer>
</Menu.Root>
)
}
```
### Button Component
```tsx
import {Button, ButtonText, ButtonIcon} from '#/components/Button'
// Solid primary button (most common)
<Button label="Save" onPress={handleSave} color="primary" size="large">
<ButtonText>Save</ButtonText>
</Button>
// With icon
<Button label="Share" onPress={handleShare} color="secondary" size="small">
<ButtonIcon icon={Share} />
<ButtonText>Share</ButtonText>
</Button>
// Icon-only button
<Button label="Close" onPress={handleClose} color="secondary" size="small" shape="round">
<ButtonIcon icon={X} />
</Button>
// Ghost variant (deprecated - use color prop)
<Button label="Cancel" variant="ghost" color="secondary" size="small">
<ButtonText>Cancel</ButtonText>
</Button>
```
**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'
<H1 style={[a.text_xl, a.font_bold]}>Heading</H1>
<P>Paragraph text with default styling.</P>
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>Custom text</Text>
// For text with emoji, add the emoji prop
<Text emoji>Hello! 👋</Text>
```
### TextField
```tsx
import * as TextField from '#/components/forms/TextField'
<TextField.LabelText>Email</TextField.LabelText>
<TextField.Root>
<TextField.Icon icon={AtSign} />
<TextField.Input
label="Email address"
placeholder="you@example.com"
defaultValue={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
/>
</TextField.Root>
```
## 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 (
<Text>
<Trans>Welcome to <Text style={a.font_bold}>Bluesky</Text></Trans>
</Text>
)
}
```
**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 (
<Toggle
value={autoplayDisabled}
onValueChange={setAutoplayDisabled}
/>
)
}
```
### Session State
```tsx
import {useSession, useAgent} from '#/state/session'
function MyComponent() {
const {hasSession, currentAccount} = useSession()
const agent = useAgent()
if (!hasSession) {
return <LoginPrompt />
}
// 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<CommonNavigatorParams, 'Profile'>
export function ProfileScreen({route, navigation}: Props) {
const {name} = route.params // Type-safe params
return (
<Layout.Screen>
{/* Screen content */}
</Layout.Screen>
)
}
// 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
<TextField.Input
defaultValue={initialEmail}
onChangeText={setEmail}
/>
// Avoid when possible - controlled (can cause performance issues)
<TextField.Input
value={email}
onChangeText={setEmail}
/>
```
### 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 `<Trans>`
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` |
Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

+8 -45
View File
@@ -37,6 +37,14 @@
font-style: italic;
font-display: swap;
}
html {
background-color: white;
}
@media (prefers-color-scheme: dark) {
html {
background-color: black;
}
}
html,
body {
margin: 0px;
@@ -51,19 +59,6 @@
-ms-overflow-style: scrollbar;
font-synthesis-weight: none;
}
:root {
--text: black;
--background: white;
--backgroundLight: #e2e7ee;
}
@media (prefers-color-scheme: dark) {
:root {
color-scheme: dark;
--text: white;
--background: black;
--backgroundLight: #232e3e;
}
}
html,
body,
#root {
@@ -72,32 +67,6 @@
min-height: 100%;
width: 100%;
}
html.theme--light,
html.theme--light body,
html.theme--light #root {
background-color: white;
--text: black;
--background: white;
--backgroundLight: #DCE2EA;
}
html.theme--dark,
html.theme--dark body,
html.theme--dark #root {
color-scheme: dark;
background-color: black;
--text: white;
--background: black;
--backgroundLight: #232E3E;
}
html.theme--dim,
html.theme--dim body,
html.theme--dim #root {
color-scheme: dark;
background-color: #151D28;
--text: white;
--background: #151D28;
--backgroundLight: #2C3A4E;
}
#splash {
display: flex;
position: fixed;
@@ -124,12 +93,6 @@
overflow-y: scroll;
}
</style>
<script>
const theme = localStorage.getItem('ALF_THEME')
if (theme) {
document.documentElement.classList.add(`theme--${theme}`)
}
</script>
{% include "scripts.html" %}
<link rel="apple-touch-icon" sizes="180x180" href="{{ staticCDNHost }}/static/apple-touch-icon.png">
+2 -4
View File
@@ -73,7 +73,7 @@
"icons:optimize": "svgo -f ./assets/icons"
},
"dependencies": {
"@atproto/api": "^0.18.15",
"@atproto/api": "^0.18.13",
"@bitdrift/react-native": "^0.6.8",
"@braintree/sanitize-url": "^6.0.2",
"@bsky.app/alf": "^0.1.6",
@@ -156,7 +156,6 @@
"expo-location": "~19.0.8",
"expo-media-library": "~18.2.1",
"expo-notifications": "~0.32.14",
"expo-privacy-sensitive": "^0.1.0",
"expo-screen-orientation": "~9.0.8",
"expo-sharing": "~14.0.8",
"expo-sms": "^14.0.7",
@@ -227,7 +226,7 @@
"zod": "^3.20.2"
},
"devDependencies": {
"@atproto/dev-env": "^0.3.204",
"@atproto/dev-env": "^0.3.196",
"@babel/core": "^7.26.0",
"@babel/preset-env": "^7.26.0",
"@babel/runtime": "^7.26.0",
@@ -285,7 +284,6 @@
"@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",
-116
View File
@@ -18,119 +18,3 @@ index 0000000..3b5b864
@@ -0,0 +1,2 @@
+# Keep FullscreenActivity from being stripped by R8/ProGuard
+-keep class expo.modules.blueskyvideo.FullscreenActivity { *; }
diff --git a/node_modules/@haileyok/bluesky-video/android/src/main/java/expo/modules/blueskyvideo/BlueskyVideoView.kt b/node_modules/@haileyok/bluesky-video/android/src/main/java/expo/modules/blueskyvideo/BlueskyVideoView.kt
index fdabd84..eda8c7c 100644
--- a/node_modules/@haileyok/bluesky-video/android/src/main/java/expo/modules/blueskyvideo/BlueskyVideoView.kt
+++ b/node_modules/@haileyok/bluesky-video/android/src/main/java/expo/modules/blueskyvideo/BlueskyVideoView.kt
@@ -1,8 +1,11 @@
package expo.modules.blueskyvideo
+import android.app.Activity
import android.content.Context
import android.content.Intent
import android.graphics.Color
+import android.os.Build
+import android.util.Log
import android.graphics.Rect
import android.net.Uri
import android.view.ViewGroup
@@ -237,9 +240,44 @@ class BlueskyVideoView(
// Fullscreen handling
fun enterFullscreen(keepDisplayOn: Boolean) {
- val currentActivity = this.appContext.currentActivity ?: return
+ val tag = "BlueskyVideo"
+
+ Log.d(tag, "enterFullscreen() called - keepDisplayOn=$keepDisplayOn")
+ Log.d(tag, " isFullscreen=$isFullscreen, isPlaying=$isPlaying, isMuted=$isMuted")
+ Log.d(tag, " player=${player != null}, url=$url")
+ Log.d(tag, " isAttachedToWindow=$isAttachedToWindow, isShown=$isShown")
+ Log.d(tag, " Android SDK: ${Build.VERSION.SDK_INT}, Device: ${Build.MANUFACTURER} ${Build.MODEL}")
+
+ val currentActivity = this.appContext.currentActivity
+ if (currentActivity == null) {
+ Log.e(tag, "enterFullscreen() FAILED: currentActivity is null")
+ Log.e(tag, " appContext=$appContext")
+ onError(mapOf("error" to "Cannot enter fullscreen: no current activity"))
+ return
+ }
+
+ Log.d(tag, " currentActivity=$currentActivity")
+ Log.d(tag, " activity.isFinishing=${currentActivity.isFinishing}")
+ Log.d(tag, " activity.isDestroyed=${currentActivity.isDestroyed}")
+ Log.d(tag, " activity.lifecycle=${(currentActivity as? androidx.lifecycle.LifecycleOwner)?.lifecycle?.currentState}")
+ Log.d(tag, " activity.hasWindowFocus=${currentActivity.hasWindowFocus()}")
+ Log.d(tag, " activity.window.isActive=${currentActivity.window?.isActive}")
+
+ // Check if activity is in a valid state to start another activity
+ if (currentActivity.isFinishing) {
+ Log.e(tag, "enterFullscreen() FAILED: currentActivity is finishing")
+ onError(mapOf("error" to "Cannot enter fullscreen: activity is finishing"))
+ return
+ }
+
+ if (currentActivity.isDestroyed) {
+ Log.e(tag, "enterFullscreen() FAILED: currentActivity is destroyed")
+ onError(mapOf("error" to "Cannot enter fullscreen: activity is destroyed"))
+ return
+ }
this.enteredFullscreenMuteState = this.isMuted
+ Log.d(tag, " saved enteredFullscreenMuteState=$enteredFullscreenMuteState")
// We always want to start with unmuted state and playing. Fire those from here so the
// event dispatcher gets called
@@ -247,18 +285,51 @@ class BlueskyVideoView(
if (!this.isPlaying) {
this.play()
}
+ Log.d(tag, " after unmute/play: isPlaying=$isPlaying, isMuted=$isMuted")
// Remove the player from this view, but don't null the player!
this.playerView.player = null
+ Log.d(tag, " detached player from playerView")
// create the intent and give it a view
val intent = Intent(context, FullscreenActivity::class.java)
intent.putExtra("keepDisplayOn", keepDisplayOn)
FullscreenActivity.asscVideoView = WeakReference(this)
+ Log.d(tag, " intent created: $intent")
+ Log.d(tag, " intent.component=${intent.component}")
+ Log.d(tag, " intent.flags=${intent.flags} (0x${Integer.toHexString(intent.flags)})")
+ Log.d(tag, " context for intent=$context")
+ Log.d(tag, " FullscreenActivity.asscVideoView set to WeakReference(this)")
+
// fire the fullscreen event and launch the intent
- this.isFullscreen = true
- currentActivity.startActivity(intent)
+ try {
+ Log.d(tag, " calling startActivity()...")
+ currentActivity.startActivity(intent)
+ this.isFullscreen = true
+ Log.d(tag, " startActivity() SUCCESS - isFullscreen set to true")
+ } catch (e: Exception) {
+ Log.e(tag, "enterFullscreen() FAILED: startActivity() threw exception", e)
+ Log.e(tag, " exception class: ${e.javaClass.name}")
+ Log.e(tag, " exception message: ${e.message}")
+ Log.e(tag, " exception cause: ${e.cause}")
+ e.printStackTrace()
+
+ // Restore state since fullscreen failed
+ this.playerView.player = this.player
+ Log.d(tag, " restored player to playerView after failure")
+
+ if (this.enteredFullscreenMuteState) {
+ this.mute()
+ Log.d(tag, " restored mute state after failure")
+ }
+
+ onError(mapOf(
+ "error" to "Failed to enter fullscreen: ${e.message}",
+ "exceptionClass" to e.javaClass.name,
+ "exceptionMessage" to (e.message ?: "unknown")
+ ))
+ }
}
fun onExitFullscreen() {
-16
View File
@@ -1,16 +0,0 @@
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)
}
-103
View File
@@ -1,103 +0,0 @@
diff --git a/node_modules/expo-image/build/Image.types.d.ts b/node_modules/expo-image/build/Image.types.d.ts
index 022ae48..416504f 100644
--- a/node_modules/expo-image/build/Image.types.d.ts
+++ b/node_modules/expo-image/build/Image.types.d.ts
@@ -152,6 +152,16 @@ export interface ImageProps extends Omit<ViewProps, 'style' | 'children'> {
* @default 'normal'
*/
priority?: 'low' | 'normal' | 'high' | null;
+ /**
+ * The loading behavior for the image. Maps to the native HTML `loading` attribute on web.
+ *
+ * - `'lazy'` - Defers loading until the image is near the viewport.
+ * - `'eager'` - Loads the image immediately.
+ *
+ * @default undefined
+ * @platform web
+ */
+ loading?: 'lazy' | 'eager' | null;
/**
* Determines whether to cache the image and where: on the disk, in the memory or both.
*
diff --git a/node_modules/expo-image/src/ExpoImage.web.tsx b/node_modules/expo-image/src/ExpoImage.web.tsx
index 2a49ff0..1c3de93 100644
--- a/node_modules/expo-image/src/ExpoImage.web.tsx
+++ b/node_modules/expo-image/src/ExpoImage.web.tsx
@@ -70,6 +70,7 @@ export default function ExpoImage({
onLoadEnd,
onDisplay,
priority,
+ loading,
blurRadius,
recyclingKey,
style,
@@ -118,6 +119,7 @@ export default function ExpoImage({
accessibilityLabel={accessibilityLabel ?? alt}
cachePolicy={cachePolicy}
priority={priority}
+ loading={loading}
tintColor={tintColor}
/>
),
@@ -149,6 +151,7 @@ export default function ExpoImage({
className={className}
cachePolicy={cachePolicy}
priority={priority}
+ loading={loading}
contentPosition={selectedSource ? contentPosition : { top: '50%', left: '50%' }}
hashPlaceholderContentPosition={contentPosition}
hashPlaceholderStyle={imageHashStyle}
diff --git a/node_modules/expo-image/src/Image.types.ts b/node_modules/expo-image/src/Image.types.ts
index 9dec0e7..61c1621 100644
--- a/node_modules/expo-image/src/Image.types.ts
+++ b/node_modules/expo-image/src/Image.types.ts
@@ -178,6 +178,17 @@ export interface ImageProps extends Omit<ViewProps, 'style' | 'children'> {
*/
priority?: 'low' | 'normal' | 'high' | null;
+ /**
+ * The loading behavior for the image. Maps to the native HTML `loading` attribute on web.
+ *
+ * - `'lazy'` - Defers loading until the image is near the viewport.
+ * - `'eager'` - Loads the image immediately.
+ *
+ * @default undefined
+ * @platform web
+ */
+ loading?: 'lazy' | 'eager' | null;
+
/**
* Determines whether to cache the image and where: on the disk, in the memory or both.
*
diff --git a/node_modules/expo-image/src/web/ImageWrapper.tsx b/node_modules/expo-image/src/web/ImageWrapper.tsx
index e8f891d..89a5cb1 100644
--- a/node_modules/expo-image/src/web/ImageWrapper.tsx
+++ b/node_modules/expo-image/src/web/ImageWrapper.tsx
@@ -30,6 +30,7 @@ const ImageWrapper = React.forwardRef(
contentPosition,
hashPlaceholderContentPosition,
priority,
+ loading,
style,
hashPlaceholderStyle,
tintColor,
@@ -82,6 +83,7 @@ const ImageWrapper = React.forwardRef(
// @ts-ignore
// eslint-disable-next-line react/no-unknown-property
fetchPriority={getFetchPriorityFromImagePriority(priority || 'normal')}
+ loading={loading || undefined}
{...getImageWrapperEventHandler(events, sourceWithHeaders)}
{...getImgPropsFromSource(source)}
{...props}
diff --git a/node_modules/expo-image/src/web/ImageWrapper.types.ts b/node_modules/expo-image/src/web/ImageWrapper.types.ts
index 19bbe2f..179837f 100644
--- a/node_modules/expo-image/src/web/ImageWrapper.types.ts
+++ b/node_modules/expo-image/src/web/ImageWrapper.types.ts
@@ -29,6 +29,7 @@ export type ImageWrapperProps = {
contentPosition?: ImageContentPositionObject;
hashPlaceholderContentPosition?: ImageContentPositionObject;
priority?: string | null;
+ loading?: 'lazy' | 'eager' | null;
style: CSSProperties;
tintColor?: string | null;
hashPlaceholderStyle?: CSSProperties;
+43 -50
View File
@@ -69,10 +69,6 @@ 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'
@@ -96,7 +92,6 @@ if (isAndroid) {
*/
Geo.resolve()
prefetchAgeAssuranceConfig()
prefetchLiveEvents()
function InnerApp() {
const [isReady, setIsReady] = React.useState(false)
@@ -146,51 +141,49 @@ function InnerApp() {
<QueryProvider currentDid={currentAccount?.did}>
<PolicyUpdateOverlayProvider>
<StatsigProvider>
<LiveEventsProvider>
<AgeAssuranceV2Provider>
<ComposerProvider>
<MessagesProvider>
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
<LabelDefsProvider>
<ModerationOptsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<HiddenRepliesProvider>
<HomeBadgeProvider>
<UnreadNotifsProvider>
<BackgroundNotificationPreferencesProvider>
<MutedThreadsProvider>
<ProgressGuideProvider>
<ServiceAccountManager>
<EmailVerificationProvider>
<HideBottomBarBorderProvider>
<GestureHandlerRootView
style={s.h100pct}>
<GlobalGestureEventsProvider>
<IntentDialogProvider>
<TestCtrls />
<Shell />
<ToastOutlet />
</IntentDialogProvider>
</GlobalGestureEventsProvider>
</GestureHandlerRootView>
</HideBottomBarBorderProvider>
</EmailVerificationProvider>
</ServiceAccountManager>
</ProgressGuideProvider>
</MutedThreadsProvider>
</BackgroundNotificationPreferencesProvider>
</UnreadNotifsProvider>
</HomeBadgeProvider>
</HiddenRepliesProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</ModerationOptsProvider>
</LabelDefsProvider>
</MessagesProvider>
</ComposerProvider>
</AgeAssuranceV2Provider>
</LiveEventsProvider>
<AgeAssuranceV2Provider>
<ComposerProvider>
<MessagesProvider>
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
<LabelDefsProvider>
<ModerationOptsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<HiddenRepliesProvider>
<HomeBadgeProvider>
<UnreadNotifsProvider>
<BackgroundNotificationPreferencesProvider>
<MutedThreadsProvider>
<ProgressGuideProvider>
<ServiceAccountManager>
<EmailVerificationProvider>
<HideBottomBarBorderProvider>
<GestureHandlerRootView
style={s.h100pct}>
<GlobalGestureEventsProvider>
<IntentDialogProvider>
<TestCtrls />
<Shell />
<ToastOutlet />
</IntentDialogProvider>
</GlobalGestureEventsProvider>
</GestureHandlerRootView>
</HideBottomBarBorderProvider>
</EmailVerificationProvider>
</ServiceAccountManager>
</ProgressGuideProvider>
</MutedThreadsProvider>
</BackgroundNotificationPreferencesProvider>
</UnreadNotifsProvider>
</HomeBadgeProvider>
</HiddenRepliesProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</ModerationOptsProvider>
</LabelDefsProvider>
</MessagesProvider>
</ComposerProvider>
</AgeAssuranceV2Provider>
</StatsigProvider>
</PolicyUpdateOverlayProvider>
</QueryProvider>
+39 -46
View File
@@ -57,10 +57,6 @@ 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'
@@ -71,7 +67,6 @@ import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottom
*/
Geo.resolve()
prefetchAgeAssuranceConfig()
prefetchLiveEvents()
function InnerApp() {
const [isReady, setIsReady] = React.useState(false)
@@ -122,47 +117,45 @@ function InnerApp() {
<QueryProvider currentDid={currentAccount?.did}>
<PolicyUpdateOverlayProvider>
<StatsigProvider>
<LiveEventsProvider>
<AgeAssuranceV2Provider>
<ComposerProvider>
<MessagesProvider>
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
<LabelDefsProvider>
<ModerationOptsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<HiddenRepliesProvider>
<HomeBadgeProvider>
<UnreadNotifsProvider>
<BackgroundNotificationPreferencesProvider>
<MutedThreadsProvider>
<SafeAreaProvider>
<ProgressGuideProvider>
<ServiceConfigProvider>
<EmailVerificationProvider>
<HideBottomBarBorderProvider>
<IntentDialogProvider>
<Shell />
<ToastOutlet />
</IntentDialogProvider>
</HideBottomBarBorderProvider>
</EmailVerificationProvider>
</ServiceConfigProvider>
</ProgressGuideProvider>
</SafeAreaProvider>
</MutedThreadsProvider>
</BackgroundNotificationPreferencesProvider>
</UnreadNotifsProvider>
</HomeBadgeProvider>
</HiddenRepliesProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</ModerationOptsProvider>
</LabelDefsProvider>
</MessagesProvider>
</ComposerProvider>
</AgeAssuranceV2Provider>
</LiveEventsProvider>
<AgeAssuranceV2Provider>
<ComposerProvider>
<MessagesProvider>
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
<LabelDefsProvider>
<ModerationOptsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<HiddenRepliesProvider>
<HomeBadgeProvider>
<UnreadNotifsProvider>
<BackgroundNotificationPreferencesProvider>
<MutedThreadsProvider>
<SafeAreaProvider>
<ProgressGuideProvider>
<ServiceConfigProvider>
<EmailVerificationProvider>
<HideBottomBarBorderProvider>
<IntentDialogProvider>
<Shell />
<ToastOutlet />
</IntentDialogProvider>
</HideBottomBarBorderProvider>
</EmailVerificationProvider>
</ServiceConfigProvider>
</ProgressGuideProvider>
</SafeAreaProvider>
</MutedThreadsProvider>
</BackgroundNotificationPreferencesProvider>
</UnreadNotifsProvider>
</HomeBadgeProvider>
</HiddenRepliesProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</ModerationOptsProvider>
</LabelDefsProvider>
</MessagesProvider>
</ComposerProvider>
</AgeAssuranceV2Provider>
</StatsigProvider>
</PolicyUpdateOverlayProvider>
</QueryProvider>
+3 -3
View File
@@ -12,12 +12,12 @@ export const enabled = (IS_DEV && false) || IS_E2E
export const geolocation: Geolocation | undefined = enabled
? {
countryCode: 'AA',
countryCode: 'BB',
regionCode: undefined,
}
: undefined
const deviceGeolocationEnabled = false || IS_E2E
const deviceGeolocationEnabled = false
export const deviceGeolocation: Geolocation | undefined =
enabled && deviceGeolocationEnabled
? {
@@ -46,7 +46,7 @@ export const config: AppBskyAgeassuranceDefs.Config = {
rules: [
{
$type: ids.Default,
access: 'full',
access: 'none',
},
],
},
+1 -6
View File
@@ -11,12 +11,7 @@ import {
import {themes} from '#/alf/themes'
import {type Device} from '#/storage'
export {
type TextStyleProp,
type Theme,
utils,
type ViewStyleProp,
} from '@bsky.app/alf'
export {type TextStyleProp, type Theme, type ViewStyleProp} from '@bsky.app/alf'
export {atoms} from '#/alf/atoms'
export * from '#/alf/breakpoints'
export * from '#/alf/fonts'
-1
View File
@@ -51,7 +51,6 @@ function updateDocument(theme: ThemeName) {
html.classList.add(`theme--${theme}`)
// set color to 'theme-color' meta tag
meta?.setAttribute('content', getBackgroundColor(theme))
window.localStorage.setItem('ALF_THEME', theme)
}
}
+2 -8
View File
@@ -359,13 +359,7 @@ export function FlatListFooter({children}: {children: React.ReactNode}) {
)
}
export function Handle({
difference = false,
fill,
}: {
difference?: boolean
fill?: string
}) {
export function Handle({difference = false}: {difference?: boolean}) {
const t = useTheme()
const {_} = useLingui()
const {screenReaderEnabled} = useA11y()
@@ -396,7 +390,7 @@ export function Handle({
opacity: 0.75,
}
: {
backgroundColor: fill || t.palette.contrast_975,
backgroundColor: t.palette.contrast_975,
opacity: 0.5,
},
]}
+3 -35
View File
@@ -1,4 +1,4 @@
import React, {useMemo} from 'react'
import React from 'react'
import {type GestureResponderEvent, View} from 'react-native'
import {
type AppBskyFeedDefs,
@@ -21,21 +21,19 @@ 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, select, useTheme} from '#/alf'
import {atoms as a, 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'
@@ -51,11 +49,7 @@ export function Default(props: Props) {
<Outer>
<Header>
<Avatar src={view.avatar} />
<TitleAndByline
title={view.displayName}
creator={view.creator}
uri={view.uri}
/>
<TitleAndByline title={view.displayName} creator={view.creator} />
<SaveButton view={view} pin />
</Header>
<Description description={view.description} />
@@ -124,40 +118,14 @@ export function AvatarPlaceholder({size = 40}: Omit<AvatarProps, 'src'>) {
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 (
<View style={[a.flex_1]}>
{uri && activeLiveEvents.has(uri) && (
<View style={[a.flex_row, a.align_center, a.gap_2xs]}>
<LiveIcon size="xs" fill={liveColor} />
<Text
style={[
a.text_2xs,
a.font_medium,
a.leading_snug,
{color: liveColor},
]}>
<Trans>Happening now</Trans>
</Text>
</View>
)}
<Text
emoji
style={[a.text_md, a.font_semi_bold, a.leading_snug]}
-1
View File
@@ -842,7 +842,6 @@ export function SuggestedFeeds() {
<FeedCard.TitleAndByline
title={feed.displayName}
creator={feed.creator}
uri={feed.uri}
/>
</FeedCard.Header>
<FeedCard.Description
+5 -7
View File
@@ -167,7 +167,6 @@ export function Item({children, label, style, onPress, ...rest}: ItemProps) {
a.gap_sm,
a.px_md,
a.rounded_md,
a.overflow_hidden,
a.border,
t.atoms.bg_contrast_25,
t.atoms.border_contrast_low,
@@ -194,6 +193,7 @@ export function ItemText({children, style}: ItemTextProps) {
a.text_md,
a.font_semi_bold,
t.atoms.text_contrast_high,
{paddingTop: 3},
style,
disabled && t.atoms.text_contrast_low,
]}>
@@ -202,18 +202,16 @@ export function ItemText({children, style}: ItemTextProps) {
)
}
export function ItemIcon({icon: Comp, fill}: ItemIconProps) {
export function ItemIcon({icon: Comp}: ItemIconProps) {
const t = useTheme()
const {disabled} = useMenuItemContext()
return (
<Comp
size="lg"
fill={
fill
? fill({disabled})
: disabled
? t.atoms.text_contrast_low.color
: t.atoms.text_contrast_medium.color
disabled
? t.atoms.text_contrast_low.color
: t.atoms.text_contrast_medium.color
}
/>
)
+4 -7
View File
@@ -262,7 +262,6 @@ 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) &&
@@ -303,7 +302,7 @@ export function ItemText({children, style}: ItemTextProps) {
)
}
export function ItemIcon({icon: Comp, position = 'left', fill}: ItemIconProps) {
export function ItemIcon({icon: Comp, position = 'left'}: ItemIconProps) {
const t = useTheme()
const {disabled} = useMenuItemContext()
return (
@@ -320,11 +319,9 @@ export function ItemIcon({icon: Comp, position = 'left', fill}: ItemIconProps) {
<Comp
size="md"
fill={
fill
? fill({disabled})
: disabled
? t.atoms.text_contrast_low.color
: t.atoms.text_contrast_medium.color
disabled
? t.atoms.text_contrast_low.color
: t.atoms.text_contrast_medium.color
}
/>
</View>
-1
View File
@@ -107,7 +107,6 @@ export type ItemTextProps = React.PropsWithChildren<TextStyleProp & {}>
export type ItemIconProps = React.PropsWithChildren<{
icon: React.ComponentType<SVGIconProps>
position?: 'left' | 'right'
fill?: (props: {disabled: boolean}) => string
}>
export type GroupProps = React.PropsWithChildren<ViewStyleProp & {}>
@@ -226,7 +226,6 @@ export function ExternalPlayer({
style={[a.flex_1]}
source={{uri: link.thumb}}
accessibilityIgnoresInvertColors
loading="lazy"
/>
<Fill
style={[
@@ -100,7 +100,6 @@ export const ExternalEmbed = ({
style={[a.aspect_card]}
source={{uri: imageUri}}
accessibilityIgnoresInvertColors
loading="lazy"
/>
) : undefined}
+3 -3
View File
@@ -17,16 +17,16 @@ export function FeedEmbed({
return (
<FeedCard.Link
view={embed.view}
style={[a.border, t.atoms.border_contrast_low, a.p_sm, a.rounded_md]}>
style={[a.border, t.atoms.border_contrast_low, a.p_md, a.rounded_sm]}>
<FeedCard.Outer>
<FeedCard.Header>
<FeedCard.Avatar src={embed.view.avatar} size={48} />
<FeedCard.Avatar src={embed.view.avatar} />
<FeedCard.TitleAndByline
title={embed.view.displayName}
creator={embed.view.creator}
uri={embed.view.uri}
/>
</FeedCard.Header>
<FeedCard.Likes count={embed.view.likeCount || 0} />
</FeedCard.Outer>
</FeedCard.Link>
)
+2 -2
View File
@@ -10,9 +10,9 @@ import {Image} from 'expo-image'
import {useLightboxControls} from '#/state/lightbox'
import {type Dimensions} from '#/view/com/lightbox/ImageViewing/@types'
import {AutoSizedImage} from '#/view/com/util/images/AutoSizedImage'
import {ImageLayoutGrid} from '#/view/com/util/images/ImageLayoutGrid'
import {atoms as a} from '#/alf'
import {AutoSizedImage} from '#/components/images/AutoSizedImage'
import {ImageLayoutGrid} from '#/components/images/ImageLayoutGrid'
import {PostEmbedViewContext} from '#/components/Post/Embed/types'
import {type EmbedType} from '#/types/bsky/post'
import {type CommonProps} from './types'
@@ -6,10 +6,10 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
import {ConstrainedImage} from '#/view/com/util/images/AutoSizedImage'
import {atoms as a} from '#/alf'
import {Button} from '#/components/Button'
import {useThrottledValue} from '#/components/hooks/useThrottledValue'
import {ConstrainedImage} from '#/components/images/AutoSizedImage'
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
import {VideoEmbedInnerNative} from './VideoEmbedInner/VideoEmbedInnerNative'
import * as VideoFallback from './VideoEmbedInner/VideoFallback'
@@ -13,10 +13,10 @@ import {useLingui} from '@lingui/react'
import {isFirefox} from '#/lib/browser'
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
import {ConstrainedImage} from '#/view/com/util/images/AutoSizedImage'
import {atoms as a, useTheme} from '#/alf'
import {useIsWithinMessage} from '#/components/dms/MessageContext'
import {useFullscreen} from '#/components/hooks/useFullscreen'
import {ConstrainedImage} from '#/components/images/AutoSizedImage'
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
import {
HLSUnsupportedError,
+7 -16
View File
@@ -48,11 +48,10 @@ export function RichTextTag({
reset: resetRemove,
} = useRemoveMutedWordsMutation()
const navigation = useNavigation<NavigationProp>()
const isCashtag = tag.startsWith('$')
const label = isCashtag ? _(msg`Cashtag ${tag}`) : _(msg`Hashtag ${tag}`)
const label = _(msg`Hashtag ${tag}`)
const hint = isNative
? _(msg`Long press to open tag menu for ${isCashtag ? tag : `#${tag}`}`)
: _(msg`Click to open tag menu for ${isCashtag ? tag : `#${tag}`}`)
? _(msg`Long press to open tag menu for #${tag}`)
: _(msg`Click to open tag menu for ${tag}`)
const isMuted = Boolean(
(preferences?.moderationPrefs.mutedWords?.find(
@@ -110,24 +109,20 @@ export function RichTextTag({
<Menu.Outer>
<Menu.Group>
<Menu.Item
label={_(msg`See ${isCashtag ? tag : `#${tag}`} posts`)}
label={_(msg`See ${tag} posts`)}
onPress={() => {
navigation.push('Hashtag', {
tag: encodeURIComponent(tag),
})
}}>
<Menu.ItemText>
{isCashtag ? (
<Trans>See {tag} posts</Trans>
) : (
<Trans>See #{tag} posts</Trans>
)}
<Trans>See #{tag} posts</Trans>
</Menu.ItemText>
<Menu.ItemIcon icon={Search} />
</Menu.Item>
{authorHandle && !isInvalidHandle(authorHandle) && (
<Menu.Item
label={_(msg`See ${isCashtag ? tag : `#${tag}`} posts by user`)}
label={_(msg`See ${tag} posts by user`)}
onPress={() => {
navigation.push('Hashtag', {
tag: encodeURIComponent(tag),
@@ -135,11 +130,7 @@ export function RichTextTag({
})
}}>
<Menu.ItemText>
{isCashtag ? (
<Trans>See {tag} posts by user</Trans>
) : (
<Trans>See #{tag} posts by user</Trans>
)}
<Trans>See #{tag} posts by user</Trans>
</Menu.ItemText>
<Menu.ItemIcon icon={Person} />
</Menu.Item>
+33 -68
View File
@@ -1,4 +1,4 @@
import {lazy, useState} from 'react'
import {lazy} from 'react'
import {View} from 'react-native'
// @ts-expect-error missing types
import QRCode from 'react-native-qrcode-styled'
@@ -102,74 +102,39 @@ export function QrCode({
export function QrCodeInner({link}: {link: string}) {
const t = useTheme()
const [logoArea, setLogoArea] = useState<{
x: number
y: number
width: number
height: number
} | null>(null)
const onLogoAreaChange = (area: {
x: number
y: number
width: number
height: number
}) => {
setLogoArea(area)
}
return (
<View style={{position: 'relative'}}>
{/* An SVG version of the logo is placed on top of normal `QRCode` `logo` prop, since the PNG fails to load before the export completes on web. */}
{isWeb && logoArea && (
<View
style={{
position: 'absolute',
left: logoArea.x,
top: logoArea.y + 1,
zIndex: 1,
padding: 4,
}}>
<Logo width={logoArea.width - 14} height={logoArea.height - 14} />
</View>
)}
<QRCode
data={link}
style={[
a.rounded_sm,
{height: 225, width: 225, backgroundColor: '#f3f3f3'},
]}
pieceSize={isWeb ? 8 : 6}
padding={20}
pieceBorderRadius={isWeb ? 4.5 : 3.5}
outerEyesOptions={{
topLeft: {
borderRadius: [12, 12, 0, 12],
color: t.palette.primary_500,
},
topRight: {
borderRadius: [12, 12, 12, 0],
color: t.palette.primary_500,
},
bottomLeft: {
borderRadius: [12, 0, 12, 12],
color: t.palette.primary_500,
},
}}
innerEyesOptions={{borderRadius: 3}}
logo={{
href: require('../../../assets/logo.png'),
...(isWeb && {
onChange: onLogoAreaChange,
padding: 28,
}),
...(!isWeb && {
padding: 2,
scale: 0.95,
}),
hidePieces: true,
}}
/>
</View>
<QRCode
data={link}
style={[
a.rounded_sm,
{height: 225, width: 225, backgroundColor: '#f3f3f3'},
]}
pieceSize={isWeb ? 8 : 6}
padding={20}
// pieceLiquidRadius={2}
pieceBorderRadius={isWeb ? 4.5 : 3.5}
outerEyesOptions={{
topLeft: {
borderRadius: [12, 12, 0, 12],
color: t.palette.primary_500,
},
topRight: {
borderRadius: [12, 12, 12, 0],
color: t.palette.primary_500,
},
bottomLeft: {
borderRadius: [12, 0, 12, 12],
color: t.palette.primary_500,
},
}}
innerEyesOptions={{borderRadius: 3}}
logo={{
href: require('../../../assets/logo.png'),
scale: 0.95,
padding: 2,
hidePieces: true,
}}
/>
)
}
@@ -68,12 +68,10 @@ export function SubscribeProfileButton({
const Icon = isSubscribed ? BellRingingIcon : BellPlusIcon
const tooltipVisible = showTooltip && !disableHint
return (
<>
<Tooltip.Outer
visible={tooltipVisible}
visible={showTooltip && !disableHint}
onVisibleChange={onDismissTooltip}
position="bottom">
<Tooltip.Target>
@@ -81,7 +79,7 @@ export function SubscribeProfileButton({
accessibilityRole="button"
testID="dmBtn"
size="small"
color={tooltipVisible ? 'primary_subtle' : 'secondary'}
color="secondary"
shape="round"
label={_(msg`Get notified when ${name} posts`)}
onPress={wrappedOnPress}>
@@ -1,209 +0,0 @@
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 (
<Dialog.Outer
control={control}
onClose={onClose}
nativeOptions={{preventExpansion: true}}>
<Dialog.Handle fill={t.palette.primary_700} />
<Dialog.ScrollableInner
label={_(msg`Show when youre live`)}
style={[web({maxWidth: 440})]}
contentContainerStyle={[
{
paddingTop: 0,
paddingLeft: 0,
paddingRight: 0,
},
]}>
<View
style={[
a.align_center,
a.overflow_hidden,
{
gap: 16,
paddingTop: isWeb ? 24 : 40,
borderTopLeftRadius: a.rounded_md.borderRadius,
borderTopRightRadius: a.rounded_md.borderRadius,
},
]}>
<LinearGradient
colors={[
t.palette.primary_100,
utils.alpha(t.palette.primary_100, 0),
]}
locations={[0, 1]}
start={{x: 0, y: 0}}
end={{x: 0, y: 1}}
style={[a.absolute, a.inset_0]}
/>
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
<BeakerIcon fill={t.palette.primary_700} size="sm" />
<Text
style={[
a.font_semi_bold,
{
color: t.palette.primary_700,
},
]}>
<Trans>Beta Feature</Trans>
</Text>
</View>
<View
style={[
a.relative,
a.w_full,
{
paddingTop: 8,
paddingHorizontal: 32,
paddingBottom: 32,
},
]}>
<View
style={[
{
borderRadius: 24,
aspectRatio: 652 / 211,
},
isWeb
? [
{
boxShadow: `0px 10px 15px -3px ${shadowColor}`,
},
]
: [
t.atoms.shadow_md,
{
shadowColor,
shadowOpacity: 0.2,
shadowOffset: {
width: 0,
height: 10,
},
},
],
]}>
<Image
accessibilityIgnoresInvertColors
source={require('../../../../assets/images/live_now_beta.webp')}
style={[
a.w_full,
{
aspectRatio: 652 / 211,
},
]}
alt={_(
msg({
message: `A screenshot of a post from @esb.lol, showing the user is currently livestreaming content on Twitch. The post reads: "Hello! I'm live on Twitch, and I'm testing Bluesky's latest feature too!"`,
comment:
'Contains a post that originally appeared in English. Consider translating the post text if it makes sense in your language, and noting that the post was translated from English.',
}),
)}
/>
</View>
</View>
</View>
<View style={[a.align_center, a.px_xl, a.gap_2xl, a.pb_sm]}>
<View style={[a.gap_sm, a.align_center]}>
<Text
style={[
a.text_3xl,
a.leading_tight,
a.font_bold,
a.text_center,
{
fontSize: isWeb ? 28 : 32,
maxWidth: 360,
},
]}>
<Trans>Show when youre live</Trans>
</Text>
<Text
style={[
a.text_md,
a.leading_snug,
a.text_center,
{
maxWidth: 340,
},
]}>
<Trans>
Streaming on Twitch? Set your live status on Bluesky to add a
badge to your avatar. Tapping it takes people straight to your
stream.
</Trans>
</Text>
</View>
{!isWeb && (
<Button
label={_(msg`Close`)}
size="large"
color="primary"
onPress={() => {
control.close()
}}
style={[a.w_full]}>
<ButtonText>
<Trans>Close</Trans>
</ButtonText>
</Button>
)}
</View>
<Dialog.Close />
</Dialog.ScrollableInner>
</Dialog.Outer>
)
}
+8 -6
View File
@@ -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 isLiveNowBetaDialogEnabled,
LiveNowBetaDialog,
} from '#/components/dialogs/nuxs/LiveNowBetaDialog'
enabled as isFindContactsAnnouncementEnabled,
FindContactsAnnouncement,
} from '#/components/dialogs/nuxs/FindContactsAnnouncement'
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.LiveNowBetaDialog,
enabled: isLiveNowBetaDialogEnabled,
id: Nux.FindContactsAnnouncement,
enabled: isFindContactsAnnouncementEnabled,
},
]
@@ -186,7 +186,9 @@ function Inner({
return (
<Context.Provider value={ctx}>
{/*For example, activeNux === Nux.NeueTypography && <NeueTypography />*/}
{activeNux === Nux.LiveNowBetaDialog && <LiveNowBetaDialog />}
{activeNux === Nux.FindContactsAnnouncement && (
<FindContactsAnnouncement />
)}
</Context.Provider>
)
}
+1 -1
View File
@@ -41,7 +41,7 @@ export function Inner() {
}, [setTrendingDisabled])
return error || noTopics ? null : (
<View style={[t.atoms.border_contrast_low, a.border_t, a.border_b]}>
<View style={[t.atoms.border_contrast_low, a.border_t]}>
<BlockDrawerGesture>
<ScrollView
horizontal
+21 -7
View File
@@ -18,6 +18,7 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import * as TextField from '#/components/forms/TextField'
import {Clock_Stroke2_Corner0_Rounded as ClockIcon} from '#/components/icons/Clock'
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import {LinkPreview} from './LinkPreview'
@@ -171,13 +172,26 @@ function DialogInner({
</TextField.Root>
</View>
{(liveLinkError || linkMetaError) && (
<Admonition type="error">
{liveLinkError ? (
<Trans>This is not a valid link</Trans>
) : (
cleanError(linkMetaError)
)}
</Admonition>
<View style={[a.flex_row, a.gap_xs, a.align_center]}>
<WarningIcon
style={[{color: t.palette.negative_500}]}
size="sm"
/>
<Text
style={[
a.text_sm,
a.leading_snug,
a.flex_1,
a.font_semi_bold,
{color: t.palette.negative_500},
]}>
{liveLinkError ? (
<Trans>This is not a valid link</Trans>
) : (
cleanError(linkMetaError)
)}
</Text>
</View>
)}
<LinkPreview linkMeta={linkMeta} loading={linkMetaLoading} />
+22 -21
View File
@@ -6,14 +6,13 @@ 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 {getLiveServiceNames} from '#/components/live/utils'
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
import {Loader} from '#/components/Loader'
import * as ProfileCard from '#/components/ProfileCard'
import * as Select from '#/components/Select'
@@ -50,10 +49,6 @@ 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) => {
@@ -144,21 +139,27 @@ function DialogInner({profile}: {profile: bsky.profile.AnyProfileView}) {
/>
</TextField.Root>
</View>
{liveLinkError || linkMetaError ? (
<Admonition type="error">
{liveLinkError ? (
<Trans>This is not a valid link</Trans>
) : (
cleanError(linkMetaError)
)}
</Admonition>
) : (
<Admonition type="tip">
<Trans>
The following services are enabled for your account:{' '}
{allowedServices}
</Trans>
</Admonition>
{(liveLinkError || linkMetaError) && (
<View style={[a.flex_row, a.gap_xs, a.align_center]}>
<WarningIcon
style={[{color: t.palette.negative_500}]}
size="sm"
/>
<Text
style={[
a.text_sm,
a.leading_snug,
a.flex_1,
a.font_semi_bold,
{color: t.palette.negative_500},
]}>
{liveLinkError ? (
<Trans>This is not a valid link</Trans>
) : (
cleanError(linkMetaError)
)}
</Text>
</View>
)}
<LinkPreview linkMeta={linkMeta} loading={linkMetaLoading} />
+7 -8
View File
@@ -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,14 +30,13 @@ 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 (!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}.`,
),
)
if (!config.domains.includes(urlp.hostname)) {
throw new Error(_(msg`${urlp.hostname} is not a valid URL`))
}
return await getLinkMeta(agent, url)
-25
View File
@@ -35,28 +35,3 @@ export function useDebouncedValue<T>(val: T, delayMs: number): T {
return prev
}
const serviceUrlToNameMap: Record<string, string> = {
'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<string>) {
const names = Array.from(
new Set(Array.from(domains.values()).map(d => serviceUrlToNameMap[d] || d)),
)
return {
names,
formatted: names.join(', '),
}
}
+5 -15
View File
@@ -108,18 +108,8 @@ 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 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
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
@@ -1,89 +0,0 @@
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 <TrendingInterstitial />
} else {
// no feed, no trending
return null
}
}
// On desktop, we show in the sidebar
if (rightNavVisible) return null
return events.feeds.map(feed => <Inner feed={feed} key={feed.id} />)
}
function Inner({feed}: {feed: LiveEventFeed}) {
const {_} = useLingui()
const optionsMenuControl = useDialogControl()
const layout = feed.layouts.wide
return (
<>
<View style={[a.px_lg, a.pt_md, a.pb_xs]}>
<View>
<LiveEventFeedCardWide feed={feed} metricContext="discover" />
<Button
label={_(msg`Configure live event banner`)}
size="tiny"
shape="round"
style={[a.absolute, a.z_10, {top: 6, right: 6}]}
onPress={() => {
optionsMenuControl.open()
}}>
{({hovered, pressed}) => (
<>
<View
style={[
a.absolute,
a.inset_0,
a.rounded_full,
{
backgroundColor: layout.overlayColor,
opacity: hovered || pressed ? 0.8 : 0.6,
},
]}
/>
<EllipsisIcon
size="sm"
fill={layout.textColor}
style={[a.z_20]}
/>
</>
)}
</Button>
</View>
</View>
<LiveEventFeedOptionsMenu
feed={feed}
control={optionsMenuControl}
metricContext="discover"
/>
</>
)
}
@@ -1,17 +0,0 @@
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 => (
<View
key={feed.id}
style={[a.p_lg, a.border_b, t.atoms.border_contrast_low]}>
<LiveEventFeedCardWide feed={feed} metricContext="explore" />
</View>
))
}
@@ -1,132 +0,0 @@
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 (
<Link
to={url}
label={_(msg`Live event happening now: ${feed.title}`)}
style={[a.w_full]}
onPress={() => {
logger.metric('liveEvents:feedBanner:click', {
feed: feed.url,
context: metricContext,
})
}}>
{({hovered, pressed}) => (
<View style={[roundedStyles, a.shadow_md, a.w_full]}>
<View
style={[a.w_full, a.align_start, a.overflow_hidden, roundedStyles]}>
<Image
accessibilityIgnoresInvertColors
source={{uri: layout.image}}
placeholder={{blurhash: layout.blurhash}}
style={[a.absolute, a.inset_0, a.w_full, a.h_full]}
contentFit="cover"
placeholderContentFit="cover"
/>
<LinearGradient
colors={[overlayColor, utils.alpha(overlayColor, 0)]}
locations={[0, 1]}
start={{x: 0, y: 0}}
end={{x: 1, y: 0}}
style={[
a.absolute,
a.inset_0,
a.transition_opacity,
{
transitionDuration: '200ms',
opacity: hovered || pressed ? 0.6 : 0,
},
]}
/>
<View style={[a.w_full, a.justify_end]}>
<LinearGradient
colors={[
overlayColor,
utils.alpha(overlayColor, 0.7),
utils.alpha(overlayColor, 0),
]}
locations={[0, 0.8, 1]}
start={{x: 0, y: 0}}
end={{x: 1, y: 0}}
style={[a.absolute, a.inset_0]}
/>
<View
style={[
a.flex_1,
a.flex_row,
a.align_center,
a.gap_xs,
a.z_10,
a.px_lg,
a.py_md,
]}>
<LiveIcon size="md" fill={textColor} />
<Text
numberOfLines={1}
style={[
a.flex_1,
a.leading_snug,
a.font_bold,
a.text_lg,
a.pr_xl,
{color: textColor},
]}>
{layout.title}
</Text>
</View>
</View>
</View>
</View>
)}
</Link>
)
}
@@ -1,139 +0,0 @@
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 (
<Link
to={url}
label={_(msg`Live event happening now: ${feed.title}`)}
style={[a.w_full]}
onPress={() => {
logger.metric('liveEvents:feedBanner:click', {
feed: feed.url,
context: metricContext,
})
}}>
{({hovered, pressed}) => (
<View style={[roundedStyles, a.shadow_md, a.w_full]}>
<View
style={[
a.align_start,
roundedStyles,
a.overflow_hidden,
{
aspectRatio: gtPhone ? 576 / 144 : 369 / 100,
},
]}>
<Image
accessibilityIgnoresInvertColors
source={{uri: layout.image}}
placeholder={{blurhash: layout.blurhash}}
style={[a.absolute, a.inset_0, a.w_full, a.h_full]}
contentFit="cover"
placeholderContentFit="cover"
/>
<LinearGradient
colors={[overlayColor, utils.alpha(overlayColor, 0)]}
locations={[0, 1]}
start={{x: 0, y: 0}}
end={{x: 1, y: 0}}
style={[
a.absolute,
a.inset_0,
a.transition_opacity,
{
transitionDuration: '200ms',
opacity: hovered || pressed ? 0.6 : 0,
},
]}
/>
<View style={[a.flex_1, a.justify_end]}>
<LinearGradient
colors={[overlayColor, utils.alpha(overlayColor, 0)]}
locations={[0, 1]}
start={{x: 0, y: 0}}
end={{x: 1, y: 0}}
style={[a.absolute, a.inset_0]}
/>
<View
style={[
a.z_10,
gtPhone ? [a.pl_xl, a.pb_lg] : [a.pl_lg, a.pb_md],
{paddingRight: 64},
]}>
<Text
style={[
a.leading_snug,
gtPhone ? a.text_xs : a.text_2xs,
{color: textColor, opacity: 0.8},
]}>
{feed.preview ? (
<Trans>Preview</Trans>
) : (
<Trans>Happening now</Trans>
)}
</Text>
<Text
style={[
a.leading_snug,
a.font_bold,
gtPhone ? a.text_3xl : a.text_lg,
{color: textColor},
]}>
{layout.title}
</Text>
</View>
</View>
</View>
</View>
)}
</Link>
)
}
@@ -1,171 +0,0 @@
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 (
<Dialog.Outer control={control}>
<Dialog.Handle />
<Dialog.ScrollableInner
label={_(msg`Configure live event banner`)}
style={[web({maxWidth: 400})]}>
<Inner control={control} feed={feed} metricContext={metricContext} />
<Dialog.Close />
</Dialog.ScrollableInner>
</Dialog.Outer>
)
}
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(
<toast.Outer>
<toast.Icon />
<toast.Text>
<Trans>Your live event preferences have been updated.</Trans>
</toast.Text>
{undoAction && (
<toast.Action
label={_(msg`Undo`)}
onPress={() => {
if (undoAction) {
update(undoAction)
}
}}>
<Trans>Undo</Trans>
</toast.Action>
)}
</toast.Outer>,
{
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 (
<View style={[a.gap_lg]}>
<View style={[a.gap_sm]}>
<Text style={[a.text_2xl, a.font_semi_bold, a.leading_snug]}>
<Trans>Live event options</Trans>
</Text>
<Text style={[a.text_md, a.leading_snug]}>
<Trans>
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.
</Trans>
</Text>
<Text style={[a.text_md, a.leading_snug]}>
<Trans>
If you choose to hide all events, you can always re-enable them from{' '}
<Span style={[a.font_semi_bold]}>Settings → Content & Media</Span>.
</Trans>
</Text>
</View>
<View style={[a.gap_sm]}>
<Button
label={_(msg`Hide this event`)}
size="large"
color="primary_subtle"
onPress={() => {
update({type: 'hideFeed', id: feed.id})
}}>
<ButtonText>
<Trans>Hide this event</Trans>
</ButtonText>
{isHidingFeed && <ButtonIcon icon={Loader} />}
</Button>
<Button
label={_(msg`Hide all events`)}
size="large"
color="secondary"
onPress={() => {
update({type: 'toggleHideAllFeeds'})
}}>
<ButtonText>
<Trans>Hide all events</Trans>
</ButtonText>
{isHidingAllFeeds && <ButtonIcon icon={Loader} />}
</Button>
{isNative && (
<Button
label={_(msg`Cancel`)}
size="large"
color="secondary_inverted"
onPress={() => control.close()}>
<ButtonText>
<Trans>Cancel</Trans>
</ButtonText>
</Button>
)}
</View>
{error && (
<Admonition type="error">
{error.clean || error.raw || _(msg`An unknown error occurred.`)}
</Admonition>
)}
</View>
)
}
@@ -1,43 +0,0 @@
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 (
<Toggle.Item
name="enable_live_event_banner"
label={_(msg`Show live events in your Discover Feed`)}
value={!hideAllFeeds}
onChange={() => {
if (!isPending) {
update({type: 'toggleHideAllFeeds'})
}
}}>
<SettingsList.Item>
<SettingsList.ItemIcon icon={LiveIcon} />
<SettingsList.ItemText>
<Trans>Show live events in your Discover Feed</Trans>
</SettingsList.ItemText>
<Toggle.Platform />
</SettingsList.Item>
</Toggle.Item>
)
}
@@ -1,13 +0,0 @@
import {LiveEventFeedCardCompact} from '#/features/liveEvents/components/LiveEventFeedCardCompact'
import {useLiveEvents} from '#/features/liveEvents/context'
export function SidebarLiveEventFeedsBanner() {
const events = useLiveEvents()
return events.feeds.map(feed => (
<LiveEventFeedCardCompact
key={feed.id}
feed={feed}
metricContext="sidebar"
/>
))
}
-110
View File
@@ -1,110 +0,0 @@
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<LiveEventsWorkerResponse | null> {
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<LiveEventsWorkerResponse>(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 <Context.Provider value={ctx}>{children}</Context.Provider>
}
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()
}),
)
}
-161
View File
@@ -1,161 +0,0 @@
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!
},
})
}
-27
View File
@@ -1,27 +0,0 @@
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<LiveEventFeedImageLayout, LiveEventFeedLayout>
}
export type LiveEventsWorkerResponse = {
feeds: LiveEventFeed[]
}
export type LiveEventFeedMetricContext =
| 'explore'
| 'discover'
| 'sidebar'
| 'settings'
-31
View File
@@ -1,31 +0,0 @@
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<ViewStyle, 'top' | 'bottom' | 'left' | 'right'>) {
const t = useTheme()
return (
<View style={[a.absolute, {top, bottom, left, right}]}>
<View
style={[
a.rounded_full,
{
height: 8,
width: 8,
backgroundColor: t.palette.primary_500,
},
]}
/>
</View>
)
}
-24
View File
@@ -1,24 +0,0 @@
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 (
<LinearGradient
colors={[
utils.alpha(t.palette.primary_500, 0.2),
utils.alpha(t.palette.primary_500, 0.1),
]}
locations={[0, 1]}
start={{x: 0, y: 0}}
end={{x: 1, y: 0}}
style={[a.absolute, a.inset_0, style]}
/>
)
}
+2 -2
View File
@@ -1,7 +1,7 @@
import {GEOLOCATION_URL} from '#/env'
import {BAPP_CONFIG_URL} from '#/env'
import {type Geolocation} from '#/geolocation/types'
export const GEOLOCATION_SERVICE_URL = `${GEOLOCATION_URL}/geolocation`
export const GEOLOCATION_SERVICE_URL = `${BAPP_CONFIG_URL}/geolocation`
/**
* Default geolocation config.
+9 -4
View File
@@ -7,7 +7,7 @@ import {
import {isAfter, parseISO} from 'date-fns'
import {useMaybeProfileShadow} from '#/state/cache/profile-shadow'
import {type LiveNowConfig, useLiveNowConfig} from '#/state/service-config'
import {useLiveNowConfig} from '#/state/service-config'
import {useTickEveryMinute} from '#/state/shell'
import type * as bsky from '#/types/bsky'
@@ -20,7 +20,7 @@ export function useActorStatus(actor?: bsky.profile.AnyProfileView) {
tick! // revalidate every minute
if (shadowed && 'status' in shadowed && shadowed.status) {
const isValid = validateStatus(shadowed.status, config)
const isValid = validateStatus(shadowed.did, shadowed.status, config)
const isDisabled = shadowed.status.isDisabled || false
const isActive = isStatusStillActive(shadowed.status.expiresAt)
if (isValid && !isDisabled && isActive) {
@@ -65,14 +65,19 @@ export function isStatusStillActive(timeStr: string | undefined) {
}
export function validateStatus(
did: string,
status: AppBskyActorDefs.StatusView,
config: LiveNowConfig,
config: {did: string; domains: string[]}[],
) {
if (status.status !== 'app.bsky.actor.status#live') return false
const sources = config.find(cfg => cfg.did === did)
if (!sources) {
return false
}
try {
if (AppBskyEmbedExternal.isView(status.embed)) {
const url = new URL(status.embed.external.uri)
return config.allowedDomains.has(url.hostname)
return sources.domains.includes(url.hostname)
} else {
return false
}
-4
View File
@@ -241,10 +241,6 @@ export const BLUESKY_MOD_SERVICE_HEADERS = {
'atproto-proxy': `${BSKY_LABELER_DID}#atproto_labeler`,
}
export const BLUESKY_NOTIF_SERVICE_HEADERS = {
'atproto-proxy': `${BLUESKY_PROXY_DID}#bsky_notif`,
}
export const webLinks = {
tos: `https://bsky.social/about/support/tos`,
privacy: `https://bsky.social/about/support/privacy-policy`,
-8
View File
@@ -1,8 +0,0 @@
import {useMemo} from 'react'
import {useGate} from '#/lib/statsig/statsig'
export function useIsBskyTeam() {
const gate = useGate()
return useMemo(() => gate('is_bsky_team_member'), [gate])
}
+3 -40
View File
@@ -2,15 +2,10 @@ import {useCallback, useEffect} from 'react'
import {Platform} from 'react-native'
import * as Notifications from 'expo-notifications'
import {getBadgeCountAsync, setBadgeCountAsync} from 'expo-notifications'
import {type AtpAgent} from '@atproto/api'
import {type AppBskyNotificationRegisterPush} from '@atproto/api'
import {type AppBskyNotificationRegisterPush, type AtpAgent} from '@atproto/api'
import debounce from 'lodash.debounce'
import {
BLUESKY_NOTIF_SERVICE_HEADERS,
PUBLIC_APPVIEW_DID,
PUBLIC_STAGING_APPVIEW_DID,
} from '#/lib/constants'
import {PUBLIC_APPVIEW_DID, PUBLIC_STAGING_APPVIEW_DID} from '#/lib/constants'
import {logger as notyLogger} from '#/lib/notifications/util'
import {isNetworkError} from '#/lib/strings/errors'
import {isNative} from '#/platform/detection'
@@ -49,9 +44,7 @@ async function _registerPushToken({
notyLogger.debug(`registerPushToken: registering`, {...payload})
await agent.app.bsky.notification.registerPush(payload, {
headers: BLUESKY_NOTIF_SERVICE_HEADERS,
})
await agent.app.bsky.notification.registerPush(payload)
notyLogger.debug(`registerPushToken: success`)
} catch (error) {
@@ -293,33 +286,3 @@ export async function resetBadgeCount() {
await BackgroundNotificationHandler.setBadgeCountAsync(0)
await setBadgeCountAsync(0)
}
export async function unregisterPushToken(agents: AtpAgent[]) {
if (!isNative) return
try {
const token = await getPushToken()
if (token) {
for (const agent of agents) {
await agent.app.bsky.notification.unregisterPush(
{
serviceDid: agent.serviceUrl.hostname.includes('staging')
? PUBLIC_STAGING_APPVIEW_DID
: PUBLIC_APPVIEW_DID,
platform: Platform.OS,
token: token.data,
appId: 'xyz.blueskyweb.app',
},
{
headers: BLUESKY_NOTIF_SERVICE_HEADERS,
},
)
notyLogger.debug(`Push token unregistered for ${agent.session?.handle}`)
}
} else {
notyLogger.debug('Tried to unregister push token, but could not find one')
}
} catch (error) {
notyLogger.debug('Failed to unregister push token', {message: error})
}
}
+2 -13
View File
@@ -1,4 +1,4 @@
import {useEffect, useRef, useState} from 'react'
import {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,15 +9,9 @@ import {
} from '@tanstack/react-query-persist-client'
import type React from 'react'
import {isNative, isWeb} from '#/platform/detection'
import {isNative} 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]
@@ -186,11 +180,6 @@ function QueryProviderInner({
dehydrateOptions,
}
})
useEffect(() => {
if (isWeb) {
window.__TANSTACK_QUERY_CLIENT__ = queryClient
}
}, [queryClient])
return (
<PersistQueryClientProvider
client={queryClient}
-2
View File
@@ -7,8 +7,6 @@ export type Gate =
| 'disable_settings_find_contacts'
| 'explore_show_suggested_feeds'
| 'feed_reply_button_open_thread'
| 'is_bsky_team_member' // special, do not remove
| 'live_now_beta'
| 'old_postonboarding'
| 'onboarding_add_video_feed'
| 'onboarding_suggested_starterpacks'
File diff suppressed because it is too large Load Diff
-2
View File
@@ -16,8 +16,6 @@ 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': {
-32
View File
@@ -1,6 +1,5 @@
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
@@ -245,8 +244,6 @@ export type MetricEvents = {
isReply: boolean
}
'post:like': {
uri: string
authorDid: string
doesLikerFollowPoster: boolean | undefined
doesPosterFollowLiker: boolean | undefined
likerClout: number | undefined
@@ -255,20 +252,14 @@ 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
}
@@ -797,27 +788,4 @@ 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
}
}
+8 -21
View File
@@ -46,22 +46,13 @@ export default function HashtagScreen({
const {tag, author} = route.params
const {_} = useLingui()
const decodedTag = React.useMemo(() => {
return decodeURIComponent(tag)
const fullTag = React.useMemo(() => {
return `#${decodeURIComponent(tag)}`
}, [tag])
const isCashtag = decodedTag.startsWith('$')
const fullTag = React.useMemo(() => {
// Cashtags already include the $ prefix, hashtags need # added
return isCashtag ? decodedTag : `#${decodedTag}`
}, [decodedTag, isCashtag])
const headerTitle = React.useMemo(() => {
// Keep cashtags uppercase, lowercase hashtags
const displayTag = isCashtag ? fullTag.toUpperCase() : fullTag.toLowerCase()
return enforceLen(displayTag, 24, true, 'middle')
}, [fullTag, isCashtag])
return enforceLen(fullTag.toLowerCase(), 24, true, 'middle')
}, [fullTag])
const sanitizedAuthor = React.useMemo(() => {
if (!author) return
@@ -181,14 +172,10 @@ function HashtagScreenTab({
const {hasSession} = useSession()
const trackPostView = usePostViewTracking('Hashtag')
const isCashtag = fullTag.startsWith('$')
const queryParam = React.useMemo(() => {
// Cashtags need # prefix for search: "#$BTC" or "#$BTC from:author"
const searchTag = isCashtag ? `#${fullTag}` : fullTag
if (!author) return searchTag
return `${searchTag} from:${author}`
}, [fullTag, author, isCashtag])
if (!author) return fullTag
return `${fullTag} from:${author}`
}, [fullTag, author])
const {
data,
@@ -268,7 +255,7 @@ function HashtagScreenTab({
isError={isError}
onRetry={refetch}
emptyType="results"
emptyMessage={_(msg`We couldn't find any results for that tag.`)}
emptyMessage={_(msg`We couldn't find any results for that hashtag.`)}
/>
) : (
<List
@@ -1,65 +0,0 @@
import {useState} from 'react'
import {View} from 'react-native'
import {PrivacySensitive} from 'expo-privacy-sensitive'
import {useAppState} from '#/lib/hooks/useAppState'
import {isIOS} from '#/platform/detection'
import {atoms as a, useTheme} from '#/alf'
import {sizes as iconSizes} from '#/components/icons/common'
import {Mark as Logo} from '#/components/icons/Logo'
const ICON_SIZE = 'xl' as const
export function GrowthHack({
children,
align = 'right',
}: {
children: React.ReactNode
align?: 'left' | 'right'
}) {
const t = useTheme()
// the button has a variable width and is absolutely positioned, so we need to manually
// set the minimum width of the underlying button
const [width, setWidth] = useState<number | undefined>(undefined)
const appState = useAppState()
if (!isIOS || appState !== 'active') return children
return (
<View
style={[
a.relative,
a.justify_center,
align === 'right' ? a.align_end : a.align_start,
width === undefined ? {opacity: 0} : {minWidth: width},
]}>
<PrivacySensitive
style={[
a.absolute,
a.z_10,
a.flex_col,
align === 'right'
? [a.right_0, a.align_end]
: [a.left_0, a.align_start],
// when finding the size of the button, we need the containing
// element to have a concrete size otherwise the text will
// collapse to 0 width. so set it to a really big number
// and hide the entire thing (see above)
width === undefined && {width: 10000},
]}>
<View
onLayout={evt => setWidth(evt.nativeEvent.layout.width)}
style={[
t.atoms.bg,
// make sure it covers the icon! the won't always be a button
{minWidth: iconSizes[ICON_SIZE], minHeight: iconSizes[ICON_SIZE]},
]}>
{children}
</View>
</PrivacySensitive>
<Logo size={ICON_SIZE} />
</View>
)
}
@@ -381,7 +381,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
</View>
</Link>
{showFollowButton && (
<View collapsable={false} style={[a.self_center]}>
<View collapsable={false}>
<ThreadItemAnchorFollowButton did={post.author.did} />
</View>
)}
@@ -5,7 +5,6 @@ import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {logger} from '#/logger'
import {isIOS} from '#/platform/detection'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {
useProfileFollowMutationQueue,
@@ -15,23 +14,10 @@ import {useRequireAuth} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useBreakpoints} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
import {GrowthHack} from './GrowthHack'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
export function ThreadItemAnchorFollowButton({did}: {did: string}) {
if (isIOS) {
return (
<GrowthHack>
<ThreadItemAnchorFollowButtonInner did={did} />
</GrowthHack>
)
}
return <ThreadItemAnchorFollowButtonInner did={did} />
}
export function ThreadItemAnchorFollowButtonInner({did}: {did: string}) {
const {data: profile, isLoading} = useProfileQuery({did})
// We will never hit this - the profile will always be cached or loaded above
@@ -127,10 +113,15 @@ function PostThreadFollowBtnLoaded({
label={_(msg`Follow ${profile.handle}`)}
onPress={onPress}
size="small"
variant="solid"
color={isFollowing ? 'secondary' : 'secondary_inverted'}
style={[a.rounded_full]}>
{gtMobile && (
<ButtonIcon icon={isFollowing ? CheckIcon : PlusIcon} size="sm" />
<ButtonIcon
icon={isFollowing ? Check : Plus}
position="left"
size="sm"
/>
)}
<ButtonText>
{!isFollowing ? (
-10
View File
@@ -68,7 +68,6 @@ 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,
@@ -202,10 +201,6 @@ type ExploreScreenItems =
type: 'interests-card'
key: 'interests-card'
}
| {
type: 'liveEventFeedsBanner'
key: string
}
export function Explore({
focusSearchInput,
@@ -689,8 +684,6 @@ export function Explore({
i.push(topBorder)
i.push(...interestsNuxModule)
i.push({type: 'liveEventFeedsBanner', key: 'liveEventFeedsBanner'})
if (useFullExperience) {
i.push(trendingTopicsModule)
i.push(...suggestedFeedsModule)
@@ -1005,9 +998,6 @@ export function Explore({
case 'interests-card': {
return <ExploreInterestsCard />
}
case 'liveEventFeedsBanner': {
return <ExploreScreenLiveEventFeedsBanner />
}
}
},
[
@@ -26,7 +26,6 @@ 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,
@@ -125,7 +124,7 @@ export function ContentAndMediaSettingsScreen({}: Props) {
<Toggle.Platform />
</SettingsList.Item>
</Toggle.Item>
{trendingEnabled ? (
{trendingEnabled && (
<>
<SettingsList.Divider />
<Toggle.Item
@@ -149,7 +148,6 @@ export function ContentAndMediaSettingsScreen({}: Props) {
<Toggle.Platform />
</SettingsList.Item>
</Toggle.Item>
<LiveEventFeedsSettingsToggle />
<Toggle.Item
name="show_trending_videos"
label={_(msg`Enable trending videos in your Discover feed`)}
@@ -172,11 +170,6 @@ export function ContentAndMediaSettingsScreen({}: Props) {
</SettingsList.Item>
</Toggle.Item>
</>
) : (
<>
<SettingsList.Divider />
<LiveEventFeedsSettingsToggle />
</>
)}
</SettingsList.Container>
</Layout.Content>
-1
View File
@@ -116,7 +116,6 @@ const schema = z.object({
}),
hiddenPosts: z.array(z.string()).optional(), // should move to server
useInAppBrowser: z.boolean().optional(),
/** @deprecated */
lastSelectedHomeFeed: z.string().optional(),
pdsAddressHistory: z.array(z.string()).optional(),
disableHaptics: z.boolean().optional(),
-12
View File
@@ -12,8 +12,6 @@ export enum Nux {
BookmarksAnnouncement = 'BookmarksAnnouncement',
FindContactsAnnouncement = 'FindContactsAnnouncement',
FindContactsDismissibleBanner = 'FindContactsDismissibleBanner',
LiveNowBetaDialog = 'LiveNowBetaDialog',
LiveNowBetaNudge = 'LiveNowBetaNudge',
/*
* Blocking announcements. New IDs are required for each new announcement.
@@ -64,14 +62,6 @@ export type AppNux = BaseNux<
id: Nux.FindContactsDismissibleBanner
data: undefined
}
| {
id: Nux.LiveNowBetaDialog
data: undefined
}
| {
id: Nux.LiveNowBetaNudge
data: undefined
}
>
export const NuxSchemas: Record<Nux, zod.ZodObject<any> | undefined> = {
@@ -85,6 +75,4 @@ export const NuxSchemas: Record<Nux, zod.ZodObject<any> | undefined> = {
[Nux.BookmarksAnnouncement]: undefined,
[Nux.FindContactsAnnouncement]: undefined,
[Nux.FindContactsDismissibleBanner]: undefined,
[Nux.LiveNowBetaDialog]: undefined,
[Nux.LiveNowBetaNudge]: undefined,
}
+8 -32
View File
@@ -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, post)
const unlikeMutation = usePostUnlikeMutation(feedDescriptor, logContext)
const queueToggle = useToggleMutationQueue({
initialState: initialLikeUri,
@@ -182,8 +182,6 @@ function usePostLikeMutation(
ownProfile = findProfileQueryData(queryClient, currentAccount.did)
}
logger.metric('post:like', {
uri,
authorDid: postAuthor.did,
logContext,
doesPosterFollowLiker: postAuthor.viewer
? Boolean(postAuthor.viewer.followedBy)
@@ -208,17 +206,11 @@ function usePostLikeMutation(
function usePostUnlikeMutation(
feedDescriptor: string | undefined,
logContext: LogEvents['post:unlike']['logContext'],
post: Shadow<AppBskyFeedDefs.PostView>,
) {
const agent = useAgent()
return useMutation<void, Error, {postUri: string; likeUri: string}>({
mutationFn: ({postUri, likeUri}) => {
logger.metric('post:unlike', {
uri: postUri,
authorDid: post.author.did,
logContext,
feedDescriptor,
})
mutationFn: ({likeUri}) => {
logger.metric('post:unlike', {logContext, feedDescriptor})
return agent.deleteLike(likeUri)
},
})
@@ -235,12 +227,8 @@ export function usePostRepostMutationQueue(
const postUri = post.uri
const postCid = post.cid
const initialRepostUri = post.viewer?.repost
const repostMutation = usePostRepostMutation(feedDescriptor, logContext, post)
const unrepostMutation = usePostUnrepostMutation(
feedDescriptor,
logContext,
post,
)
const repostMutation = usePostRepostMutation(feedDescriptor, logContext)
const unrepostMutation = usePostUnrepostMutation(feedDescriptor, logContext)
const queueToggle = useToggleMutationQueue({
initialState: initialRepostUri,
@@ -292,7 +280,6 @@ export function usePostRepostMutationQueue(
function usePostRepostMutation(
feedDescriptor: string | undefined,
logContext: LogEvents['post:repost']['logContext'],
post: Shadow<AppBskyFeedDefs.PostView>,
) {
const agent = useAgent()
return useMutation<
@@ -301,12 +288,7 @@ 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', {
uri,
authorDid: post.author.did,
logContext,
feedDescriptor,
})
logger.metric('post:repost', {logContext, feedDescriptor})
return agent.repost(uri, cid, via)
},
})
@@ -315,17 +297,11 @@ function usePostRepostMutation(
function usePostUnrepostMutation(
feedDescriptor: string | undefined,
logContext: LogEvents['post:unrepost']['logContext'],
post: Shadow<AppBskyFeedDefs.PostView>,
) {
const agent = useAgent()
return useMutation<void, Error, {postUri: string; repostUri: string}>({
mutationFn: ({postUri, repostUri}) => {
logger.metric('post:unrepost', {
uri: postUri,
authorDid: post.author.did,
logContext,
feedDescriptor,
})
mutationFn: ({repostUri}) => {
logger.metric('post:unrepost', {logContext, feedDescriptor})
return agent.deleteRepost(repostUri)
},
})
-4
View File
@@ -45,8 +45,4 @@ export const DEFAULT_LOGGED_OUT_PREFERENCES: UsePreferencesQueryResponse = {
verificationPrefs: {
hideBadges: false,
},
liveEventPreferences: {
hideAllFeeds: false,
hiddenFeedIds: [],
},
}
+10 -22
View File
@@ -1,10 +1,7 @@
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 = {
@@ -21,7 +18,7 @@ const TrendingContext = createContext<TrendingContext>({
})
TrendingContext.displayName = 'TrendingContext'
const LiveNowContext = createContext<LiveNowContext>([])
const LiveNowContext = createContext<LiveNowContext | null>(null)
LiveNowContext.displayName = 'LiveNowContext'
const CheckEmailConfirmedContext = createContext<boolean | null>(null)
@@ -85,28 +82,19 @@ export function useTrendingConfig() {
return useContext(TrendingContext)
}
const DEFAULT_LIVE_ALLOWED_DOMAINS = ['twitch.tv', 'www.twitch.tv']
export type LiveNowConfig = {
allowedDomains: Set<string>
}
export function useLiveNowConfig(): LiveNowConfig {
export function useLiveNowConfig() {
const ctx = useContext(LiveNowContext)
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 : []),
),
if (!ctx) {
throw new Error(
'useLiveNowConfig must be used within a ServiceConfigManager',
)
}
return ctx
}
export function useCanGoLive() {
const gate = useGate()
const {hasSession} = useSession()
if (!hasSession) return false
return IS_DEV ? true : gate('live_now_beta')
export function useCanGoLive(did?: string) {
const config = useLiveNowConfig()
return !!config.find(cfg => cfg.did === did)
}
export function useCheckEmailConfirmed() {
@@ -12,11 +12,6 @@ jest.mock('jwt-decode', () => ({
jest.mock('../../birthdate')
jest.mock('../../../ageAssurance/data')
jest.mock('#/lib/notifications/notifications', () => ({
unregisterPushToken(_agents: BskyAgent[]) {
return Promise.resolve()
},
}))
describe('session', () => {
it('can log in and out', () => {
+3 -49
View File
@@ -1,11 +1,8 @@
import {type AtpAgent, type AtpSessionEvent} from '@atproto/api'
import {type AtpSessionEvent, type BskyAgent} from '@atproto/api'
import {unregisterPushToken} from '#/lib/notifications/notifications'
import {logger} from '#/lib/notifications/util'
import {createPublicAgent} from './agent'
import {wrapSessionReducerForLogging} from './logging'
import {type SessionAccount} from './types'
import {createTemporaryAgentsAndResume} from './util'
// A hack so that the reducer can't read anything from the agent.
// From the reducer's point of view, it should be a completely opaque object.
@@ -140,23 +137,6 @@ let reducer = (state: State, action: Action): State => {
}
case 'removed-account': {
const {accountDid} = action
// side effect
const account = state.accounts.find(a => a.did === accountDid)
if (account) {
createTemporaryAgentsAndResume([account])
.then(agents => unregisterPushToken(agents))
.then(() =>
logger.debug('Push token unregistered', {did: accountDid}),
)
.catch(err => {
logger.error('Failed to unregister push token', {
did: accountDid,
error: err,
})
})
}
return {
accounts: state.accounts.filter(a => a.did !== accountDid),
currentAgentState:
@@ -168,26 +148,9 @@ let reducer = (state: State, action: Action): State => {
}
case 'logged-out-current-account': {
const {currentAgentState} = state
const accountDid = currentAgentState.did
// side effect
const account = state.accounts.find(a => a.did === accountDid)
if (account && accountDid) {
createTemporaryAgentsAndResume([account])
.then(agents => unregisterPushToken(agents))
.then(() =>
logger.debug('Push token unregistered', {did: accountDid}),
)
.catch(err => {
logger.error('Failed to unregister push token', {
did: accountDid,
error: err,
})
})
}
return {
accounts: state.accounts.map(a =>
a.did === accountDid
a.did === currentAgentState.did
? {
...a,
refreshJwt: undefined,
@@ -200,15 +163,6 @@ let reducer = (state: State, action: Action): State => {
}
}
case 'logged-out-every-account': {
createTemporaryAgentsAndResume(state.accounts)
.then(agents => unregisterPushToken(agents))
.then(() => logger.debug('Push token unregistered'))
.catch(err => {
logger.error('Failed to unregister push token', {
error: err,
})
})
return {
accounts: state.accounts.map(a => ({
...a,
@@ -233,7 +187,7 @@ let reducer = (state: State, action: Action): State => {
}
case 'partial-refresh-session': {
const {accountDid, patch} = action
const agent = state.currentAgentState.agent as AtpAgent
const agent = state.currentAgentState.agent as BskyAgent
/*
* Only mutating values that are safe. Be very careful with this.
-31
View File
@@ -1,10 +1,8 @@
import AtpAgent from '@atproto/api'
import {jwtDecode} from 'jwt-decode'
import {isJwtExpired} from '#/lib/jwt'
import {hasProp} from '#/lib/type-guards'
import * as persisted from '#/state/persisted'
import {sessionAccountToSession} from './agent'
import {type SessionAccount} from './types'
export function readLastActiveAccount() {
@@ -30,32 +28,3 @@ export function isSessionExpired(account: SessionAccount) {
return true
}
}
/**
* Creates and attempted to resumeSession for every stored session.
* Intended to be used to send push token revokations just before logout.
*/
export async function createTemporaryAgentsAndResume(
accounts: SessionAccount[],
) {
const agents = await Promise.allSettled(
accounts.map(async account => {
const agent: AtpAgent = new AtpAgent({service: account.service})
if (account.pdsUrl) {
agent.sessionManager.pdsUrl = new URL(account.pdsUrl)
}
const session = sessionAccountToSession(account)
const res = await agent.resumeSession(session)
if (!res.success) throw new Error('Failed to resume session')
agent.assertAuthenticated() // confirm auth success
return agent
}),
)
return agents
.filter(x => x.status === 'fulfilled')
.map(promise => promise.value)
}
+21 -30
View File
@@ -1,19 +1,18 @@
import {createContext, useCallback, useContext, useState} from 'react'
import React from 'react'
import {isWeb} from '#/platform/detection'
import * as persisted from '#/state/persisted'
import {type FeedDescriptor} from '#/state/queries/post-feed'
import {useSession} from '#/state/session'
import {account} from '#/storage'
type StateContext = FeedDescriptor | null
type SetContext = (v: FeedDescriptor) => void
const stateContext = createContext<StateContext>(null)
const stateContext = React.createContext<StateContext>(null)
stateContext.displayName = 'SelectedFeedStateContext'
const setContext = createContext<SetContext>((_: string) => {})
const setContext = React.createContext<SetContext>((_: string) => {})
setContext.displayName = 'SelectedFeedSetContext'
function getInitialFeed(did?: string): FeedDescriptor | null {
function getInitialFeed(): FeedDescriptor | null {
if (isWeb) {
if (window.location.pathname === '/') {
const params = new URLSearchParams(window.location.search)
@@ -31,35 +30,27 @@ function getInitialFeed(did?: string): FeedDescriptor | null {
}
}
if (did) {
const feedFromStorage = account.get([did, 'lastSelectedHomeFeed'])
if (feedFromStorage) {
// Fall back to the last chosen one across all tabs.
return feedFromStorage as FeedDescriptor
}
const feedFromPersisted = persisted.get('lastSelectedHomeFeed')
if (feedFromPersisted) {
// Fall back to the last chosen one across all tabs.
return feedFromPersisted as FeedDescriptor
}
return null
}
export function Provider({children}: React.PropsWithChildren<{}>) {
const {currentAccount} = useSession()
const [state, setState] = useState(() => getInitialFeed(currentAccount?.did))
const [state, setState] = React.useState(() => getInitialFeed())
const saveState = useCallback(
(feed: FeedDescriptor) => {
setState(feed)
if (isWeb) {
try {
sessionStorage.setItem('lastSelectedHomeFeed', feed)
} catch {}
}
if (currentAccount?.did) {
account.set([currentAccount?.did, 'lastSelectedHomeFeed'], feed)
}
},
[currentAccount?.did],
)
const saveState = React.useCallback((feed: FeedDescriptor) => {
setState(feed)
if (isWeb) {
try {
sessionStorage.setItem('lastSelectedHomeFeed', feed)
} catch {}
}
persisted.write('lastSelectedHomeFeed', feed)
}, [])
return (
<stateContext.Provider value={state}>
@@ -69,9 +60,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
}
export function useSelectedFeed() {
return useContext(stateContext)
return React.useContext(stateContext)
}
export function useSetSelectedFeed() {
return useContext(setContext)
return React.useContext(setContext)
}
-2
View File
@@ -66,6 +66,4 @@ export type Account = {
* this device.
*/
birthdateLastUpdatedAt?: string
lastSelectedHomeFeed?: string
}
+34
View File
@@ -11,6 +11,40 @@
*
* HTML & BODY STYLES IN `web/index.html` and `bskyweb/templates/base.html`
*/
:root {
--text: black;
--background: white;
--backgroundLight: #f9fafb;
}
@media (prefers-color-scheme: dark) {
:root {
color-scheme: dark;
--text: white;
--background: black;
--backgroundLight: #111822;
}
}
html.theme--light {
--text: black;
--background: white;
--backgroundLight: #f9fafb;
background-color: white;
}
html.theme--dark {
color-scheme: dark;
background-color: black;
--text: white;
--background: black;
--backgroundLight: #111822;
}
html.theme--dim {
color-scheme: dark;
background-color: #151d28;
--text: white;
--background: #151d28;
--backgroundLight: #1c2736;
}
/* Buttons and inputs have a font set by UA, so we'll have to reset that */
button,
@@ -14,11 +14,7 @@
* the facet-set.
*/
import {
CASHTAG_REGEX,
TAG_REGEX,
TRAILING_PUNCTUATION_REGEX,
} from '@atproto/api'
import {TAG_REGEX, TRAILING_PUNCTUATION_REGEX} from '@atproto/api'
import {Mark} from '@tiptap/core'
import {type Node as ProsemirrorNode} from '@tiptap/pm/model'
import {Plugin, PluginKey} from '@tiptap/pm/state'
@@ -32,7 +28,6 @@ function getDecorations(doc: ProsemirrorNode) {
const regex = TAG_REGEX
const textContent = node.textContent
// Detect hashtags
let match
while ((match = regex.exec(textContent))) {
const [matchedString, __, tag] = match
@@ -57,27 +52,6 @@ function getDecorations(doc: ProsemirrorNode) {
}),
)
}
// Detect cashtags
const cashtagRegex = new RegExp(CASHTAG_REGEX.source, 'gu')
while ((match = cashtagRegex.exec(textContent))) {
const [_fullMatch, leading, ticker] = match
if (!ticker) continue
// Calculate positions: leading char + $ + ticker
const matchedFrom = match.index + leading.length
const matchedTo = matchedFrom + 1 + ticker.length // +1 for $
const start = pos + matchedFrom
const end = pos + matchedTo
decorations.push(
Decoration.inline(start, end, {
class: 'autolink',
}),
)
}
}
})
+2
View File
@@ -148,6 +148,8 @@ export function ComposerPrompt() {
a.relative,
a.flex_row,
a.align_start,
a.border_t,
t.atoms.border_contrast_low,
{
paddingLeft: 18,
paddingRight: 15,
+11 -13
View File
@@ -70,7 +70,6 @@ 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'
@@ -156,10 +155,6 @@ type FeedRow =
type: 'composerPrompt'
key: string
}
| {
type: 'liveEventFeedsAndTrendingBanner'
key: string
}
export function getItemsForFeedback(feedRow: FeedRow): {
item: FeedPostSliceItem
@@ -365,7 +360,7 @@ let PostFeed = ({
const showProgressIntersitial =
(followProgressGuide || followAndLikeProgressGuide) && !rightNavVisible
const {trendingVideoDisabled} = useTrendingSettings()
const {trendingDisabled, trendingVideoDisabled} = useTrendingSettings()
const ageAssuranceBannerState = useAgeAssuranceBannerState()
const selectedFeed = useSelectedFeed()
@@ -515,10 +510,13 @@ let PostFeed = ({
})
}
}
arr.push({
type: 'liveEventFeedsAndTrendingBanner',
key: 'liveEventFeedsAndTrendingBanner-' + sliceIndex,
})
if (!rightNavVisible && !trendingDisabled) {
arr.push({
type: 'interstitialTrending',
key:
'interstitial2-' + sliceIndex + '-' + lastFetchedAt,
})
}
// Show composer prompt for Discover and Following feeds
if (
hasSession &&
@@ -674,7 +672,9 @@ let PostFeed = ({
feedTab,
hasSession,
showProgressIntersitial,
trendingDisabled,
trendingVideoDisabled,
rightNavVisible,
gtMobile,
isVideoFeed,
areVideoFeedsEnabled,
@@ -773,8 +773,6 @@ let PostFeed = ({
return <AgeAssuranceDismissibleFeedBanner />
} else if (row.type === 'interstitialTrending') {
return <TrendingInterstitial />
} else if (row.type === 'liveEventFeedsAndTrendingBanner') {
return <DiscoverFeedLiveEventFeedsAndTrendingBanner />
} else if (row.type === 'composerPrompt') {
return <ComposerPrompt />
} else if (row.type === 'interstitialTrendingVideos') {
@@ -954,7 +952,7 @@ let PostFeed = ({
const actor = post.author
if (
actor.status &&
validateStatus(actor.status, liveNowConfig) &&
validateStatus(actor.did, actor.status, liveNowConfig) &&
isStatusStillActive(actor.status.expiresAt)
) {
if (!seenActorWithStatusRef.current.has(actor.did)) {
+18 -62
View File
@@ -15,7 +15,6 @@ 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,
@@ -26,7 +25,6 @@ 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'
@@ -61,8 +59,6 @@ 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 = ({
@@ -70,7 +66,6 @@ let ProfileMenu = ({
}: {
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
}): React.ReactNode => {
const t = useTheme()
const {_} = useLingui()
const {currentAccount, hasSession} = useSession()
const {openModal} = useModalControls()
@@ -84,15 +79,8 @@ let ProfileMenu = ({
const isLabelerAndNotBlocked = !!profile.associated?.labeler && !isBlocked
const [devModeEnabled] = useDevMode()
const verification = useFullVerificationState({profile})
const canGoLive = useCanGoLive()
const canGoLive = useCanGoLive(currentAccount?.did)
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)
@@ -241,22 +229,17 @@ let ProfileMenu = ({
<Menu.Trigger label={_(msg`More options`)}>
{({props}) => {
return (
<>
<Button
{...props}
testID="profileHeaderDropdownBtn"
label={_(msg`More options`)}
hitSlop={HITSLOP_20}
variant="solid"
color="secondary"
size="small"
shape="round">
{statusNudgeActive && <Gradient style={[a.rounded_full]} />}
<ButtonIcon icon={Ellipsis} size="sm" />
</Button>
{statusNudgeActive && <Dot top={1} right={1} />}
</>
<Button
{...props}
testID="profileHeaderDropdownBtn"
label={_(msg`More options`)}
hitSlop={HITSLOP_20}
variant="solid"
color="secondary"
size="small"
shape="round">
<ButtonIcon icon={Ellipsis} size="sm" />
</Button>
)
}}
</Menu.Trigger>
@@ -354,19 +337,11 @@ let ProfileMenu = ({
? _(msg`Edit live status`)
: _(msg`Go live`)
}
onPress={() => {
if (status.isDisabled) {
goLiveDisabledDialogControl.open()
} else {
goLiveDialogControl.open()
}
saveNux({
id: Nux.LiveNowBetaNudge,
data: undefined,
completed: true,
})
}}>
{statusNudgeActive && <Gradient />}
onPress={
status.isDisabled
? goLiveDisabledDialogControl.open
: goLiveDialogControl.open
}>
<Menu.ItemText>
{status.isDisabled ? (
<Trans>Go live (disabled)</Trans>
@@ -376,26 +351,7 @@ let ProfileMenu = ({
<Trans>Go live</Trans>
)}
</Menu.ItemText>
{statusNudgeActive && (
<Menu.ItemText
style={[
a.flex_0,
{
color: t.palette.primary_500,
right: isWeb ? -8 : -4,
},
]}>
<Trans>New</Trans>
</Menu.ItemText>
)}
<Menu.ItemIcon
icon={LiveIcon}
fill={
statusNudgeActive
? () => t.palette.primary_500
: undefined
}
/>
<Menu.ItemIcon icon={LiveIcon} />
</Menu.Item>
)}
{verification.viewer.role === 'verifier' &&
+5 -5
View File
@@ -1,6 +1,6 @@
import {memo, useCallback, useMemo, useState} from 'react'
import {
Image as RNImage,
Image,
Pressable,
type StyleProp,
StyleSheet,
@@ -8,7 +8,6 @@ import {
type ViewStyle,
} from 'react-native'
import Svg, {Circle, Path, Rect} from 'react-native-svg'
import {Image as ExpoImage} from 'expo-image'
import {type ModerationUI} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
@@ -38,6 +37,7 @@ import {
} from '#/state/gallery'
import {unstableCacheProfileView} from '#/state/queries/unstable-profile-cache'
import {EditImageDialog} from '#/view/com/composer/photos/EditImageDialog'
import {HighPriorityImage} from '#/view/com/util/images/Image'
import {atoms as a, tokens, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog'
@@ -289,7 +289,7 @@ let UserAvatar = ({
!((moderation?.blur && isAndroid) /* android crashes with blur */) ? (
<View style={containerStyle}>
{usePlainRNImage ? (
<RNImage
<Image
accessibilityIgnoresInvertColors
testID="userAvatarImage"
style={aviStyle}
@@ -301,7 +301,7 @@ let UserAvatar = ({
onLoad={onLoad}
/>
) : (
<ExpoImage
<HighPriorityImage
testID="userAvatarImage"
style={aviStyle}
contentFit="cover"
@@ -441,7 +441,7 @@ let EditableUserAvatar = ({
{({props}) => (
<Pressable {...props} testID="changeAvatarBtn">
{avatar ? (
<ExpoImage
<HighPriorityImage
testID="userAvatarImage"
style={aviStyle}
source={{uri: avatar}}
@@ -1,4 +1,4 @@
import {useMemo, useRef} from 'react'
import React, {useRef} from 'react'
import {type DimensionValue, Pressable, View} from 'react-native'
import Animated, {
type AnimatedRef,
@@ -34,7 +34,7 @@ export function ConstrainedImage({
* Computed as a % value to apply as `paddingTop`, this basically controls
* the height of the image.
*/
const outerAspectRatio = useMemo<DimensionValue>(() => {
const outerAspectRatio = React.useMemo<DimensionValue>(() => {
const ratio = isNative
? Math.min(1 / aspectRatio, minMobileAspectRatio ?? 16 / 9) // 9:16 bounding box
: Math.min(1 / aspectRatio, 1) // 1:1 bounding box
@@ -127,7 +127,6 @@ export function AutoSizedImage({
}
}
}}
loading="lazy"
/>
<MediaInsetBorder />
@@ -29,7 +29,7 @@ interface Props {
viewContext?: PostEmbedViewContext
insetBorderStyle?: StyleProp<ViewStyle>
containerRefs: AnimatedRef<any>[]
thumbDimsRef: React.RefObject<(Dimensions | null)[]>
thumbDimsRef: React.MutableRefObject<(Dimensions | null)[]>
}
export function GalleryItem({
@@ -87,7 +87,6 @@ export function GalleryItem({
height: e.source.height,
}
}}
loading="lazy"
/>
<MediaInsetBorder style={insetBorderStyle} />
</Pressable>
+13
View File
@@ -0,0 +1,13 @@
import {Image, type ImageProps, type ImageSource} from 'expo-image'
interface HighPriorityImageProps extends ImageProps {
source: ImageSource
}
export function HighPriorityImage({source, ...props}: HighPriorityImageProps) {
const updatedSource = {
uri: typeof source === 'object' && source ? source.uri : '',
} satisfies ImageSource
return (
<Image accessibilityIgnoresInvertColors source={updatedSource} {...props} />
)
}
+3
View File
@@ -0,0 +1,3 @@
import {Image} from 'react-native'
export const HighPriorityImage = Image
@@ -1,11 +1,11 @@
import {useRef} from 'react'
import React from 'react'
import {type StyleProp, View, type ViewStyle} from 'react-native'
import {type AnimatedRef, useAnimatedRef} from 'react-native-reanimated'
import {type AppBskyEmbedImages} from '@atproto/api'
import {type Dimensions} from '#/view/com/lightbox/ImageViewing/@types'
import {atoms as a, useBreakpoints} from '#/alf'
import {PostEmbedViewContext} from '#/components/Post/Embed/types'
import {type Dimensions} from '../../lightbox/ImageViewing/@types'
import {GalleryItem} from './Gallery'
interface ImageLayoutGridProps {
@@ -60,7 +60,7 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
const containerRef2 = useAnimatedRef()
const containerRef3 = useAnimatedRef()
const containerRef4 = useAnimatedRef()
const thumbDimsRef = useRef<(Dimensions | null)[]>([])
const thumbDimsRef = React.useRef<(Dimensions | null)[]>([])
switch (count) {
case 2: {
+2 -4
View File
@@ -74,11 +74,9 @@ export function DesktopFeeds() {
overflowY: 'auto',
}),
]}>
{pinnedFeedInfos.map((feedInfo, index) => {
{pinnedFeedInfos.map(feedInfo => {
const feed = feedInfo.feedDescriptor
const current =
route.name === 'Home' &&
(selectedFeed ? feed === selectedFeed : index === 0)
const current = route.name === 'Home' && feed === selectedFeed
return (
<FeedItem
+2 -5
View File
@@ -22,7 +22,6 @@ import {CENTER_COLUMN_OFFSET} from '#/components/Layout'
import {InlineLinkText} from '#/components/Link'
import {ProgressGuideList} from '#/components/ProgressGuide/List'
import {Text} from '#/components/Typography'
import {SidebarLiveEventFeedsBanner} from '#/features/liveEvents/components/SidebarLiveEventFeedsBanner'
function useWebQueryParams() {
const navigation = useNavigation()
@@ -50,8 +49,7 @@ export function DesktopRightNav({routeName}: {routeName: string}) {
const isSearchScreen = routeName === 'Search'
const webqueryParams = useWebQueryParams()
const searchQuery = webqueryParams?.q
const showExploreScreenDuplicatedContent =
!isSearchScreen || (isSearchScreen && !!searchQuery)
const showTrending = !isSearchScreen || (isSearchScreen && !!searchQuery)
const {rightNavVisible, centerColumnOffset, leftNavMinimal} =
useLayoutBreakpoints()
@@ -92,8 +90,7 @@ export function DesktopRightNav({routeName}: {routeName: string}) {
</>
)}
{showExploreScreenDuplicatedContent && <SidebarLiveEventFeedsBanner />}
{showExploreScreenDuplicatedContent && <SidebarTrendingTopics />}
{showTrending && <SidebarTrendingTopics />}
<Text style={[a.leading_snug, t.atoms.text_contrast_low]}>
{hasSession && (
+8 -46
View File
@@ -19,7 +19,6 @@
<title>%WEB_TITLE%</title>
<link rel="preload" as="font" type="font/woff2" href="/static/media/InterVariable.c504db5c06caaf7cdfba.woff2" crossorigin>
<link rel="stylesheet" href="/static/style.css">
<style>
/**
@@ -43,6 +42,14 @@
font-style: italic;
font-display: swap;
}
html {
background-color: white;
}
@media (prefers-color-scheme: dark) {
html {
background-color: black;
}
}
html,
body {
margin: 0px;
@@ -57,19 +64,6 @@
-ms-overflow-style: scrollbar;
font-synthesis-weight: none;
}
:root {
--text: black;
--background: white;
--backgroundLight: #e2e7ee;
}
@media (prefers-color-scheme: dark) {
:root {
color-scheme: dark;
--text: white;
--background: black;
--backgroundLight: #232e3e;
}
}
html,
body,
#root {
@@ -78,32 +72,6 @@
min-height: 100%;
width: 100%;
}
html.theme--light,
html.theme--light body,
html.theme--light #root {
background-color: white;
--text: black;
--background: white;
--backgroundLight: #DCE2EA;
}
html.theme--dark,
html.theme--dark body,
html.theme--dark #root {
color-scheme: dark;
background-color: black;
--text: white;
--background: black;
--backgroundLight: #232E3E;
}
html.theme--dim,
html.theme--dim body,
html.theme--dim #root {
color-scheme: dark;
background-color: #151D28;
--text: white;
--background: #151D28;
--backgroundLight: #2C3A4E;
}
#splash {
display: flex;
position: fixed;
@@ -130,12 +98,6 @@
overflow-y: scroll;
}
</style>
<script>
const theme = localStorage.getItem('ALF_THEME')
if (theme) {
document.documentElement.classList.add(`theme--${theme}`)
}
</script>
</head>
<body>
+148 -186
View File
@@ -20,16 +20,16 @@
"@jridgewell/gen-mapping" "^0.3.0"
"@jridgewell/trace-mapping" "^0.3.9"
"@atproto-labs/did-resolver@0.2.5":
version "0.2.5"
resolved "https://registry.yarnpkg.com/@atproto-labs/did-resolver/-/did-resolver-0.2.5.tgz#74b34e38b10fe1d18a42b35b32909b9a196e9693"
integrity sha512-he7EC6OMSifNs01a4RT9mta/yYitoKDzlK9ty2TFV5Uj/+HpB4vYMRdIDFrRW0Hcsehy90E2t/dw0t7361MEKQ==
"@atproto-labs/did-resolver@0.2.4":
version "0.2.4"
resolved "https://registry.yarnpkg.com/@atproto-labs/did-resolver/-/did-resolver-0.2.4.tgz#3df8f94845fae10bb284303d6e73ffaa5a91b158"
integrity sha512-sbXxBnAJWsKv/FEGG6a/WLz7zQYUr1vA2TXvNnPwwJQJCjPwEJMOh1vM22wBr185Phy7D2GD88PcRokn7eUVyw==
dependencies:
"@atproto-labs/fetch" "0.2.3"
"@atproto-labs/pipe" "0.1.1"
"@atproto-labs/simple-store" "0.3.0"
"@atproto-labs/simple-store-memory" "0.1.4"
"@atproto/did" "0.2.4"
"@atproto/did" "0.2.3"
zod "^3.23.8"
"@atproto-labs/fetch-node@0.2.0":
@@ -82,12 +82,12 @@
"@atproto/xrpc" "^0.7.6"
"@atproto/xrpc-server" "^0.10.0"
"@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==
"@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==
dependencies:
"@atproto/common-web" "^0.4.12"
"@atproto/common-web" "^0.4.11"
"@atproto/lexicon" "^0.6.0"
"@atproto/syntax" "^0.4.2"
"@atproto/xrpc" "^0.7.7"
@@ -96,12 +96,26 @@
tlds "^1.234.0"
zod "^3.23.8"
"@atproto/api@^0.18.14", "@atproto/api@^0.18.8":
version "0.18.14"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.18.14.tgz#8cda4323f46928a651bb8f5cae4994bc47e78885"
integrity sha512-1pWAPbuG3RA1o8uOAwYWZOddvNjuweYOxwTvys1q/r9NCjoGkZY0uJUy1dr6LKFaDk8bjikd2O1cgsRwFfv6Fw==
"@atproto/api@^0.18.5", "@atproto/api@^0.18.7":
version "0.18.7"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.18.7.tgz#3175ec8f1909ddcae488183a2180de234e7acce4"
integrity sha512-vUluqN1XU5AX5tgfSJjjjUzALCMq8DjdI0jlIhRYyn2Chb0ZOCU8k0ZTpUAcuDFE2FoxxW4S3kvtlHwLMtN5dQ==
dependencies:
"@atproto/common-web" "^0.4.12"
"@atproto/common-web" "^0.4.7"
"@atproto/lexicon" "^0.6.0"
"@atproto/syntax" "^0.4.2"
"@atproto/xrpc" "^0.7.7"
await-lock "^2.2.2"
multiformats "^9.9.0"
tlds "^1.234.0"
zod "^3.23.8"
"@atproto/api@^0.18.6":
version "0.18.6"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.18.6.tgz#04c26b97bda01cbe276dea523de6e4a184894c18"
integrity sha512-dkzy2OHSAGgzG9GExvOiwRY73EzVD2AiD3nksng+V6erG0kwLfbmVYjoP9mq9Y16BCXr/7q9lekfogthqU614Q==
dependencies:
"@atproto/common-web" "^0.4.7"
"@atproto/lexicon" "^0.6.0"
"@atproto/syntax" "^0.4.2"
"@atproto/xrpc" "^0.7.7"
@@ -128,23 +142,23 @@
multiformats "^9.9.0"
uint8arrays "3.0.0"
"@atproto/bsky@^0.0.210":
version "0.0.210"
resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.210.tgz#e9217025479e49d371f80b498992f0599eb2b79f"
integrity sha512-FZ/AWxAvg7BaHE1AZErUZpAD1zA/laOSlaWhk7/E0nKHcXH7mgWXWeE5CJsCT8sSpoLm4GIYV/4wC3lFbSu33g==
"@atproto/bsky@^0.0.202":
version "0.0.202"
resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.202.tgz#7b4376dba273a4c1b810846074974d1b11a58e5c"
integrity sha512-QE0T/Vr/WdPZ1lUAZNtNqkH9D+Ii7+95oUR0Q2EYyr0J9G3hlLTvc/FJog91aSe6fzZludRB/n1sRUfdMRePdg==
dependencies:
"@atproto-labs/fetch-node" "0.2.0"
"@atproto-labs/xrpc-utils" "0.0.24"
"@atproto/api" "^0.18.13"
"@atproto/common" "^0.5.7"
"@atproto/api" "^0.18.7"
"@atproto/common" "^0.5.3"
"@atproto/crypto" "^0.4.5"
"@atproto/did" "^0.2.4"
"@atproto/did" "^0.2.3"
"@atproto/identity" "^0.4.10"
"@atproto/lexicon" "^0.6.0"
"@atproto/repo" "^0.8.12"
"@atproto/sync" "^0.1.39"
"@atproto/syntax" "^0.4.2"
"@atproto/xrpc-server" "^0.10.8"
"@atproto/xrpc-server" "^0.10.3"
"@bufbuild/protobuf" "^1.5.0"
"@connectrpc/connect" "^1.1.4"
"@connectrpc/connect-express" "^1.1.4"
@@ -194,13 +208,13 @@
pino-http "^8.2.1"
typed-emitter "^2.1.0"
"@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==
"@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.8"
"@atproto/lex-json" "0.0.8"
"@atproto/lex-data" "0.0.7"
"@atproto/lex-json" "0.0.7"
zod "^3.23.8"
"@atproto/common-web@^0.4.4", "@atproto/common-web@^0.4.6":
@@ -265,18 +279,6 @@
multiformats "^9.9.0"
pino "^8.21.0"
"@atproto/common@^0.5.7", "@atproto/common@^0.5.8":
version "0.5.8"
resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.5.8.tgz#90d8a99405140885631a380fe24a97be15107c49"
integrity sha512-6BS6OJ/eiN/w8cu3xG1NA/waq9jBsYXZ6pfV85WUDegbfZaGS/IVtpJtjdE7LemE8cJys3AqGFDVJzeXDBQgbw==
dependencies:
"@atproto/common-web" "^0.4.12"
"@atproto/lex-cbor" "0.0.8"
"@atproto/lex-data" "0.0.8"
iso-datestring-validator "^2.2.2"
multiformats "^9.9.0"
pino "^8.21.0"
"@atproto/crypto@0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@atproto/crypto/-/crypto-0.1.0.tgz#bc73a479f9dbe06fa025301c182d7f7ab01bc568"
@@ -306,23 +308,23 @@
"@noble/hashes" "^1.6.1"
uint8arrays "3.0.0"
"@atproto/dev-env@^0.3.204":
version "0.3.204"
resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.204.tgz#cb7360ee2f6be7301c60d964b6e3f96351808be6"
integrity sha512-MdqkjGVXH2AovnFpHrWgImr7dGtbEKsaVpDxKo2LelJ9Cwc015fc1Mzfa+4h7DHoDwtjiAZIBG7rH8ZWO7hZPQ==
"@atproto/dev-env@^0.3.196":
version "0.3.196"
resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.196.tgz#e98d3f3e7a7cee874425b01bfc164e22d1111664"
integrity sha512-65LrDGcGIFrZ+JnQlW38ZFO9jYO1Sp9fHc2rRq6bZzfAGT6c7K3EWHhXhNDtQ4oL3DYAT9+p56Pcv68XMaSipA==
dependencies:
"@atproto/api" "^0.18.13"
"@atproto/bsky" "^0.0.210"
"@atproto/api" "^0.18.7"
"@atproto/bsky" "^0.0.202"
"@atproto/bsync" "^0.0.23"
"@atproto/common-web" "^0.4.11"
"@atproto/common-web" "^0.4.7"
"@atproto/crypto" "^0.4.5"
"@atproto/identity" "^0.4.10"
"@atproto/lexicon" "^0.6.0"
"@atproto/ozone" "^0.1.161"
"@atproto/pds" "^0.4.203"
"@atproto/ozone" "^0.1.160"
"@atproto/pds" "^0.4.199"
"@atproto/sync" "^0.1.39"
"@atproto/syntax" "^0.4.2"
"@atproto/xrpc-server" "^0.10.8"
"@atproto/xrpc-server" "^0.10.3"
"@did-plc/lib" "^0.0.1"
"@did-plc/server" "^0.0.1"
dotenv "^16.0.3"
@@ -332,14 +334,7 @@
uint8arrays "3.0.0"
undici "^6.14.1"
"@atproto/did@0.2.4", "@atproto/did@^0.2.4":
version "0.2.4"
resolved "https://registry.yarnpkg.com/@atproto/did/-/did-0.2.4.tgz#8843b4e53cc1de22e710d4db47170ca2f631dee4"
integrity sha512-nxNiCgXeo7pfjojq9fpfZxCO0X0xUipNVKW+AHNZwQKiUDt6zYL0VXEfm8HBUwQOCmKvj2pRRSM1Cur+tUWk3g==
dependencies:
zod "^3.23.8"
"@atproto/did@^0.2.3":
"@atproto/did@0.2.3", "@atproto/did@^0.2.3":
version "0.2.3"
resolved "https://registry.yarnpkg.com/@atproto/did/-/did-0.2.3.tgz#83cd18ae105324913b30a2259cd9d886677b2d15"
integrity sha512-VI8JJkSizvM2cHYJa37WlbzeCm5tWpojyc1/Zy8q8OOjyoy6X4S4BEfoP941oJcpxpMTObamibQIXQDo7tnIjg==
@@ -379,7 +374,7 @@
multiformats "^9.9.0"
tslib "^2.8.1"
"@atproto/lex-cbor@0.0.3":
"@atproto/lex-cbor@0.0.3", "@atproto/lex-cbor@^0.0.3":
version "0.0.3"
resolved "https://registry.yarnpkg.com/@atproto/lex-cbor/-/lex-cbor-0.0.3.tgz#13712fa5216cd336ebbd31fbd34d35bf6ea21492"
integrity sha512-N8lCV3kK5ZcjSOWxKLWqzlnaSpK4isjXRZ0EqApl/5y9KB64s78hQ/U3KIE5qnPRlBbW5kSH3YACoU27u9nTOA==
@@ -388,22 +383,14 @@
multiformats "^9.9.0"
tslib "^2.8.1"
"@atproto/lex-cbor@0.0.8", "@atproto/lex-cbor@^0.0.8":
version "0.0.8"
resolved "https://registry.yarnpkg.com/@atproto/lex-cbor/-/lex-cbor-0.0.8.tgz#191e3443bb3157618f4b66637b305c6127ab5c28"
integrity sha512-WFUkNTLUMunPaA+NkD2INwfhrgo5fAMz7zSk2ncoqbK2AS78X2ith8TJSevY0ynPukbFmaJ9BdauzCpWQ4ZIqQ==
"@atproto/lex-client@0.0.4":
version "0.0.4"
resolved "https://registry.yarnpkg.com/@atproto/lex-client/-/lex-client-0.0.4.tgz#04fca87296f3177ba122e41bed89d36b4d81b01a"
integrity sha512-tGaenywYo6IvzKKMYuZB+sMBQNDkQ53PLz/NM0WeXnaa/e58VhOGzwVLnNSI/QR9qLlwHFfMf8AIFZyAVHBWCw==
dependencies:
"@atproto/lex-data" "0.0.8"
tslib "^2.8.1"
"@atproto/lex-client@0.0.9":
version "0.0.9"
resolved "https://registry.yarnpkg.com/@atproto/lex-client/-/lex-client-0.0.9.tgz#595cc805583587bcd19d1ce3a7957d15289c0e0b"
integrity sha512-30WtEedG0s/JNkbHzxpObkUg0id4+/p1O7LcUVCQWrNhWRw/hCzhHySSgFKKIVeLKAYIrZmaWt1XlAdNhGO7DQ==
dependencies:
"@atproto/lex-data" "0.0.8"
"@atproto/lex-json" "0.0.8"
"@atproto/lex-schema" "0.0.9"
"@atproto/lex-data" "0.0.3"
"@atproto/lex-json" "0.0.3"
"@atproto/lex-schema" "0.0.4"
tslib "^2.8.1"
"@atproto/lex-data@0.0.2":
@@ -417,7 +404,7 @@
uint8arrays "3.0.0"
unicode-segmenter "^0.14.0"
"@atproto/lex-data@0.0.3":
"@atproto/lex-data@0.0.3", "@atproto/lex-data@^0.0.3":
version "0.0.3"
resolved "https://registry.yarnpkg.com/@atproto/lex-data/-/lex-data-0.0.3.tgz#1ce1bd19e17af41b2991a2a02ad439a136dec4e9"
integrity sha512-ivo1IpY/EX+RIpxPgCf4cPhQo5bfu4nrpa1vJCt8hCm9SfoonJkDFGa0n4SMw4JnXZoUcGcrJ46L+D8bH6GI2g==
@@ -428,10 +415,10 @@
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"
integrity sha512-1Y5tz7BkS7380QuLNXaE8GW8Xba+mRWugt8BKM4BUFYjjUZdmirU8lr72iM4XlEBrzRu8Cfvj+MbsbYaZv+IgA==
"@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"
@@ -439,12 +426,12 @@
uint8arrays "3.0.0"
unicode-segmenter "^0.14.0"
"@atproto/lex-document@0.0.10":
version "0.0.10"
resolved "https://registry.yarnpkg.com/@atproto/lex-document/-/lex-document-0.0.10.tgz#a58cdd79fc49e411d4024037f18a2ad0297887e1"
integrity sha512-GrvO36UyWhStSNN0CtVswMyzYK7eUA0zLjYJRqpghAyzYV9ZVXTUL1Vx79MUSg3tC1jDM1A0hmtsjE1Cyo2rHQ==
"@atproto/lex-document@0.0.5":
version "0.0.5"
resolved "https://registry.yarnpkg.com/@atproto/lex-document/-/lex-document-0.0.5.tgz#8d4851b351149ba673c1de9c1898c3c9ebd8b4b3"
integrity sha512-faGcwsupdtvoFZ8ILEu14MYJ0z/pCiQ+Yu4WEkVtJvy8jIkFxeZ8MxxZgm8KrlSt6jxsyYvMpscQCtYpVmafFQ==
dependencies:
"@atproto/lex-schema" "0.0.9"
"@atproto/lex-schema" "0.0.4"
core-js "^3"
tslib "^2.8.1"
@@ -464,35 +451,35 @@
"@atproto/lex-data" "0.0.3"
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"
integrity sha512-w1Qmkae1QhmNz+i1Zm3xr3jp0UPPRENmdlpU0qIrdxWDo9W4Mzkeyc3eSoa+Zs+zN8xkRSQw7RLZte/B7Ipdwg==
"@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.8"
"@atproto/lex-data" "0.0.7"
tslib "^2.8.1"
"@atproto/lex-resolver@0.0.10":
version "0.0.10"
resolved "https://registry.yarnpkg.com/@atproto/lex-resolver/-/lex-resolver-0.0.10.tgz#2139836dd80a5f5d31979d9fb2f10872a4a844b6"
integrity sha512-7cV/vjJGHMUbzv1y2kIOChC/B5ox1F5hKpG3fFZdkM8eZ5o9NuMiIojvTgCYdxyH+XA/X6qjblMrru73fFRtmA==
"@atproto/lex-resolver@0.0.5":
version "0.0.5"
resolved "https://registry.yarnpkg.com/@atproto/lex-resolver/-/lex-resolver-0.0.5.tgz#9d0645c43423cb99b491ca8e658770395d2cd630"
integrity sha512-gBqHY1HS/g2rxuk8CPzB1cmMxmOWK897/jIboMhYDXnuavg8zh54eljCFXr8GvaJdo5DnMGyBEilHNppJOD4mg==
dependencies:
"@atproto-labs/did-resolver" "0.2.5"
"@atproto-labs/did-resolver" "0.2.4"
"@atproto/crypto" "0.4.5"
"@atproto/lex-client" "0.0.9"
"@atproto/lex-data" "0.0.8"
"@atproto/lex-document" "0.0.10"
"@atproto/lex-schema" "0.0.9"
"@atproto/lex-client" "0.0.4"
"@atproto/lex-data" "0.0.3"
"@atproto/lex-document" "0.0.5"
"@atproto/lex-schema" "0.0.4"
"@atproto/repo" "0.8.12"
"@atproto/syntax" "0.4.2"
tslib "^2.8.1"
"@atproto/lex-schema@0.0.9":
version "0.0.9"
resolved "https://registry.yarnpkg.com/@atproto/lex-schema/-/lex-schema-0.0.9.tgz#b47173092d4ee9850b1fe013963b18b3a8bcf1ed"
integrity sha512-nsXpG0BdWu5Qn8qgs/+tHKP2gdVoYNiYwIUl+lt6EFb5juuOScmPilhZOa9MPDpLLzVgl35x9whjh842CvpHJQ==
"@atproto/lex-schema@0.0.4":
version "0.0.4"
resolved "https://registry.yarnpkg.com/@atproto/lex-schema/-/lex-schema-0.0.4.tgz#382998f5c384b864a1bc0c00bd073c1168503895"
integrity sha512-WlF+w/OH16KR9XYW9J7hNEDgHp37uG2EWm8/iJknDFsWYjbhgl71x1jcqqCM8BcDxAYxyJLOvPuadOC6cG5JjA==
dependencies:
"@atproto/lex-data" "0.0.8"
"@atproto/lex-data" "0.0.3"
"@atproto/syntax" "0.4.2"
tslib "^2.8.1"
@@ -518,49 +505,49 @@
multiformats "^9.9.0"
zod "^3.23.8"
"@atproto/oauth-provider-api@0.3.6":
version "0.3.6"
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-api/-/oauth-provider-api-0.3.6.tgz#ab8ee8f86c3eac7c6008f786205b7f149678f85a"
integrity sha512-ddUCbBH/1X+3YaegJzOTESAc8+ZUwn0sLgKpxp4Na3J6cPeZfXiheWGKxbu5pTPwJr1msCNOakqSMkoLbP7UEA==
"@atproto/oauth-provider-api@0.3.4":
version "0.3.4"
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-api/-/oauth-provider-api-0.3.4.tgz#bce9b1a5a6bd759b0de8f6b80c4aced9b64f0c79"
integrity sha512-K3gBqyf9VlYE6tvfD0EDya9WQ9XWtbuhxkI1XHyCIyAvAemhBGoJ1As0ESo3UpJmd2JhA2DmLj4oOvBqknamBA==
dependencies:
"@atproto/jwk" "0.6.0"
"@atproto/oauth-types" "0.6.1"
"@atproto/oauth-types" "0.5.2"
"@atproto/oauth-provider-frontend@0.2.7":
version "0.2.7"
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-frontend/-/oauth-provider-frontend-0.2.7.tgz#b4157fea9fffd970c357f25ed7588359e820d94a"
integrity sha512-cXS/lonP0WzPEcCiv1BuMUyp1Oq+eZpwX3sbvhsNYwtyM+7PLU1KhD9y2VR01S7UhCxRPmpQiIMLvsdVWNddZA==
"@atproto/oauth-provider-frontend@0.2.5":
version "0.2.5"
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-frontend/-/oauth-provider-frontend-0.2.5.tgz#6a62475bf70cc9d198e8e5f6683fb0c8d39797a0"
integrity sha512-9+23B2Wp2G5UvHPiKQGwoK3sOu3JHa+jVfWjbUkXhho0HGL60hAbyrdm0C6n3UER/mLfn8MTjzW9jQSuJXHosg==
optionalDependencies:
"@atproto/oauth-provider-api" "0.3.6"
"@atproto/oauth-provider-api" "0.3.4"
"@atproto/oauth-provider-ui@0.4.1":
version "0.4.1"
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-ui/-/oauth-provider-ui-0.4.1.tgz#5f957f1eed89409ba842bffb5b7e9bf25c6bcc44"
integrity sha512-70K0uwCCz68qK5bwce8NG6RsBp1pBp/YMzZUGq09XhYVwx/wHgaf1HUFuwLIGV4sVKgahLsNBrRHY9n7fXFbRA==
"@atproto/oauth-provider-ui@0.3.6":
version "0.3.6"
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-ui/-/oauth-provider-ui-0.3.6.tgz#1181040d33b19ed7124f5ad12833200bcd7892e6"
integrity sha512-uxnBWEX/Ht2JJbeibMhCu3OatKchhQGV4v5KfXzTylX2VIZrRmG8PVr5YnHmijjJZD+NgeDWlFSdyGdZZ7qU9w==
optionalDependencies:
"@atproto/oauth-provider-api" "0.3.6"
"@atproto/oauth-provider-api" "0.3.4"
"@atproto/oauth-provider@^0.15.4":
version "0.15.4"
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.15.4.tgz#eb89d0458f39f3a6c1445b98cbfd5367027fdd26"
integrity sha512-Ypa6WnG8SiTDfQWXF0MUcZJg/4A9jXObWHi4IAd0FZj9kN/vln8hzq8BSQpyAn3Yy8+XVoaKyN/jhzwxkqQ9Sw==
"@atproto/oauth-provider@^0.14.1":
version "0.14.1"
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.14.1.tgz#b1fd01408bb988ee29f19e2ab6ab323a31ea7590"
integrity sha512-n0qHOhUfSwp6w3i54tC5IjmeE5raoxEyxchJkqvoj/xWBiysdMJwpLTIvQ90rcXbjIA22Jv58oQ/mtx07w82xA==
dependencies:
"@atproto-labs/fetch" "0.2.3"
"@atproto-labs/fetch-node" "0.2.0"
"@atproto-labs/pipe" "0.1.1"
"@atproto-labs/simple-store" "0.3.0"
"@atproto-labs/simple-store-memory" "0.1.4"
"@atproto/common" "^0.5.8"
"@atproto/did" "0.2.4"
"@atproto/common" "^0.5.3"
"@atproto/did" "0.2.3"
"@atproto/jwk" "0.6.0"
"@atproto/jwk-jose" "0.1.11"
"@atproto/lex-document" "0.0.10"
"@atproto/lex-resolver" "0.0.10"
"@atproto/oauth-provider-api" "0.3.6"
"@atproto/oauth-provider-frontend" "0.2.7"
"@atproto/oauth-provider-ui" "0.4.1"
"@atproto/lex-document" "0.0.5"
"@atproto/lex-resolver" "0.0.5"
"@atproto/oauth-provider-api" "0.3.4"
"@atproto/oauth-provider-frontend" "0.2.5"
"@atproto/oauth-provider-ui" "0.3.6"
"@atproto/oauth-scopes" "0.3.0"
"@atproto/oauth-types" "0.6.1"
"@atproto/oauth-types" "0.5.2"
"@atproto/syntax" "0.4.2"
"@hapi/accept" "^6.0.3"
"@hapi/address" "^5.1.1"
@@ -582,29 +569,29 @@
"@atproto/did" "^0.2.3"
"@atproto/syntax" "^0.4.2"
"@atproto/oauth-types@0.6.1":
version "0.6.1"
resolved "https://registry.yarnpkg.com/@atproto/oauth-types/-/oauth-types-0.6.1.tgz#c75be82016052fd2f877030a4f4832af7d7d7016"
integrity sha512-3z92GN/6zCq9E2GTTfZM27tWEbvi1qwFSA7KoS5+wqBC4kSsLvnLxmbKH402Z40DfWS4YWqw0DkHsgP0LNFDEA==
"@atproto/oauth-types@0.5.2":
version "0.5.2"
resolved "https://registry.yarnpkg.com/@atproto/oauth-types/-/oauth-types-0.5.2.tgz#443d2b004403f33fbdcbe4f3406f645c2785fe04"
integrity sha512-9DCDvtvCanTwAaU5UakYDO0hzcOITS3RutK5zfLytE5Y9unj0REmTDdN8Xd8YCfUJl7T/9pYpf04Uyq7bFTASg==
dependencies:
"@atproto/did" "0.2.4"
"@atproto/did" "0.2.3"
"@atproto/jwk" "0.6.0"
zod "^3.23.8"
"@atproto/ozone@^0.1.161":
version "0.1.161"
resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.161.tgz#31f776c168c4eba8e095ebcf8f4cfbd13f6f175c"
integrity sha512-gGbo0sbopWnW2zkpeGBrERLuAeI0lhWmnAKoHK3g1/F/r3V/8k2MRAEMq7bhwOY7uCVoZq4ob4sX+pCNPUM8AQ==
"@atproto/ozone@^0.1.160":
version "0.1.160"
resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.160.tgz#1131fa6c0d1b37e3a7b0aaa2f788b36eb2f7a750"
integrity sha512-p0CP5/1Anv1qbxoRBC4IOaSkFe28Y3nKmzfrx0Z6in/o8VF/LZ9dhuW1UgrDQs4PyMzMOar3UFfO2ZUL4Gg58A==
dependencies:
"@atproto/api" "^0.18.8"
"@atproto/api" "^0.18.5"
"@atproto/common" "^0.5.3"
"@atproto/crypto" "^0.4.5"
"@atproto/identity" "^0.4.10"
"@atproto/lexicon" "^0.6.0"
"@atproto/syntax" "^0.4.2"
"@atproto/ws-client" "^0.0.4"
"@atproto/ws-client" "^0.0.3"
"@atproto/xrpc" "^0.7.7"
"@atproto/xrpc-server" "^0.10.4"
"@atproto/xrpc-server" "^0.10.3"
"@did-plc/lib" "^0.0.1"
compression "^1.7.4"
cors "^2.8.5"
@@ -622,30 +609,30 @@
undici "^6.14.1"
ws "^8.12.0"
"@atproto/pds@^0.4.203":
version "0.4.204"
resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.204.tgz#d6f6280b7c21bdac2a80470ec187f726a5c94c63"
integrity sha512-bADjh9TWbfyN/FGPRmolvDPatmfoWfv3pstlujEXBi3ja67nJAbhdtqrMesVAltmD8GzrLw1/APrYoYJ0Pi8GA==
"@atproto/pds@^0.4.199":
version "0.4.199"
resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.199.tgz#8cdf10ad449c57206b4820191ecfb358346908bd"
integrity sha512-EtXVzPusvLVRIhjUutKcb75lPN9oWD24bZGbRrDaehcbI0QXbosGek/vgil0ZFLn0YW3G/BssM0K63KUotscrw==
dependencies:
"@atproto-labs/fetch-node" "0.2.0"
"@atproto-labs/simple-store" "0.3.0"
"@atproto-labs/simple-store-memory" "0.1.4"
"@atproto-labs/simple-store-redis" "0.0.1"
"@atproto-labs/xrpc-utils" "0.0.24"
"@atproto/api" "^0.18.14"
"@atproto/api" "^0.18.6"
"@atproto/aws" "^0.2.31"
"@atproto/common" "^0.5.8"
"@atproto/common" "^0.5.3"
"@atproto/crypto" "^0.4.5"
"@atproto/identity" "^0.4.10"
"@atproto/lex-cbor" "^0.0.8"
"@atproto/lex-data" "^0.0.8"
"@atproto/lex-cbor" "^0.0.3"
"@atproto/lex-data" "^0.0.3"
"@atproto/lexicon" "^0.6.0"
"@atproto/oauth-provider" "^0.15.4"
"@atproto/oauth-provider" "^0.14.1"
"@atproto/oauth-scopes" "^0.3.0"
"@atproto/repo" "^0.8.12"
"@atproto/syntax" "^0.4.2"
"@atproto/xrpc" "^0.7.7"
"@atproto/xrpc-server" "^0.10.9"
"@atproto/xrpc-server" "^0.10.3"
"@did-plc/lib" "^0.0.4"
"@hapi/address" "^5.1.1"
better-sqlite3 "^10.0.0"
@@ -738,14 +725,6 @@
"@atproto/common" "^0.5.0"
ws "^8.12.0"
"@atproto/ws-client@^0.0.4":
version "0.0.4"
resolved "https://registry.yarnpkg.com/@atproto/ws-client/-/ws-client-0.0.4.tgz#9e436c0e72abea5da0d5a7e8ec862cec0fdb10cd"
integrity sha512-dox1XIymuC7/ZRhUqKezIGgooZS45C6vHCfu0PnWjfvsLCK2kAlnvX4IBkA/WpcoijDhQ9ejChnFbo/sLmgvAg==
dependencies:
"@atproto/common" "^0.5.3"
ws "^8.12.0"
"@atproto/xrpc-server@^0.10.0":
version "0.10.2"
resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.10.2.tgz#b68f42a7b6df5bb8081525e5c981a709b9b02739"
@@ -784,25 +763,6 @@
ws "^8.12.0"
zod "^3.23.8"
"@atproto/xrpc-server@^0.10.4", "@atproto/xrpc-server@^0.10.8", "@atproto/xrpc-server@^0.10.9":
version "0.10.9"
resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.10.9.tgz#d1047c49c458a99442ba6516541fd9ff0f23299e"
integrity sha512-6PVlkbvGitKAAqR9Lt8KzLp/2c8RHjpJdJ3OBM5baHUwpej7nKESyXvO5bk/GZHTvIcp0Vg0b45spn3/F9tsWg==
dependencies:
"@atproto/common" "^0.5.8"
"@atproto/crypto" "^0.4.5"
"@atproto/lex-cbor" "0.0.8"
"@atproto/lex-data" "0.0.8"
"@atproto/lexicon" "^0.6.0"
"@atproto/ws-client" "^0.0.4"
"@atproto/xrpc" "^0.7.7"
express "^4.17.2"
http-errors "^2.0.0"
mime-types "^2.1.35"
rate-limiter-flexible "^2.4.1"
ws "^8.12.0"
zod "^3.23.8"
"@atproto/xrpc@^0.7.6":
version "0.7.6"
resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.7.6.tgz#bc12b0e37f81fa76589691634d4fac9774fd0cb5"
@@ -6184,13 +6144,20 @@
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@^1.15.2":
"@react-native-async-storage/async-storage@2.2.0":
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"
@@ -11372,11 +11339,6 @@ expo-notifications@~0.32.14:
expo-application "~7.0.8"
expo-constants "~18.0.11"
expo-privacy-sensitive@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/expo-privacy-sensitive/-/expo-privacy-sensitive-0.1.0.tgz#2177d7a3cb8ed352df94c5806d012dfb7b48bc84"
integrity sha512-N0xa8yz+u7HvGY5CqZeo5cwtTOyFQxOxxt15jeW1eAjLKZAcNrtrDGGJP18TX2eh5TfJ3I6OtmECJg9Q8+Yorw==
expo-pwa@0.0.127:
version "0.0.127"
resolved "https://registry.yarnpkg.com/expo-pwa/-/expo-pwa-0.0.127.tgz#b8d2fd28efff408a24e0f2539bfb47e09f8e4ebe"