Compare commits

..

1 Commits

Author SHA1 Message Date
Samuel Newman 3b94b819e4 Patch video player to add a ton of debug logging 2026-01-08 15:18:37 +02:00
64 changed files with 677 additions and 1710 deletions
@@ -16,9 +16,6 @@ jobs:
if: github.repository == 'bluesky-social/social-app'
name: Build and Submit Android
runs-on: Linux-x64-32core
concurrency:
group: android-build
cancel-in-progress: false
steps:
- name: Check for EXPO_TOKEN
run: >
-3
View File
@@ -16,9 +16,6 @@ jobs:
if: github.repository == 'bluesky-social/social-app'
name: Build and Submit iOS
runs-on: macos-26-xlarge
concurrency:
group: ios-build
cancel-in-progress: false
steps:
- name: Check for EXPO_TOKEN
run: >
@@ -157,8 +157,8 @@ jobs:
name: Build and Submit iOS
runs-on: macos-26
concurrency:
group: ios-build
cancel-in-progress: false
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-build-ios
cancel-in-progress: true
needs: [bundleDeploy]
# Gotta check if its NOT '[]' because any md5 hash in the outputs is detected as a possible secret and won't be
# available here
@@ -262,7 +262,7 @@ jobs:
name: Build and Submit Android
runs-on: ubuntu-latest
concurrency:
group: android-build
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-build-android
cancel-in-progress: false
needs: [bundleDeploy]
# Gotta check if its NOT '[]' because any md5 hash in the outputs is detected as a possible secret and won't be
-54
View File
@@ -1,54 +0,0 @@
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
# prompt: 'Update the pull request description to include a summary of changes.'
# Optional: Add claude_args to customize behavior and configuration
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options
# claude_args: '--allowed-tools Bash(gh pr:*)'
# NOTE(sfn): we can add a custom system prompt here
claude_args: |
--model claude-opus-4-5-20251101
-6
View File
@@ -1,6 +0,0 @@
{
"scripts": {
"setup": "yarn install",
"run": "yarn web --port $CONDUCTOR_PORT"
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "bsky.app",
"version": "1.114.0",
"version": "1.113.1",
"private": true,
"engines": {
"node": ">=20"
@@ -73,7 +73,7 @@
"icons:optimize": "svgo -f ./assets/icons"
},
"dependencies": {
"@atproto/api": "^0.18.13",
"@atproto/api": "^0.18.8",
"@bitdrift/react-native": "^0.6.8",
"@braintree/sanitize-url": "^6.0.2",
"@bsky.app/alf": "^0.1.6",
+116
View File
@@ -18,3 +18,119 @@ 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() {
@@ -1,30 +0,0 @@
diff --git a/node_modules/react-native-pager-view/ios/RNCPagerView.m b/node_modules/react-native-pager-view/ios/RNCPagerView.m
index adfc7c6..366df60 100644
--- a/node_modules/react-native-pager-view/ios/RNCPagerView.m
+++ b/node_modules/react-native-pager-view/ios/RNCPagerView.m
@@ -498,6 +498,25 @@ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecogni
return YES;
}
+ // iOS 26+ full-screen back gesture (interactiveContentPopGestureRecognizer)
+ if (@available(iOS 26, *)) {
+ if (gestureRecognizer == self.panGestureRecognizer &&
+ otherGestureRecognizer == self.reactViewController.navigationController.interactiveContentPopGestureRecognizer) {
+ UIPanGestureRecognizer* panGestureRecognizer = (UIPanGestureRecognizer*) gestureRecognizer;
+ CGPoint velocity = [panGestureRecognizer velocityInView:self];
+ BOOL isLTR = [self isLtrLayout];
+ BOOL isBackGesture = (isLTR && velocity.x > 0) || (!isLTR && velocity.x < 0);
+
+ if (self.currentIndex == 0 && isBackGesture) {
+ self.scrollView.panGestureRecognizer.enabled = false;
+ } else {
+ self.scrollView.panGestureRecognizer.enabled = self.scrollEnabled;
+ }
+
+ return YES;
+ }
+ }
+
self.scrollView.panGestureRecognizer.enabled = self.scrollEnabled;
return NO;
}
@@ -1,11 +0,0 @@
# react-native-pager-view+6.8.0.patch
Adds support for iOS 26's `interactiveContentPopGestureRecognizer` (full-screen back gesture).
The pager already handles `RNSPanGestureRecognizer` (react-native-screens' custom full-screen gesture for pre-iOS 26) in `shouldRecognizeSimultaneouslyWithGestureRecognizer:`. It checks if the user is on the leftmost page and swiping right - if so, it disables the scrollview's pan gesture to let the back gesture through.
This patch adds the same logic for iOS 26's native `interactiveContentPopGestureRecognizer`, so the back gesture works on the leftmost page while the pager still handles swipes on other pages.
Related issues:
- https://github.com/software-mansion/react-native-screens/issues/3512
- https://github.com/software-mansion/react-native-screens/pull/3420
+1 -10
View File
@@ -177,19 +177,10 @@ export function NoAccessScreen() {
</Trans>
</Text>
{!aa.flags.isOverRegionMinAccessAge && (
<Text style={[textStyles]}>
<Trans>
Unfortunately, your declared age indicates that you
are not old enough to access Bluesky in your region.
</Trans>
</Text>
)}
{!isBlocked && birthdateUpdateText}
</View>
{aa.flags.isOverRegionMinAccessAge && <AccessSection />}
<AccessSection />
</>
) : (
<View style={[a.gap_lg]}>
-13
View File
@@ -136,15 +136,6 @@ export async function prefetchConfig() {
}
})
}
export async function refetchConfig() {
logger.debug(`refetchConfig: fetching...`)
const res = await getConfig()
qc.setQueryData<AppBskyAgeassuranceGetConfig.OutputSchema>(
configQueryKey,
res,
)
return res
}
export function useConfigQuery() {
return useQuery(
{
@@ -155,10 +146,6 @@ export function useConfigQuery() {
* @see https://tanstack.com/query/latest/docs/framework/react/guides/initial-query-data#initial-data-from-the-cache-with-initialdataupdatedat
*/
staleTime: IS_DEV ? 5e3 : 1000 * 60 * 60,
/**
* N.B. if prefetch failed above, we'll have no `initialData`, and this
* query will run on startup.
*/
initialData: getConfigFromCache(),
initialDataUpdatedAt: () =>
qc.getQueryState(configQueryKey)?.dataUpdatedAt,
+7 -21
View File
@@ -12,26 +12,23 @@ export const enabled = (IS_DEV && false) || IS_E2E
export const geolocation: Geolocation | undefined = enabled
? {
countryCode: 'BB',
countryCode: 'AA',
regionCode: undefined,
}
: undefined
const deviceGeolocationEnabled = false
export const deviceGeolocation: Geolocation | undefined =
enabled && deviceGeolocationEnabled
? {
countryCode: 'AA',
regionCode: undefined,
}
: undefined
export const deviceGeolocation: Geolocation | undefined = enabled
? {
countryCode: 'AA',
regionCode: undefined,
}
: undefined
export const config: AppBskyAgeassuranceDefs.Config = {
regions: [
{
countryCode: 'AA',
regionCode: undefined,
minAccessAge: 13,
rules: [
{
$type: ids.Default,
@@ -39,17 +36,6 @@ export const config: AppBskyAgeassuranceDefs.Config = {
},
],
},
{
countryCode: 'BB',
regionCode: undefined,
minAccessAge: 16,
rules: [
{
$type: ids.Default,
access: 'none',
},
],
},
],
}
+5 -23
View File
@@ -14,11 +14,7 @@ import {
type AgeAssuranceState,
AgeAssuranceStatus,
} from '#/ageAssurance/types'
import {
isUnderAge,
MIN_ACCESS_AGE,
useAgeAssuranceRegionConfigWithFallback,
} from '#/ageAssurance/util'
import {isUserUnderAdultAge} from '#/ageAssurance/util'
export {
prefetchConfig as prefetchAgeAssuranceConfig,
@@ -28,7 +24,6 @@ export {
usePatchServerState as usePatchAgeAssuranceServerState,
} from '#/ageAssurance/data'
export {logger} from '#/ageAssurance/logger'
export {MIN_ACCESS_AGE} from '#/ageAssurance/util'
const AgeAssuranceStateContext = createContext<{
Access: typeof AgeAssuranceAccess
@@ -37,8 +32,6 @@ const AgeAssuranceStateContext = createContext<{
flags: {
adultContentDisabled: boolean
chatDisabled: boolean
isOverRegionMinAccessAge: boolean
isOverAppMinAccessAge: boolean
}
}>({
Access: AgeAssuranceAccess,
@@ -51,8 +44,6 @@ const AgeAssuranceStateContext = createContext<{
flags: {
adultContentDisabled: false,
chatDisabled: false,
isOverRegionMinAccessAge: false,
isOverAppMinAccessAge: false,
},
})
@@ -78,7 +69,6 @@ export function Provider({children}: {children: React.ReactNode}) {
function InnerProvider({children}: {children: React.ReactNode}) {
const state = useAgeAssuranceState()
const {data} = useAgeAssuranceDataContext()
const config = useAgeAssuranceRegionConfigWithFallback()
const getAndRegisterPushToken = useGetAndRegisterPushToken()
const handleAccessUpdate = useCallback(
@@ -99,17 +89,11 @@ function InnerProvider({children}: {children: React.ReactNode}) {
<AgeAssuranceStateContext.Provider
value={useMemo(() => {
const chatDisabled = state.access !== AgeAssuranceAccess.Full
const isUnderAdultAge = data?.birthdate
? isUnderAge(data.birthdate, 18)
const isUnderage = data?.birthdate
? isUserUnderAdultAge(data.birthdate)
: true
const isOverRegionMinAccessAge = data?.birthdate
? !isUnderAge(data.birthdate, config.minAccessAge)
: false
const isOverAppMinAccessAge = data?.birthdate
? !isUnderAge(data.birthdate, MIN_ACCESS_AGE)
: false
const adultContentDisabled =
state.access !== AgeAssuranceAccess.Full || isUnderAdultAge
state.access !== AgeAssuranceAccess.Full || isUnderage
return {
Access: AgeAssuranceAccess,
Status: AgeAssuranceStatus,
@@ -117,11 +101,9 @@ function InnerProvider({children}: {children: React.ReactNode}) {
flags: {
adultContentDisabled,
chatDisabled,
isOverRegionMinAccessAge,
isOverAppMinAccessAge,
},
}
}, [state, data, config])}>
}, [state, data])}>
{children}
</AgeAssuranceStateContext.Provider>
)
+2 -9
View File
@@ -30,19 +30,12 @@ export function useAgeAssuranceState(): AgeAssuranceState {
access: AgeAssuranceAccess.Safe,
}
/**
* This can happen if the prefetch fails (such as due to network issues).
* The query handler will try it again, but if it continues to fail, of
* course we won't have config.
*
* In this case, fail open to avoid blocking users.
*/
// should never happen, but need to guard
if (!config) {
logger.warn('useAgeAssuranceState: missing config')
return {
status: AgeAssuranceStatus.Unknown,
access: AgeAssuranceAccess.Safe,
error: 'config',
access: AgeAssuranceAccess.Unknown,
}
}
-1
View File
@@ -18,7 +18,6 @@ export type AgeAssuranceState = {
lastInitiatedAt?: string
status: AgeAssuranceStatus
access: AgeAssuranceAccess
error?: 'config' // maybe other specific cases in the future
}
export function parseStatusFromString(raw: string) {
+26 -30
View File
@@ -12,23 +12,7 @@ import {useAgeAssuranceDataContext} from '#/ageAssurance/data'
import {AgeAssuranceAccess} from '#/ageAssurance/types'
import {type Geolocation, useGeolocation} from '#/geolocation'
export const MIN_ACCESS_AGE = 13
const FALLBACK_REGION_CONFIG: AppBskyAgeassuranceDefs.ConfigRegion = {
countryCode: '*',
regionCode: undefined,
minAccessAge: MIN_ACCESS_AGE,
rules: [
{
$type: ids.IfDeclaredOverAge,
age: MIN_ACCESS_AGE,
access: AgeAssuranceAccess.Full,
},
{
$type: ids.Default,
access: AgeAssuranceAccess.None,
},
],
}
const DEFAULT_MIN_AGE = 13
/**
* Get age assurance region config based on geolocation, with fallback to
@@ -46,7 +30,23 @@ export function getAgeAssuranceRegionConfigWithFallback(
regionCode: geolocation.regionCode,
})
return region || FALLBACK_REGION_CONFIG
return (
region || {
countryCode: '*',
regionCode: undefined,
rules: [
{
$type: ids.IfDeclaredOverAge,
age: DEFAULT_MIN_AGE,
access: AgeAssuranceAccess.Full,
},
{
$type: ids.Default,
access: AgeAssuranceAccess.None,
},
],
}
)
}
/**
@@ -67,14 +67,6 @@ export function useAgeAssuranceRegionConfig() {
}, [config, geolocation])
}
/**
* Hook to get the age assurance region config based on current geolocation.
* Falls back to our app defaults if no region config is found.
*/
export function useAgeAssuranceRegionConfigWithFallback() {
return useAgeAssuranceRegionConfig() || FALLBACK_REGION_CONFIG
}
/**
* Some users may have erroneously set their birth date to the current date
* if one wasn't set on their account. We previously didn't do validation on
@@ -86,11 +78,15 @@ export function isLegacyBirthdateBug(birthDate: string) {
}
/**
* Returns whether the date (converted to an age as a whole integer) is under
* the provided minimum age.
* Returns whether the user is under the minimum age required to use the app.
* This applies to all regions.
*/
export function isUnderAge(birthDate: string, age: number) {
return getAge(new Date(birthDate)) < age
export function isUserUnderMinimumAge(birthDate: string) {
return getAge(new Date(birthDate)) < DEFAULT_MIN_AGE
}
export function isUserUnderAdultAge(birthDate: string) {
return getAge(new Date(birthDate)) < 18
}
export function getBirthdateStringFromAge(age: number) {
+1 -8
View File
@@ -933,15 +933,8 @@ export function SuggestedFeeds() {
export function ProgressGuide() {
const t = useTheme()
const {gtMobile} = useBreakpoints()
return (
<View
style={[
t.atoms.border_contrast_low,
a.px_lg,
a.py_lg,
!gtMobile && {marginTop: 4},
]}>
<View style={[t.atoms.border_contrast_low, a.px_lg, a.py_lg, a.pb_lg]}>
<ProgressGuideList />
</View>
)
+2 -14
View File
@@ -8,7 +8,6 @@ import type React from 'react'
import {useCleanError} from '#/lib/hooks/useCleanError'
import {logger} from '#/logger'
import {type Shadow} from '#/state/cache/post-shadow'
import {useFeedFeedbackContext} from '#/state/feed-feedback'
import {useBookmarkMutation} from '#/state/queries/bookmarks/useBookmarkMutation'
import {useRequireAuth} from '#/state/session'
import {useTheme} from '#/alf'
@@ -33,7 +32,6 @@ export const BookmarkButton = memo(function BookmarkButton({
const {mutateAsync: bookmark} = useBookmarkMutation()
const cleanError = useCleanError()
const requireAuth = useRequireAuth()
const {feedDescriptor} = useFeedFeedbackContext()
const {viewer} = post
const isBookmarked = !!viewer?.bookmarked
@@ -52,12 +50,7 @@ export const BookmarkButton = memo(function BookmarkButton({
post,
})
logger.metric('post:bookmark', {
uri: post.uri,
authorDid: post.author.did,
logContext,
feedDescriptor,
})
logger.metric('post:bookmark', {logContext})
toast.show(
<toast.Outer>
@@ -92,12 +85,7 @@ export const BookmarkButton = memo(function BookmarkButton({
uri: post.uri,
})
logger.metric('post:unbookmark', {
uri: post.uri,
authorDid: post.author.did,
logContext,
feedDescriptor,
})
logger.metric('post:unbookmark', {logContext})
toast.show(
<toast.Outer>
@@ -98,7 +98,6 @@ let PostMenuItems = ({
richText,
threadgateRecord,
onShowLess,
logContext,
}: {
testID: string
post: Shadow<AppBskyFeedDefs.PostView>
@@ -112,7 +111,6 @@ let PostMenuItems = ({
timestamp: string
threadgateRecord?: AppBskyFeedThreadgate.Record
onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
}): React.ReactNode => {
const {hasSession, currentAccount} = useSession()
const {_} = useLingui()
@@ -212,21 +210,9 @@ let PostMenuItems = ({
try {
if (isThreadMuted) {
unmuteThread()
logger.metric('post:unmute', {
uri: postUri,
authorDid: postAuthor.did,
logContext,
feedDescriptor: feedFeedback.feedDescriptor,
})
Toast.show(_(msg`You will now receive notifications for this thread`))
} else {
muteThread()
logger.metric('post:mute', {
uri: postUri,
authorDid: postAuthor.did,
logContext,
feedDescriptor: feedFeedback.feedDescriptor,
})
Toast.show(
_(msg`You will no longer receive notifications for this thread`),
)
@@ -286,12 +272,6 @@ let PostMenuItems = ({
feedContext: postFeedContext,
reqId: postReqId,
})
logger.metric('post:showMore', {
uri: postUri,
authorDid: postAuthor.did,
logContext,
feedDescriptor: feedFeedback.feedDescriptor,
})
Toast.show(
_(msg({message: 'Feedback sent to feed operator', context: 'toast'})),
)
@@ -304,12 +284,6 @@ let PostMenuItems = ({
feedContext: postFeedContext,
reqId: postReqId,
})
logger.metric('post:showLess', {
uri: postUri,
authorDid: postAuthor.did,
logContext,
feedDescriptor: feedFeedback.feedDescriptor,
})
if (onShowLess) {
onShowLess({
item: postUri,
@@ -29,7 +29,6 @@ let PostMenuButton = ({
threadgateRecord,
onShowLess,
hitSlop,
logContext,
}: {
testID: string
post: Shadow<AppBskyFeedDefs.PostView>
@@ -42,7 +41,6 @@ let PostMenuButton = ({
threadgateRecord?: AppBskyFeedThreadgate.Record
onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void
hitSlop?: Insets
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
}): React.ReactNode => {
const {_} = useLingui()
@@ -89,7 +87,6 @@ let PostMenuButton = ({
timestamp={timestamp}
threadgateRecord={threadgateRecord}
onShowLess={onShowLess}
logContext={logContext}
/>
)}
</Menu.Root>
@@ -16,7 +16,6 @@ import {useGate} from '#/lib/statsig/statsig'
import {toShareUrl} from '#/lib/strings/url-helpers'
import {logger} from '#/logger'
import {type Shadow} from '#/state/cache/post-shadow'
import {useFeedFeedbackContext} from '#/state/feed-feedback'
import {EventStopper} from '#/view/com/util/EventStopper'
import {native} from '#/alf'
import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ArrowOutOfBoxIcon} from '#/components/icons/ArrowOutOfBox'
@@ -36,7 +35,6 @@ let ShareMenuButton = ({
threadgateRecord,
onShare,
hitSlop,
logContext,
}: {
testID: string
post: Shadow<AppBskyFeedDefs.PostView>
@@ -47,11 +45,9 @@ let ShareMenuButton = ({
threadgateRecord?: AppBskyFeedThreadgate.Record
onShare: () => void
hitSlop?: Insets
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
}): React.ReactNode => {
const {_} = useLingui()
const gate = useGate()
const {feedDescriptor} = useFeedFeedbackContext()
const ShareIcon = gate('alt_share_icon')
? ArrowShareRightIcon
@@ -69,27 +65,13 @@ let ShareMenuButton = ({
setTimeout(menuControl.open)
logger.metric(
'post:share',
{
uri: post.uri,
authorDid: post.author.did,
logContext,
feedDescriptor,
postContext: big ? 'thread' : 'feed',
},
'share:open',
{context: big ? 'thread' : 'feed'},
{statsig: true},
)
},
}),
[
menuControl,
setHasBeenOpen,
big,
logContext,
feedDescriptor,
post.uri,
post.author.did,
],
[menuControl, setHasBeenOpen, big],
)
const onNativeLongPress = () => {
+1 -19
View File
@@ -13,7 +13,6 @@ import {CountWheel} from '#/lib/custom-animations/CountWheel'
import {AnimatedLikeIcon} from '#/lib/custom-animations/LikeIcon'
import {useHaptics} from '#/lib/haptics'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {logger} from '#/logger'
import {type Shadow} from '#/state/cache/types'
import {useFeedFeedbackContext} from '#/state/feed-feedback'
import {
@@ -175,12 +174,6 @@ let PostControls = ({
feedContext,
reqId,
})
logger.metric('post:clickQuotePost', {
uri: post.uri,
authorDid: post.author.did,
logContext,
feedDescriptor,
})
openComposer({
quote: post,
onPost: onPostReply,
@@ -224,16 +217,7 @@ let PostControls = ({
testID="replyBtn"
onPress={
!replyDisabled
? () =>
requireAuth(() => {
logger.metric('post:clickReply', {
uri: post.uri,
authorDid: post.author.did,
logContext,
feedDescriptor,
})
onPressReply()
})
? () => requireAuth(() => onPressReply())
: undefined
}
label={_(
@@ -331,7 +315,6 @@ let PostControls = ({
left: secondaryControlSpacingStyles.gap / 2,
right: secondaryControlSpacingStyles.gap / 2,
}}
logContext={logContext}
/>
<PostMenuButton
testID="postDropdownBtn"
@@ -347,7 +330,6 @@ let PostControls = ({
hitSlop={{
left: secondaryControlSpacingStyles.gap / 2,
}}
logContext={logContext}
/>
</View>
</View>
@@ -389,7 +389,6 @@ let Card = ({
{data && moderationOpts ? (
status.isActive ? (
<LiveStatus
status={status}
profile={data}
embed={status.embed}
padding="lg"
+7 -12
View File
@@ -31,8 +31,8 @@ import {
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon} from '#/components/icons/Arrow'
import {MagnifyingGlass_Stroke2_Corner0_Rounded as SearchIcon} from '#/components/icons/MagnifyingGlass'
import {PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon} from '#/components/icons/Person'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {boostInterests, InterestTabs} from '#/components/InterestTabs'
import * as ProfileCard from '#/components/ProfileCard'
@@ -60,16 +60,10 @@ type Item =
key: string
}
export function FollowDialog({
guide,
showArrow,
}: {
guide: Follow10ProgressGuide
showArrow?: boolean
}) {
export function FollowDialog({guide}: {guide: Follow10ProgressGuide}) {
const {_} = useLingui()
const control = Dialog.useDialogControl()
const {gtPhone} = useBreakpoints()
const {gtMobile} = useBreakpoints()
const {height: minHeight} = useWindowDimensions()
return (
@@ -80,12 +74,13 @@ export function FollowDialog({
control.open()
logEvent('progressGuide:followDialog:open', {})
}}
size={gtPhone ? 'small' : 'large'}
color="primary">
size={gtMobile ? 'small' : 'large'}
color="primary"
variant="solid">
<ButtonIcon icon={PersonGroupIcon} />
<ButtonText>
<Trans>Find people to follow</Trans>
</ButtonText>
{showArrow && <ButtonIcon icon={ArrowRightIcon} />}
</Button>
<Dialog.Outer control={control} nativeOptions={{minHeight}}>
<Dialog.Handle />
+21 -124
View File
@@ -2,62 +2,37 @@ import {type StyleProp, View, type ViewStyle} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
import {useSession} from '#/state/session'
import {
useProgressGuide,
useProgressGuideControls,
} from '#/state/shell/progress-guide'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useBreakpoints, useLayoutBreakpoints, useTheme} from '#/alf'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import {Person_Stroke2_Corner2_Rounded as PersonIcon} from '#/components/icons/Person'
import {TimesLarge_Stroke2_Corner0_Rounded as Times} from '#/components/icons/Times'
import {Text} from '#/components/Typography'
import type * as bsky from '#/types/bsky'
import {FollowDialog} from './FollowDialog'
import {ProgressGuideTask} from './Task'
const TOTAL_AVATARS = 10
export function ProgressGuideList({style}: {style?: StyleProp<ViewStyle>}) {
const t = useTheme()
const {_} = useLingui()
const {gtPhone} = useBreakpoints()
const {rightNavVisible} = useLayoutBreakpoints()
const {currentAccount} = useSession()
const followProgressGuide = useProgressGuide('follow-10')
const followAndLikeProgressGuide = useProgressGuide('like-10-and-follow-7')
const guide = followProgressGuide || followAndLikeProgressGuide
const {endProgressGuide} = useProgressGuideControls()
const {data: follows} = useProfileFollowsQuery(currentAccount?.did, {
limit: TOTAL_AVATARS,
})
const actualFollowsCount = follows?.pages?.[0]?.follows?.length ?? 0
// Hide if user already follows 10+ people
if (guide?.guide === 'follow-10' && actualFollowsCount >= TOTAL_AVATARS) {
return null
}
// Inline layout when left nav visible but no right sidebar (800-1100px)
const inlineLayout = gtPhone && !rightNavVisible
if (guide) {
return (
<View
style={[
a.flex_col,
a.gap_md,
a.rounded_md,
t.atoms.bg_contrast_25,
a.p_lg,
style,
]}>
<View style={[a.flex_col, a.gap_md, style]}>
<View style={[a.flex_row, a.align_center, a.justify_between]}>
<Text style={[t.atoms.text, a.font_semi_bold, a.text_md]}>
<Trans>Follow 10 people to get started</Trans>
<Text
style={[
t.atoms.text_contrast_medium,
a.font_semi_bold,
a.text_sm,
{textTransform: 'uppercase'},
]}>
<Trans>Getting started</Trans>
</Text>
<Button
variant="ghost"
@@ -65,28 +40,20 @@ export function ProgressGuideList({style}: {style?: StyleProp<ViewStyle>}) {
color="secondary"
shape="round"
label={_(msg`Dismiss getting started guide`)}
onPress={endProgressGuide}
style={[a.bg_transparent, {marginTop: -6, marginRight: -6}]}>
<ButtonIcon icon={Times} size="xs" />
onPress={endProgressGuide}>
<ButtonIcon icon={Times} size="sm" />
</Button>
</View>
{guide.guide === 'follow-10' && (
<View
style={[
inlineLayout
? [
a.flex_row,
a.flex_wrap,
a.align_center,
a.justify_between,
a.gap_sm,
]
: a.flex_col,
!inlineLayout && a.gap_md,
]}>
<StackedAvatars follows={follows?.pages?.[0]?.follows} />
<FollowDialog guide={guide} showArrow={inlineLayout} />
</View>
<>
<ProgressGuideTask
current={guide.numFollows + 1}
total={10 + 1}
title={_(msg`Follow 10 accounts`)}
subtitle={_(msg`Bluesky is better with friends!`)}
/>
<FollowDialog guide={guide} />
</>
)}
{guide.guide === 'like-10-and-follow-7' && (
<>
@@ -109,73 +76,3 @@ export function ProgressGuideList({style}: {style?: StyleProp<ViewStyle>}) {
}
return null
}
function StackedAvatars({follows}: {follows?: bsky.profile.AnyProfileView[]}) {
const t = useTheme()
const {centerColumnOffset} = useLayoutBreakpoints()
// Smaller avatars for narrower viewport
const avatarSize = centerColumnOffset ? 30 : 37
const overlap = centerColumnOffset ? 9 : 11
const iconSize = centerColumnOffset ? 14 : 18
// Use actual follows count, not the guide's event counter
const followedAvatars = follows?.slice(0, TOTAL_AVATARS) ?? []
const remainingSlots = TOTAL_AVATARS - followedAvatars.length
// Total width calculation: first avatar + (remaining * visible portion)
const totalWidth = avatarSize + (TOTAL_AVATARS - 1) * (avatarSize - overlap)
return (
<View style={[a.flex_row, a.self_start, {width: totalWidth}]}>
{/* Show followed user avatars */}
{followedAvatars.map((follow, i) => (
<View
key={follow.did}
style={[
a.rounded_full,
{
marginLeft: i === 0 ? 0 : -overlap,
zIndex: TOTAL_AVATARS - i,
borderWidth: 2,
borderColor: t.atoms.bg_contrast_25.backgroundColor,
},
]}>
<UserAvatar
type="user"
size={avatarSize - 4}
avatar={follow.avatar}
/>
</View>
))}
{/* Show placeholder avatars for remaining slots */}
{Array(remainingSlots)
.fill(0)
.map((_, i) => (
<View
key={`placeholder-${i}`}
style={[
a.align_center,
a.justify_center,
a.rounded_full,
t.atoms.bg_contrast_100,
{
width: avatarSize,
height: avatarSize,
marginLeft:
followedAvatars.length === 0 && i === 0 ? 0 : -overlap,
zIndex: TOTAL_AVATARS - followedAvatars.length - i,
borderWidth: 2,
borderColor: t.atoms.bg_contrast_25.backgroundColor,
},
]}>
<PersonIcon
width={iconSize}
height={iconSize}
fill={t.atoms.text_contrast_low.color}
/>
</View>
))}
</View>
)
}
+2 -2
View File
@@ -31,11 +31,11 @@ export function ProgressGuideTask({
size={20}
thickness={3}
borderWidth={0}
unfilledColor={t.palette.contrast_100}
unfilledColor={t.palette.contrast_50}
/>
)}
<View style={[a.flex_col, a.gap_xs, subtitle && {marginTop: -2}]}>
<View style={[a.flex_col, a.gap_2xs, subtitle && {marginTop: -2}]}>
<Text
style={[
a.text_sm,
+1 -5
View File
@@ -11,9 +11,6 @@ import {RichTextTag} from '#/components/RichTextTag'
import {Text, type TextProps} from '#/components/Typography'
const WORD_WRAP = {wordWrap: 1}
// lifted from facet detection in `RichText` impl, _without_ `gm` flags
const URL_REGEX =
/(^|\s|\()((https?:\/\/[\S]+)|((?<domain>[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*))/i
export type RichTextProps = TextStyleProp &
Pick<TextProps, 'selectable' | 'onLayout' | 'onTextLayout'> & {
@@ -118,8 +115,7 @@ export function RichText({
</ProfileHoverCard>,
)
} else if (link && AppBskyRichtextFacet.validateLink(link).success) {
const isValidLink = URL_REGEX.test(link.uri)
if (!isValidLink || disableLinks) {
if (disableLinks) {
els.push(toShortUrl(segment.text))
} else {
els.push(
+9 -10
View File
@@ -20,12 +20,8 @@ export function TrendingTopic({
topic: raw,
size,
style,
hovered,
}: {
topic: TrendingTopic
size?: 'large' | 'small'
hovered?: boolean
} & ViewStyleProp) {
}: {topic: TrendingTopic; size?: 'large' | 'small'} & ViewStyleProp) {
const t = useTheme()
const topic = useTopic(raw)
const isSmall = size === 'small'
@@ -37,14 +33,18 @@ export function TrendingTopic({
style={[
a.flex_row,
a.align_center,
a.rounded_full,
a.border,
t.atoms.border_contrast_medium,
t.atoms.bg,
isSmall
? [
{
paddingVertical: 2,
paddingHorizontal: 4,
paddingVertical: 5,
paddingHorizontal: 10,
},
]
: [a.py_xs, a.px_sm],
: [a.py_sm, a.px_md],
hasIcon && {gap: 6},
style,
]}>
@@ -93,7 +93,6 @@ export function TrendingTopic({
a.font_semi_bold,
a.leading_tight,
isSmall ? [a.text_sm] : [a.text_md, {paddingBottom: 1}],
hovered && {textDecorationLine: 'underline'},
]}
numberOfLines={1}>
{topic.displayName}
@@ -8,7 +8,6 @@ import {atoms as a, useBreakpoints, useTheme, type ViewStyleProp} from '#/alf'
import {Admonition} from '#/components/Admonition'
import {AgeAssuranceAppealDialog} from '#/components/ageAssurance/AgeAssuranceAppealDialog'
import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge'
import {AgeAssuranceConfigUnavailableError} from '#/components/ageAssurance/AgeAssuranceErrors'
import {
AgeAssuranceInitDialog,
useDialogControl,
@@ -28,13 +27,6 @@ import {useDeviceGeolocationApi} from '#/geolocation'
export function AgeAssuranceAccountCard({style}: ViewStyleProp & {}) {
const aa = useAgeAssurance()
if (aa.state.access === aa.Access.Full) return null
if (aa.state.error === 'config') {
return (
<View style={style}>
<AgeAssuranceConfigUnavailableError />
</View>
)
}
return <Inner style={style} />
}
@@ -3,7 +3,6 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {atoms as a, select, useTheme, type ViewStyleProp} from '#/alf'
import {AgeAssuranceConfigUnavailableError} from '#/components/ageAssurance/AgeAssuranceErrors'
import {useDialogControl} from '#/components/ageAssurance/AgeAssuranceInitDialog'
import type * as Dialog from '#/components/Dialog'
import {ShieldCheck_Stroke2_Corner0_Rounded as Shield} from '#/components/icons/Shield'
@@ -20,9 +19,6 @@ export function AgeAssuranceAdmonition({
const aa = useAgeAssurance()
if (aa.state.access === aa.Access.Full) return null
if (aa.state.error === 'config') {
return <AgeAssuranceConfigUnavailableError style={style} />
}
return (
<Inner style={style} control={control}>
@@ -23,7 +23,6 @@ export function useInternalState() {
const visible = useMemo(() => {
if (aa.state.access === aa.Access.Full) return false
if (aa.state.lastInitiatedAt) return false
if (aa.state.error === 'config') return false
if (hidden) return false
if (nux && nux.completed) return false
return true
@@ -5,7 +5,6 @@ import {useLingui} from '@lingui/react'
import {Nux, useNux, useSaveNux} from '#/state/queries/nuxs'
import {atoms as a, type ViewStyleProp} from '#/alf'
import {AgeAssuranceAdmonition} from '#/components/ageAssurance/AgeAssuranceAdmonition'
import {AgeAssuranceConfigUnavailableError} from '#/components/ageAssurance/AgeAssuranceErrors'
import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy'
import {Button, ButtonIcon} from '#/components/Button'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
@@ -27,37 +26,33 @@ export function AgeAssuranceDismissibleNotice({style}: ViewStyleProp & {}) {
return (
<View style={style}>
{aa.state.error === 'config' ? (
<AgeAssuranceConfigUnavailableError />
) : (
<View>
<AgeAssuranceAdmonition>{copy.notice}</AgeAssuranceAdmonition>
<View>
<AgeAssuranceAdmonition>{copy.notice}</AgeAssuranceAdmonition>
<Button
label={_(msg`Don't show again`)}
size="tiny"
variant="solid"
color="secondary_inverted"
shape="round"
onPress={() => {
save({
id: Nux.AgeAssuranceDismissibleNotice,
completed: true,
data: undefined,
})
logger.metric('ageAssurance:dismissSettingsNotice', {})
}}
style={[
a.absolute,
{
top: 12,
right: 12,
},
]}>
<ButtonIcon icon={X} />
</Button>
</View>
)}
<Button
label={_(msg`Don't show again`)}
size="tiny"
variant="solid"
color="secondary_inverted"
shape="round"
onPress={() => {
save({
id: Nux.AgeAssuranceDismissibleNotice,
completed: true,
data: undefined,
})
logger.metric('ageAssurance:dismissSettingsNotice', {})
}}
style={[
a.absolute,
{
top: 12,
right: 12,
},
]}>
<ButtonIcon icon={X} />
</Button>
</View>
</View>
)
}
@@ -1,37 +0,0 @@
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {type ViewStyleProp} from '#/alf'
import * as Admonition from '#/components/Admonition'
import {ButtonIcon, ButtonText} from '#/components/Button'
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as RetryIcon} from '#/components/icons/ArrowRotate'
import {refetchConfig} from '#/ageAssurance/data'
export function AgeAssuranceConfigUnavailableError(props: ViewStyleProp) {
const {_} = useLingui()
return (
<Admonition.Outer type="error" style={props.style}>
<Admonition.Row>
<Admonition.Icon />
<Admonition.Content>
<Admonition.Text>
<Trans>
We were unable to load the age assurance configuration for your
region, probably due to a network error. Some content and features
may be unavailable temporarily. Please try again later.
</Trans>
</Admonition.Text>
</Admonition.Content>
<Admonition.Button
color="negative_subtle"
label={_(msg`Retry`)}
onPress={() => refetchConfig().catch(() => {})}>
<ButtonText>
<Trans>Retry</Trans>
</ButtonText>
<ButtonIcon icon={RetryIcon} />
</Admonition.Button>
</Admonition.Row>
</Admonition.Outer>
)
}
@@ -5,7 +5,6 @@ import {useLingui} from '@lingui/react'
import {atoms as a} from '#/alf'
import {Admonition} from '#/components/Admonition'
import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge'
import {AgeAssuranceConfigUnavailableError} from '#/components/ageAssurance/AgeAssuranceErrors'
import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy'
import {ButtonIcon, ButtonText} from '#/components/Button'
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
@@ -45,12 +44,6 @@ export function AgeRestrictedScreen({
</Layout.Header.Outer>
<Layout.Content>
<View style={[a.p_lg]}>
{aa.state.error === 'config' && (
<View style={[a.pb_lg]}>
<AgeAssuranceConfigUnavailableError />
</View>
)}
<View style={[a.align_start, a.pb_lg]}>
<AgeAssuranceBadge />
</View>
-7
View File
@@ -3,7 +3,6 @@ import {createContext, useContext, useMemo, useState} from 'react'
import {type AgeAssuranceRedirectDialogState} from '#/components/ageAssurance/AgeAssuranceRedirectDialog'
import * as Dialog from '#/components/Dialog'
import {type Screen} from '#/components/dialogs/EmailDialog/types'
import {type ReportSubject} from '#/components/moderation/ReportDialog'
type Control = Dialog.DialogControlProps
@@ -25,7 +24,6 @@ type ControlsContext = {
share?: boolean
}>
ageAssuranceRedirectDialogControl: StatefulControl<AgeAssuranceRedirectDialogState>
reportDialogControl: StatefulControl<{subject: ReportSubject}>
}
const ControlsContext = createContext<ControlsContext | null>(null)
@@ -53,9 +51,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
}>()
const ageAssuranceRedirectDialogControl =
useStatefulDialogControl<AgeAssuranceRedirectDialogState>()
const reportDialogControl = useStatefulDialogControl<{
subject: ReportSubject
}>()
const ctx = useMemo<ControlsContext>(
() => ({
@@ -65,7 +60,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
emailDialogControl,
linkWarningDialogControl,
ageAssuranceRedirectDialogControl,
reportDialogControl,
}),
[
mutedWordsDialogControl,
@@ -74,7 +68,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
emailDialogControl,
linkWarningDialogControl,
ageAssuranceRedirectDialogControl,
reportDialogControl,
],
)
+2 -1
View File
@@ -99,9 +99,10 @@ export function Inner() {
<View style={[a.py_lg]}>
<Text
style={[
t.atoms.text_contrast_medium,
t.atoms.text,
a.text_sm,
a.font_semi_bold,
{opacity: 0.7}, // NOTE: we use opacity 0.7 instead of a color to match the color of the home pager tab bar
]}>
{topic.topic}
</Text>
@@ -1,147 +0,0 @@
import {useCallback, useState} from 'react'
import {View} from 'react-native'
import {type AppBskyActorDefs, ToolsOzoneReportDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useMutation} from '@tanstack/react-query'
import {BLUESKY_MOD_SERVICE_HEADERS} from '#/lib/constants'
import {logger} from '#/logger'
import {useAgent} from '#/state/session'
import {atoms as a, web} from '#/alf'
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 {Text} from '#/components/Typography'
export function GoLiveDisabledDialog({
control,
status,
}: {
control: Dialog.DialogControlProps
status: AppBskyActorDefs.StatusView
}) {
return (
<Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
<Dialog.Handle />
<DialogInner control={control} status={status} />
</Dialog.Outer>
)
}
export function DialogInner({
control,
status,
}: {
control: Dialog.DialogControlProps
status: AppBskyActorDefs.StatusView
}) {
const {_} = useLingui()
const agent = useAgent()
const [details, setDetails] = useState('')
const {mutate, isPending} = useMutation({
mutationFn: async () => {
if (!agent.session?.did) {
throw new Error('Not logged in')
}
if (!status.uri || !status.cid) {
throw new Error('Status is missing uri or cid')
}
if (__DEV__) {
logger.info('Submitting go live appeal', {
details,
})
} else {
await agent.createModerationReport(
{
reasonType: ToolsOzoneReportDefs.REASONAPPEAL,
subject: {
$type: 'com.atproto.repo.strongRef',
uri: status.uri,
cid: status.cid,
},
reason: details,
},
{
encoding: 'application/json',
headers: BLUESKY_MOD_SERVICE_HEADERS,
},
)
}
},
onError: () => {
Toast.show(_(msg`Failed to submit appeal, please try again.`), {
type: 'error',
})
},
onSuccess: () => {
control.close()
Toast.show(_(msg({message: 'Appeal submitted', context: 'toast'})), {
type: 'success',
})
},
})
const onSubmit = useCallback(() => mutate(), [mutate])
return (
<Dialog.ScrollableInner
label={_(msg`Appeal livestream suspension`)}
style={[web({maxWidth: 400})]}>
<View style={[a.gap_lg]}>
<View style={[a.gap_md]}>
<Text
style={[
a.flex_1,
a.text_2xl,
a.font_semi_bold,
a.leading_snug,
a.pr_4xl,
]}>
<Trans>Going live is currently disabled for your account</Trans>
</Text>
<Text style={[a.text_md, a.leading_snug]}>
<Trans>
You are currently blocked from using the Go Live feature. To
appeal this moderation decision, please submit the form below.
</Trans>
</Text>
<Text style={[a.text_md, a.leading_snug]}>
<Trans>
This appeal will be sent to Bluesky's moderation service.
</Trans>
</Text>
</View>
<View style={[a.gap_md]}>
<Dialog.Input
label={_(msg`Text input field`)}
placeholder={_(
msg`Please explain why you think your Go Live access was incorrectly disabled.`,
)}
value={details}
onChangeText={setDetails}
autoFocus={true}
numberOfLines={3}
multiline
maxLength={300}
/>
<Button
testID="submitBtn"
variant="solid"
color="primary"
size="large"
onPress={onSubmit}
label={_(msg`Submit`)}>
<ButtonText>{_(msg`Submit`)}</ButtonText>
{isPending && <ButtonIcon icon={Loader} />}
</Button>
</View>
</View>
<Dialog.Close />
</Dialog.ScrollableInner>
)
}
+8 -52
View File
@@ -17,9 +17,6 @@ import {unstableCacheProfileView} from '#/state/queries/profile'
import {android, atoms as a, platform, tokens, useTheme, web} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon} from '#/components/icons/CircleInfo'
import {createStaticClick, SimpleInlineLinkText} from '#/components/Link'
import {useGlobalReportDialogControl} from '#/components/moderation/ReportDialog'
import * as ProfileCard from '#/components/ProfileCard'
import {Text} from '#/components/Typography'
import type * as bsky from '#/types/bsky'
@@ -31,7 +28,6 @@ export function LiveStatusDialog({
control,
profile,
embed,
status,
}: {
control: Dialog.DialogControlProps
profile: bsky.profile.AnyProfileView
@@ -42,12 +38,7 @@ export function LiveStatusDialog({
return (
<Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
<Dialog.Handle difference={!!embed.external.thumb} />
<DialogInner
status={status}
profile={profile}
embed={embed}
navigation={navigation}
/>
<DialogInner profile={profile} embed={embed} navigation={navigation} />
</Dialog.Outer>
)
}
@@ -56,12 +47,10 @@ function DialogInner({
profile,
embed,
navigation,
status,
}: {
profile: bsky.profile.AnyProfileView
embed: AppBskyEmbedExternal.View
navigation: NavigationProp
status: AppBskyActorDefs.StatusView
}) {
const {_} = useLingui()
const control = Dialog.useDialogContext()
@@ -80,7 +69,6 @@ function DialogInner({
contentContainerStyle={[a.pt_0, a.px_0]}
style={[web({maxWidth: 420}), a.overflow_hidden]}>
<LiveStatus
status={status}
profile={profile}
embed={embed}
onPressOpenProfile={onPressOpenProfile}
@@ -91,13 +79,11 @@ function DialogInner({
}
export function LiveStatus({
status,
profile,
embed,
padding = 'xl',
onPressOpenProfile,
}: {
status: AppBskyActorDefs.StatusView
profile: bsky.profile.AnyProfileView
embed: AppBskyEmbedExternal.View
padding?: 'lg' | 'xl'
@@ -108,8 +94,6 @@ export function LiveStatus({
const queryClient = useQueryClient()
const openLink = useOpenLink()
const moderationOpts = useModerationOpts()
const reportDialogControl = useGlobalReportDialogControl()
const dialogContext = Dialog.useDialogContext()
return (
<>
@@ -221,43 +205,15 @@ export function LiveStatus({
</Button>
</ProfileCard.Header>
)}
<View
<Text
style={[
a.flex_row,
a.align_center,
a.justify_between,
a.flex_1,
a.pt_sm,
a.w_full,
a.text_center,
t.atoms.text_contrast_low,
a.text_sm,
]}>
<View style={[a.flex_row, a.align_center, a.gap_xs, a.flex_1]}>
<CircleInfoIcon size="sm" fill={t.atoms.text_contrast_low.color} />
<Text style={[t.atoms.text_contrast_low, a.text_sm]}>
<Trans>Live feature is in beta</Trans>
</Text>
</View>
{status && (
<SimpleInlineLinkText
label={_(msg`Report this livestream`)}
{...createStaticClick(() => {
function open() {
reportDialogControl.open({
subject: {
...status,
$type: 'app.bsky.actor.defs#statusView',
},
})
}
if (dialogContext.isWithinDialog) {
dialogContext.close(open)
} else {
open()
}
})}
style={[a.text_sm, a.underline, t.atoms.text_contrast_medium]}>
<Trans>Report</Trans>
</SimpleInlineLinkText>
)}
</View>
<Trans>Live feature is in beta testing</Trans>
</Text>
</View>
</>
)
@@ -70,7 +70,6 @@ export function useSubmitReportMutation() {
}
break
}
case 'status':
case 'post':
case 'list':
case 'feed':
@@ -3,8 +3,6 @@ import {
ToolsOzoneReportDefs as OzoneReportDefs,
} from '@atproto/api'
import {type ParsedReportSubject} from '#/components/moderation/ReportDialog/types'
export const DMCA_LINK = 'https://bsky.social/about/support/copyright'
export const SUPPORT_PAGE = 'https://bsky.social/about/support'
@@ -114,10 +112,3 @@ export const BSKY_LABELER_ONLY_REPORT_REASONS: Set<OzoneReportDefs.ReasonType> =
OzoneReportDefs.REASONCHILDSAFETYOTHER,
OzoneReportDefs.REASONVIOLENCEEXTREMISTCONTENT,
])
/**
* Set of _parsed_ subject types that should only be sent to Bluesky's
* moderation service.
*/
export const BSKY_LABELER_ONLY_SUBJECT_TYPES: Set<ParsedReportSubject['type']> =
new Set(['convoMessage', 'status'])
@@ -14,12 +14,6 @@ export function useCopyForSubject(subject: ParsedReportSubject) {
subtitle: _(msg`Why should this user be reviewed?`),
}
}
case 'status': {
return {
title: _(msg`Report this livestream`),
subtitle: _(msg`Why should this livestream be reviewed?`),
}
}
case 'post': {
return {
title: _(msg`Report this post`),
@@ -16,7 +16,6 @@ import {atoms as a, useGutters, useTheme} from '#/alf'
import * as Admonition from '#/components/Admonition'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
import {useDelayedLoading} from '#/components/hooks/useDelayedLoading'
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as Retry} from '#/components/icons/ArrowRotate'
import {
@@ -32,7 +31,6 @@ import {Text} from '#/components/Typography'
import {useSubmitReportMutation} from './action'
import {
BSKY_LABELER_ONLY_REPORT_REASONS,
BSKY_LABELER_ONLY_SUBJECT_TYPES,
NEW_TO_OLD_REASONS_MAP,
SUPPORT_PAGE,
} from './const'
@@ -46,27 +44,17 @@ import {
useReportOptions,
} from './utils/useReportOptions'
export {type ReportSubject} from './types'
export {useDialogControl as useReportDialogControl} from '#/components/Dialog'
export function useGlobalReportDialogControl() {
return useGlobalDialogsControlContext().reportDialogControl
}
const logger = Logger.create(Logger.Context.ReportDialog)
export function GlobalReportDialog() {
const {value, control} = useGlobalReportDialogControl()
return <ReportDialog control={control} subject={value?.subject} />
}
export function ReportDialog(
props: Omit<ReportDialogProps, 'subject'> & {
subject?: ReportSubject
subject: ReportSubject
},
) {
const subject = React.useMemo(
() => (props.subject ? parseReportSubject(props.subject) : undefined),
() => parseReportSubject(props.subject),
[props.subject],
)
const onClose = React.useCallback(() => {
@@ -128,10 +116,8 @@ function Inner(props: ReportDialogProps) {
const isBskyOnlyReason = state?.selectedOption?.reason
? BSKY_LABELER_ONLY_REPORT_REASONS.has(state.selectedOption.reason)
: false
// some subjects ONLY go to Bluesky
const isBskyOnlySubject = BSKY_LABELER_ONLY_SUBJECT_TYPES.has(
props.subject.type,
)
// some subjects (chats) only go to Bluesky
const isBskyOnlySubject = props.subject.type === 'convoMessage'
/**
* Labelers that support this `subject` and its NSID collection
@@ -838,7 +824,12 @@ function LabelerCard({
{title}
</Text>
<Text
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
style={[
a.text_sm,
,
a.leading_snug,
t.atoms.text_contrast_medium,
]}>
<Trans>By {sanitizeHandle(labeler.creator.handle, '@')}</Trans>
</Text>
</View>
@@ -18,7 +18,6 @@ export type ReportSubject =
| $Typed<AppBskyActorDefs.ProfileViewBasic>
| $Typed<AppBskyActorDefs.ProfileView>
| $Typed<AppBskyActorDefs.ProfileViewDetailed>
| $Typed<AppBskyActorDefs.StatusView>
| $Typed<AppBskyGraphDefs.ListView>
| $Typed<AppBskyFeedDefs.GeneratorView>
| $Typed<AppBskyGraphDefs.StarterPackView>
@@ -39,12 +38,6 @@ export type ParsedReportSubject =
quote: boolean
}
}
| {
type: 'status'
uri: string
cid: string
nsid: string
}
| {
type: 'list'
uri: string
@@ -33,14 +33,6 @@ export function parseReportSubject(
did: subject.did,
nsid: 'app.bsky.actor.profile',
}
} else if (AppBskyActorDefs.isStatusView(subject)) {
if (!subject.uri || !subject.cid) return
return {
type: 'status',
uri: subject.uri,
cid: subject.cid,
nsid: 'app.bsky.actor.status',
}
} else if (AppBskyGraphDefs.isListView(subject)) {
return {
type: 'list',
+6 -12
View File
@@ -5,19 +5,13 @@ import {type Geolocation} from '#/geolocation/types'
const localEnabled = false
export const enabled = IS_DEV && (localEnabled || aaDebug.geolocation)
export const geolocation: Geolocation = aaDebug.geolocation ?? {
countryCode: 'US',
regionCode: 'TX',
countryCode: 'AU',
regionCode: undefined,
}
export const deviceGeolocation: Geolocation = aaDebug.deviceGeolocation ?? {
countryCode: 'AU',
regionCode: undefined,
}
const deviceLocalEnabled = false
export const deviceGeolocation: Geolocation | undefined =
aaDebug.deviceGeolocation ||
(deviceLocalEnabled
? {
countryCode: 'US',
regionCode: 'TX',
}
: undefined)
export async function resolve<T>(data: T) {
await new Promise(y => setTimeout(y, 500)) // simulate network
+1 -20
View File
@@ -1,5 +1,4 @@
import {useCallback, useEffect, useRef} from 'react'
import {Platform} from 'react-native'
import * as Location from 'expo-location'
import {createPermissionHook} from 'expo-modules-core'
@@ -47,8 +46,7 @@ const useForegroundPermissions = createPermissionHook({
})
export async function getDeviceGeolocation(): Promise<Geolocation> {
if (debug.enabled && debug.deviceGeolocation)
return debug.resolve(debug.deviceGeolocation)
if (debug.enabled) return debug.resolve(debug.deviceGeolocation)
try {
const geocode = await Location.getCurrentPositionAsync()
@@ -58,18 +56,6 @@ export async function getDeviceGeolocation(): Promise<Geolocation> {
})
const location = locations.at(0)
const normalized = location ? normalizeDeviceLocation(location) : undefined
if (normalized?.regionCode && normalized.regionCode.length > 5) {
/*
* We want short codes only, and we're still seeing some full names here.
* 5 is just a heuristic for a region that is probably not formatted as a
* short code.
*/
logger.error('getDeviceGeolocation: invalid regionCode', {
os: Platform.OS,
version: Platform.Version,
regionCode: normalized.regionCode,
})
}
return {
countryCode: normalized?.countryCode ?? undefined,
regionCode: normalized?.regionCode ?? undefined,
@@ -156,8 +142,3 @@ export function useSyncDeviceGeolocationOnStartup(
})
}, [status, sync])
}
export function useIsDeviceGeolocationGranted() {
const [status] = useForegroundPermissions()
return status?.granted === true
}
+1 -4
View File
@@ -12,10 +12,7 @@ import {type Geolocation} from '#/geolocation/types'
import {mergeGeolocations} from '#/geolocation/util'
import {device, useStorage} from '#/storage'
export {
useIsDeviceGeolocationGranted,
useRequestDeviceGeolocation,
} from '#/geolocation/device'
export {useRequestDeviceGeolocation} from '#/geolocation/device'
export {resolve} from '#/geolocation/service'
export * from '#/geolocation/types'
+8 -21
View File
@@ -19,27 +19,15 @@ export function useActorStatus(actor?: bsky.profile.AnyProfileView) {
return useMemo(() => {
tick! // revalidate every minute
if (shadowed && 'status' in shadowed && shadowed.status) {
const isValid = validateStatus(shadowed.did, shadowed.status, config)
const isDisabled = shadowed.status.isDisabled || false
const isActive = isStatusStillActive(shadowed.status.expiresAt)
if (isValid && !isDisabled && isActive) {
return {
uri: shadowed.status.uri,
cid: shadowed.status.cid,
isDisabled: false,
isActive: true,
status: 'app.bsky.actor.status#live',
embed: shadowed.status.embed as $Typed<AppBskyEmbedExternal.View>, // temp_isStatusValid asserts this
expiresAt: shadowed.status.expiresAt!, // isStatusStillActive asserts this
record: shadowed.status.record,
} satisfies AppBskyActorDefs.StatusView
}
if (
shadowed &&
'status' in shadowed &&
shadowed.status &&
validateStatus(shadowed.did, shadowed.status, config) &&
isStatusStillActive(shadowed.status.expiresAt)
) {
return {
uri: shadowed.status.uri,
cid: shadowed.status.cid,
isDisabled,
isActive: false,
isActive: true,
status: 'app.bsky.actor.status#live',
embed: shadowed.status.embed as $Typed<AppBskyEmbedExternal.View>, // temp_isStatusValid asserts this
expiresAt: shadowed.status.expiresAt!, // isStatusStillActive asserts this
@@ -48,7 +36,6 @@ export function useActorStatus(actor?: bsky.profile.AnyProfileView) {
} else {
return {
status: '',
isDisabled: false,
isActive: false,
record: {},
} satisfies AppBskyActorDefs.StatusView
File diff suppressed because it is too large Load Diff
+9 -77
View File
@@ -176,19 +176,13 @@ export type MetricEvents = {
'feed:suggestion:press': {
feedUrl: string
}
'post:showMore': {
uri: string
authorDid: string
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
feedDescriptor?: string
position?: number
'feed:showMore': {
feed: string
feedContext: string
}
'post:showLess': {
uri: string
authorDid: string
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
feedDescriptor?: string
position?: number
'feed:showLess': {
feed: string
feedContext: string
}
'feed:clickthrough': {
feed: string
@@ -263,70 +257,15 @@ export type MetricEvents = {
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
feedDescriptor?: string
}
'post:mute': {
uri: string
authorDid: string
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
feedDescriptor?: string
position?: number
}
'post:unmute': {
uri: string
authorDid: string
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
feedDescriptor?: string
position?: number
}
'post:mute': {}
'post:unmute': {}
'post:pin': {}
'post:unpin': {}
'post:bookmark': {
uri: string
authorDid: string
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
feedDescriptor?: string
position?: number
}
'post:unbookmark': {
uri: string
authorDid: string
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
feedDescriptor?: string
position?: number
}
'post:clickReply': {
uri: string
authorDid: string
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
feedDescriptor?: string
position?: number
}
'post:clickQuotePost': {
uri: string
authorDid: string
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
feedDescriptor?: string
position?: number
}
'post:clickthroughAuthor': {
uri: string
authorDid: string
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
feedDescriptor?: string
position?: number
}
'post:clickthroughItem': {
uri: string
authorDid: string
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
feedDescriptor?: string
position?: number
}
'post:clickthroughEmbed': {
uri: string
authorDid: string
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
feedDescriptor?: string
position?: number
}
'post:view': {
uri: string
@@ -626,14 +565,7 @@ export type MetricEvents = {
'live:view:profile': {subject: string}
'live:view:post': {subject: string; feed?: string}
'post:share': {
uri: string
authorDid: string
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
feedDescriptor?: string
postContext: 'feed' | 'thread'
position?: number
}
'share:open': {context: 'feed' | 'thread'}
'share:press:copyLink': {}
'share:press:nativeShare': {}
'share:press:openDmSearch': {}
@@ -281,12 +281,6 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
])
const onOpenAuthor = () => {
logger.metric('post:clickthroughAuthor', {
uri: post.uri,
authorDid: post.author.did,
logContext: 'PostThreadItem',
feedDescriptor: feedFeedback.feedDescriptor,
})
if (postSource) {
feedFeedback.sendInteraction({
item: post.uri,
@@ -298,12 +292,6 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
}
const onOpenEmbed = () => {
logger.metric('post:clickthroughEmbed', {
uri: post.uri,
authorDid: post.author.did,
logContext: 'PostThreadItem',
feedDescriptor: feedFeedback.feedDescriptor,
})
if (postSource) {
feedFeedback.sendInteraction({
item: post.uri,
+28 -3
View File
@@ -11,8 +11,12 @@ import {Text} from '#/components/Typography'
export const Policies = ({
serviceDescription,
needsGuardian,
under13,
}: {
serviceDescription: ComAtprotoServerDescribeServer.OutputSchema
needsGuardian: boolean
under13: boolean
}) => {
const t = useTheme()
const {_} = useLingui()
@@ -87,9 +91,30 @@ export const Policies = ({
return null
}
return els ? (
<Text style={[a.leading_snug, t.atoms.text_contrast_medium]}>{els}</Text>
) : null
return (
<View style={[a.gap_sm]}>
{els ? (
<Text style={[a.leading_snug, t.atoms.text_contrast_medium]}>
{els}
</Text>
) : null}
{under13 ? (
<Admonition type="error">
<Trans>
You must be 13 years of age or older to create an account.
</Trans>
</Admonition>
) : needsGuardian ? (
<Admonition type="warning">
<Trans>
If you are not yet an adult according to the laws of your country,
your parent or legal guardian must read these Terms on your behalf.
</Trans>
</Admonition>
) : undefined}
</View>
)
}
function validWebLink(url?: string): string | undefined {
+8 -100
View File
@@ -7,13 +7,9 @@ import type tldts from 'tldts'
import {isEmailMaybeInvalid} from '#/lib/strings/email'
import {logger} from '#/logger'
import {isNative} from '#/platform/detection'
import {useSignupContext} from '#/screens/Signup/state'
import {is13, is18, useSignupContext} from '#/screens/Signup/state'
import {Policies} from '#/screens/Signup/StepInfo/Policies'
import {atoms as a, native} from '#/alf'
import * as Admonition from '#/components/Admonition'
import * as Dialog from '#/components/Dialog'
import {DeviceLocationRequestDialog} from '#/components/dialogs/DeviceLocationRequestDialog'
import * as DateField from '#/components/forms/DateField'
import {type DateFieldRef} from '#/components/forms/DateField/types'
import {FormError} from '#/components/forms/FormError'
@@ -22,19 +18,8 @@ import * as TextField from '#/components/forms/TextField'
import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '#/components/icons/Envelope'
import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock'
import {Ticket_Stroke2_Corner0_Rounded as Ticket} from '#/components/icons/Ticket'
import {createStaticClick, SimpleInlineLinkText} from '#/components/Link'
import {Loader} from '#/components/Loader'
import {usePreemptivelyCompleteActivePolicyUpdate} from '#/components/PolicyUpdateOverlay/usePreemptivelyCompleteActivePolicyUpdate'
import * as Toast from '#/components/Toast'
import {
isUnderAge,
MIN_ACCESS_AGE,
useAgeAssuranceRegionConfigWithFallback,
} from '#/ageAssurance/util'
import {
useDeviceGeolocationApi,
useIsDeviceGeolocationGranted,
} from '#/geolocation'
import {BackNextButtons} from '../BackNextButtons'
function sanitizeDate(date: Date): Date {
@@ -72,20 +57,6 @@ export function StepInfo({
const passwordInputRef = useRef<TextInput>(null)
const birthdateInputRef = useRef<DateFieldRef>(null)
const aaRegionConfig = useAgeAssuranceRegionConfigWithFallback()
const {setDeviceGeolocation} = useDeviceGeolocationApi()
const locationControl = Dialog.useDialogControl()
const isOverRegionMinAccessAge = state.dateOfBirth
? !isUnderAge(state.dateOfBirth.toISOString(), aaRegionConfig.minAccessAge)
: true
const isOverAppMinAccessAge = state.dateOfBirth
? !isUnderAge(state.dateOfBirth.toISOString(), MIN_ACCESS_AGE)
: true
const isOverMinAdultAge = state.dateOfBirth
? !isUnderAge(state.dateOfBirth.toISOString(), 18)
: true
const isDeviceGeolocationGranted = useIsDeviceGeolocationGranted()
const [hasWarnedEmail, setHasWarnedEmail] = React.useState<boolean>(false)
const tldtsRef = React.useRef<typeof tldts>(undefined)
@@ -105,7 +76,7 @@ export function StepInfo({
const emailChanged = prevEmailValueRef.current !== email
const password = passwordValueRef.current
if (!isOverRegionMinAccessAge) {
if (!is13(state.dateOfBirth)) {
return
}
@@ -303,79 +274,16 @@ export function StepInfo({
maximumDate={new Date()}
/>
</View>
<View style={[a.gap_sm]}>
<Policies serviceDescription={state.serviceDescription} />
{!isOverRegionMinAccessAge || !isOverAppMinAccessAge ? (
<Admonition.Outer type="error">
<Admonition.Row>
<Admonition.Icon />
<Admonition.Content>
<Admonition.Text>
{!isOverAppMinAccessAge ? (
<Trans>
You must be {MIN_ACCESS_AGE} years of age or older
to create an account.
</Trans>
) : (
<Trans>
You must be {aaRegionConfig.minAccessAge} years of
age or older to create an account in your region.
</Trans>
)}
</Admonition.Text>
{isNative &&
!isDeviceGeolocationGranted &&
isOverAppMinAccessAge && (
<Admonition.Text>
<Trans>
Have we got your location wrong?{' '}
<SimpleInlineLinkText
label={_(
msg`Tap here to confirm your location with GPS.`,
)}
{...createStaticClick(() => {
locationControl.open()
})}>
Tap here to confirm your location with GPS.
</SimpleInlineLinkText>
</Trans>
</Admonition.Text>
)}
</Admonition.Content>
</Admonition.Row>
</Admonition.Outer>
) : !isOverMinAdultAge ? (
<Admonition.Admonition type="warning">
<Trans>
If you are not yet an adult according to the laws of your
country, your parent or legal guardian must read these Terms
on your behalf.
</Trans>
</Admonition.Admonition>
) : undefined}
</View>
{isNative && (
<DeviceLocationRequestDialog
control={locationControl}
onLocationAcquired={props => {
props.closeDialog(() => {
// set this after close!
setDeviceGeolocation(props.geolocation)
Toast.show(_(msg`Your location has been updated.`), {
type: 'success',
})
})
}}
/>
)}
<Policies
serviceDescription={state.serviceDescription}
needsGuardian={!is18(state.dateOfBirth)}
under13={!is13(state.dateOfBirth)}
/>
</>
) : undefined}
</View>
<BackNextButtons
hideNext={!isOverRegionMinAccessAge}
hideNext={!is13(state.dateOfBirth)}
showRetry={isServerError}
isLoading={state.isLoading}
onBackPress={onPressBack}
+20 -1
View File
@@ -137,6 +137,7 @@ export function useFeedFeedback(
sendOrAggregateInteractionsForStats(
aggregatedStats.current,
interactionsToSend,
feed?.feedDescriptor ?? 'unknown',
)
throttledFlushAggregatedStats()
logger.debug('flushed')
@@ -273,10 +274,28 @@ function createAggregatedStats(): AggregatedStats {
function sendOrAggregateInteractionsForStats(
stats: AggregatedStats,
interactions: AppBskyFeedDefs.Interaction[],
feed: string,
) {
for (let interaction of interactions) {
switch (interaction.event) {
// The events are aggregated and sent later in batches.
// Pressing "Show more" / "Show less" is relatively uncommon so we won't aggregate them.
// This lets us send the feed context together with them.
case 'app.bsky.feed.defs#requestLess': {
logger.metric('feed:showLess', {
feed,
feedContext: interaction.feedContext ?? '',
})
break
}
case 'app.bsky.feed.defs#requestMore': {
logger.metric('feed:showMore', {
feed,
feedContext: interaction.feedContext ?? '',
})
break
}
// The rest of the events are aggregated and sent later in batches.
case 'app.bsky.feed.defs#clickthroughAuthor':
case 'app.bsky.feed.defs#clickthroughEmbed':
case 'app.bsky.feed.defs#clickthroughItem':
+2
View File
@@ -373,6 +373,7 @@ function useThreadMuteMutation() {
{uri: string} // the root post's uri
>({
mutationFn: ({uri}) => {
logger.metric('post:mute', {})
return agent.api.app.bsky.graph.muteThread({root: uri})
},
})
@@ -382,6 +383,7 @@ function useThreadUnmuteMutation() {
const agent = useAgent()
return useMutation<{}, Error, {uri: string}>({
mutationFn: ({uri}) => {
logger.metric('post:unmute', {})
return agent.api.app.bsky.graph.unmuteThread({root: uri})
},
})
-45
View File
@@ -4,14 +4,12 @@ import {
type AppBskyActorGetProfile,
type AppBskyActorGetProfiles,
type AppBskyActorProfile,
type AppBskyGraphGetFollows,
AtUri,
type BskyAgent,
type ComAtprotoRepoUploadBlob,
type Un$Typed,
} from '@atproto/api'
import {
type InfiniteData,
keepPreviousData,
type QueryClient,
useMutation,
@@ -28,7 +26,6 @@ import {type Shadow} from '#/state/cache/types'
import {type ImageMeta} from '#/state/gallery'
import {STALE} from '#/state/queries'
import {resetProfilePostsQueries} from '#/state/queries/post-feed'
import {RQKEY as PROFILE_FOLLOWS_RQKEY} from '#/state/queries/profile-follows'
import {
unstableCacheProfileView,
useUnstableProfileViewCache,
@@ -250,7 +247,6 @@ export function useProfileFollowMutationQueue(
) {
const agent = useAgent()
const queryClient = useQueryClient()
const {currentAccount} = useSession()
const did = profile.did
const initialFollowingUri = profile.viewer?.following
const followMutation = useProfileFollowMutation(
@@ -287,47 +283,6 @@ export function useProfileFollowMutationQueue(
followingUri: finalFollowingUri,
})
// Optimistically update profile follows cache for avatar displays
if (currentAccount?.did) {
type FollowsQueryData =
InfiniteData<AppBskyGraphGetFollows.OutputSchema>
queryClient.setQueryData<FollowsQueryData>(
PROFILE_FOLLOWS_RQKEY(currentAccount.did),
old => {
if (!old?.pages?.[0]) return old
if (finalFollowingUri) {
// Add the followed profile to the beginning
const alreadyExists = old.pages[0].follows.some(
f => f.did === profile.did,
)
if (alreadyExists) return old
return {
...old,
pages: [
{
...old.pages[0],
follows: [
profile as AppBskyActorDefs.ProfileView,
...old.pages[0].follows,
],
},
...old.pages.slice(1),
],
}
} else {
// Remove the unfollowed profile
return {
...old,
pages: old.pages.map(page => ({
...page,
follows: page.follows.filter(f => f.did !== profile.did),
})),
}
}
},
)
}
if (finalFollowingUri) {
agent.app.bsky.graph
.getSuggestedFollowsByActor({
+1 -21
View File
@@ -21,7 +21,6 @@ import {makeProfileLink} from '#/lib/routes/links'
import {type NavigationProp} from '#/lib/routes/types'
import {useGate} from '#/lib/statsig/statsig'
import {countLines} from '#/lib/strings/helpers'
import {logger} from '#/logger'
import {
POST_TOMBSTONE,
type Shadow,
@@ -174,8 +173,7 @@ let FeedItemInner = ({
const urip = new AtUri(post.uri)
return [makeProfileLink(post.author, 'post', urip.rkey), urip.rkey]
}, [post.uri, post.author])
const {sendInteraction, feedSourceInfo, feedDescriptor} =
useFeedFeedbackContext()
const {sendInteraction, feedSourceInfo} = useFeedFeedbackContext()
const onPressReply = () => {
sendInteraction({
@@ -211,12 +209,6 @@ let FeedItemInner = ({
feedContext,
reqId,
})
logger.metric('post:clickthroughAuthor', {
uri: post.uri,
authorDid: post.author.did,
logContext: 'FeedItem',
feedDescriptor,
})
}
const onOpenReposter = () => {
@@ -235,12 +227,6 @@ let FeedItemInner = ({
feedContext,
reqId,
})
logger.metric('post:clickthroughEmbed', {
uri: post.uri,
authorDid: post.author.did,
logContext: 'FeedItem',
feedDescriptor,
})
}
const onBeforePress = () => {
@@ -250,12 +236,6 @@ let FeedItemInner = ({
feedContext,
reqId,
})
logger.metric('post:clickthroughItem', {
uri: post.uri,
authorDid: post.author.did,
logContext: 'FeedItem',
feedDescriptor,
})
unstableCacheProfileView(queryClient, post.author)
setUnstablePostSource(buildPostSourceKey(post.uri, post.author.handle), {
feedSourceInfo,
+8 -22
View File
@@ -49,7 +49,6 @@ import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/
import {StarterPack} from '#/components/icons/StarterPack'
import {EditLiveDialog} from '#/components/live/EditLiveDialog'
import {GoLiveDialog} from '#/components/live/GoLiveDialog'
import {GoLiveDisabledDialog} from '#/components/live/GoLiveDisabledDialog'
import * as Menu from '#/components/Menu'
import {
ReportDialog,
@@ -80,7 +79,6 @@ let ProfileMenu = ({
const [devModeEnabled] = useDevMode()
const verification = useFullVerificationState({profile})
const canGoLive = useCanGoLive(currentAccount?.did)
const status = useActorStatus(profile)
const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile)
const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
@@ -92,7 +90,6 @@ let ProfileMenu = ({
const blockPromptControl = Prompt.usePromptControl()
const loggedOutWarningPromptControl = Prompt.usePromptControl()
const goLiveDialogControl = useDialogControl()
const goLiveDisabledDialogControl = useDialogControl()
const addToStarterPacksDialogControl = useDialogControl()
const showLoggedOutWarning = React.useMemo(() => {
@@ -223,6 +220,8 @@ let ProfileMenu = ({
return v.issuer === currentAccount?.did
}) ?? []
const status = useActorStatus(profile)
return (
<EventStopper onKeyDown={false}>
<Menu.Root>
@@ -331,21 +330,13 @@ let ProfileMenu = ({
<Menu.Item
testID="profileHeaderDropdownListAddRemoveBtn"
label={
status.isDisabled
? _(msg`Go live (disabled)`)
: status.isActive
? _(msg`Edit live status`)
: _(msg`Go live`)
status.isActive
? _(msg`Edit live status`)
: _(msg`Go live`)
}
onPress={
status.isDisabled
? goLiveDisabledDialogControl.open
: goLiveDialogControl.open
}>
onPress={goLiveDialogControl.open}>
<Menu.ItemText>
{status.isDisabled ? (
<Trans>Go live (disabled)</Trans>
) : status.isActive ? (
{status.isActive ? (
<Trans>Edit live status</Trans>
) : (
<Trans>Go live</Trans>
@@ -526,12 +517,7 @@ let ProfileMenu = ({
verifications={currentAccountVerifications}
/>
{status.isDisabled ? (
<GoLiveDisabledDialog
control={goLiveDisabledDialogControl}
status={status}
/>
) : status.isActive ? (
{status.isActive ? (
<EditLiveDialog
control={goLiveDialogControl}
status={status}
+36 -148
View File
@@ -1,4 +1,4 @@
import {Pressable, View} from 'react-native'
import {View} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation, useNavigationState} from '@react-navigation/native'
@@ -7,18 +7,10 @@ import {getCurrentRoute} from '#/lib/routes/helpers'
import {type NavigationProp} from '#/lib/routes/types'
import {logger} from '#/logger'
import {emitSoftReset} from '#/state/events'
import {
type SavedFeedSourceInfo,
usePinnedFeedsInfos,
} from '#/state/queries/feed'
import {usePinnedFeedsInfos} from '#/state/queries/feed'
import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme, web} from '#/alf'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline'
import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {Link} from '#/components/Link'
import {Text} from '#/components/Typography'
import {createStaticClick, InlineLinkText} from '#/components/Link'
export function DesktopFeeds() {
const t = useTheme()
@@ -65,12 +57,13 @@ export function DesktopFeeds() {
style={[
a.flex_1,
web({
gap: 2,
gap: 10,
/*
* Small padding prevents overflow prior to actually overflowing the
* height of the screen with lots of feeds.
*/
paddingTop: 2,
paddingVertical: 2,
marginHorizontal: -2,
overflowY: 'auto',
}),
]}>
@@ -79,11 +72,10 @@ export function DesktopFeeds() {
const current = route.name === 'Home' && feed === selectedFeed
return (
<FeedItem
<InlineLinkText
key={feedInfo.uri}
feedInfo={feedInfo}
current={current}
onPress={() => {
label={feedInfo.displayName}
{...createStaticClick(() => {
logger.metric(
'desktopFeeds:feed:click',
{
@@ -97,143 +89,39 @@ export function DesktopFeeds() {
if (route.name === 'Home' && feed === selectedFeed) {
emitSoftReset()
}
}}
/>
})}
style={[
a.text_md,
a.leading_snug,
a.flex_shrink_0,
current
? [a.font_semi_bold, t.atoms.text]
: [t.atoms.text_contrast_medium],
web({
marginHorizontal: 2,
width: 'calc(100% - 4px)',
}),
]}
numberOfLines={1}>
{feedInfo.displayName}
</InlineLinkText>
)
})}
<Link
<InlineLinkText
to="/feeds"
label={_(msg`More feeds`)}
style={[
a.flex_row,
a.align_center,
a.gap_sm,
a.self_start,
a.rounded_sm,
{paddingVertical: 6, paddingHorizontal: 8},
route.name === 'Feeds' && {backgroundColor: t.palette.primary_50},
]}>
{({hovered}) => {
const isActive = route.name === 'Feeds'
return (
<>
<View
style={[
a.align_center,
a.justify_center,
a.rounded_xs,
isActive
? {backgroundColor: t.palette.primary_100}
: t.atoms.bg_contrast_50,
{
width: 20,
height: 20,
},
]}>
<Plus
style={{width: 16, height: 16}}
fill={
isActive || hovered
? t.atoms.text.color
: t.atoms.text_contrast_medium.color
}
/>
</View>
<Text
style={[
a.text_md,
a.leading_snug,
isActive
? [t.atoms.text, a.font_semi_bold]
: hovered
? t.atoms.text
: t.atoms.text_contrast_medium,
]}
numberOfLines={1}>
{_(msg`More feeds`)}
</Text>
</>
)
}}
</Link>
a.text_md,
a.leading_snug,
web({
marginHorizontal: 2,
width: 'calc(100% - 4px)',
}),
]}
numberOfLines={1}>
{_(msg`More feeds`)}
</InlineLinkText>
</View>
)
}
function FeedItem({
feedInfo,
current,
onPress,
}: {
feedInfo: SavedFeedSourceInfo
current: boolean
onPress: () => void
}) {
const t = useTheme()
const {_} = useLingui()
const {
state: hovered,
onIn: onHoverIn,
onOut: onHoverOut,
} = useInteractionState()
const isFollowing = feedInfo.feedDescriptor === 'following'
return (
<Pressable
accessibilityRole="link"
accessibilityLabel={feedInfo.displayName}
accessibilityHint={_(msg`Opens ${feedInfo.displayName} feed`)}
onPress={onPress}
onHoverIn={onHoverIn}
onHoverOut={onHoverOut}
style={[
a.flex_row,
a.align_center,
a.gap_sm,
a.self_start,
a.rounded_sm,
{paddingVertical: 6, paddingHorizontal: 8},
current && {backgroundColor: t.palette.primary_50},
]}>
{isFollowing ? (
<View
style={[
a.align_center,
a.justify_center,
a.rounded_xs,
{
width: 20,
height: 20,
backgroundColor: t.palette.primary_500,
},
]}>
<FilterTimeline
style={{width: 14, height: 14}}
fill={t.palette.white}
/>
</View>
) : (
<UserAvatar
type={feedInfo.type === 'list' ? 'list' : 'algo'}
size={20}
avatar={feedInfo.avatar}
noBorder
/>
)}
<Text
style={[
a.text_md,
a.leading_snug,
current
? [t.atoms.text, a.font_semi_bold]
: hovered
? t.atoms.text
: t.atoms.text_contrast_medium,
]}
numberOfLines={1}>
{feedInfo.displayName}
</Text>
</Pressable>
)
}
+7 -11
View File
@@ -18,6 +18,7 @@ import {
web,
} from '#/alf'
import {AppLanguageDropdown} from '#/components/AppLanguageDropdown'
import {Divider} from '#/components/Divider'
import {CENTER_COLUMN_OFFSET} from '#/components/Layout'
import {InlineLinkText} from '#/components/Link'
import {ProgressGuideList} from '#/components/ProgressGuide/List'
@@ -85,8 +86,9 @@ export function DesktopRightNav({routeName}: {routeName: string}) {
{hasSession && (
<>
<DesktopFeeds />
<ProgressGuideList />
<DesktopFeeds />
<Divider />
</>
)}
@@ -100,31 +102,25 @@ export function DesktopRightNav({routeName}: {routeName: string}) {
email: currentAccount?.email,
handle: currentAccount?.handle,
})}
style={[t.atoms.text_contrast_medium]}
label={_(msg`Feedback`)}>
{_(msg`Feedback`)}
</InlineLinkText>
<Text style={[t.atoms.text_contrast_low]}>{' '}</Text>
{' '}
</>
)}
<InlineLinkText
to="https://bsky.social/about/support/privacy-policy"
style={[t.atoms.text_contrast_medium]}
label={_(msg`Privacy`)}>
{_(msg`Privacy`)}
</InlineLinkText>
<Text style={[t.atoms.text_contrast_low]}>{' '}</Text>
{' '}
<InlineLinkText
to="https://bsky.social/about/support/tos"
style={[t.atoms.text_contrast_medium]}
label={_(msg`Terms`)}>
{_(msg`Terms`)}
</InlineLinkText>
<Text style={[t.atoms.text_contrast_low]}>{' '}</Text>
<InlineLinkText
label={_(msg`Help`)}
to={HELP_DESK_URL}
style={[t.atoms.text_contrast_medium]}>
{' '}
<InlineLinkText label={_(msg`Help`)} to={HELP_DESK_URL}>
{_(msg`Help`)}
</InlineLinkText>
</Text>
@@ -1,8 +1,9 @@
import React from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {logger} from '#/logger'
import {logEvent} from '#/lib/statsig/statsig'
import {
useTrendingSettings,
useTrendingSettingsApi,
@@ -11,13 +12,18 @@ import {useTrendingTopics} from '#/state/queries/trending/useTrendingTopics'
import {useTrendingConfig} from '#/state/service-config'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import {DotGrid_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid'
import {Trending3_Stroke2_Corner1_Rounded as TrendingIcon} from '#/components/icons/Trending'
import {Divider} from '#/components/Divider'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending'
import * as Prompt from '#/components/Prompt'
import {TrendingTopicLink} from '#/components/TrendingTopics'
import {
TrendingTopic,
TrendingTopicLink,
TrendingTopicSkeleton,
} from '#/components/TrendingTopics'
import {Text} from '#/components/Typography'
const TRENDING_LIMIT = 5
const TRENDING_LIMIT = 6
export function SidebarTrendingTopics() {
const {enabled} = useTrendingConfig()
@@ -33,88 +39,64 @@ function Inner() {
const {data: trending, error, isLoading} = useTrendingTopics()
const noTopics = !isLoading && !error && !trending?.topics?.length
const onConfirmHide = () => {
logger.metric('trendingTopics:hide', {context: 'sidebar'})
const onConfirmHide = React.useCallback(() => {
logEvent('trendingTopics:hide', {context: 'sidebar'})
setTrendingDisabled(true)
}
}, [setTrendingDisabled])
return error || noTopics ? null : (
<>
<View
style={[a.p_lg, a.rounded_md, a.border, t.atoms.border_contrast_low]}>
<View style={[a.flex_row, a.align_center, a.gap_xs, a.pb_md]}>
<TrendingIcon width={16} height={16} fill={t.atoms.text.color} />
<Text style={[a.flex_1, a.text_md, a.font_semi_bold, t.atoms.text]}>
<View style={[a.gap_sm, {paddingBottom: 2}]}>
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
<Graph size="sm" />
<Text
style={[
a.flex_1,
a.text_sm,
a.font_semi_bold,
t.atoms.text_contrast_medium,
]}>
<Trans>Trending</Trans>
</Text>
<Button
variant="ghost"
label={_(msg`Hide trending topics`)}
size="tiny"
variant="ghost"
color="secondary"
shape="round"
label={_(msg`Trending options`)}
onPress={() => trendingPrompt.open()}
style={[a.bg_transparent, {marginTop: -6, marginRight: -6}]}>
<ButtonIcon icon={Ellipsis} size="xs" />
onPress={() => trendingPrompt.open()}>
<ButtonIcon icon={X} />
</Button>
</View>
<View style={[a.gap_xs]}>
<View style={[a.flex_row, a.flex_wrap, {gap: '6px 4px'}]}>
{isLoading ? (
Array(TRENDING_LIMIT)
.fill(0)
.map((_n, i) => (
<View key={i} style={[a.flex_row, a.align_center, a.gap_sm]}>
<Text
style={[
a.text_sm,
t.atoms.text_contrast_low,
{minWidth: 16},
]}>
{i + 1}.
</Text>
<View
style={[
a.rounded_xs,
t.atoms.bg_contrast_50,
{height: 14, width: i % 2 === 0 ? 80 : 100},
]}
/>
</View>
<TrendingTopicSkeleton key={i} size="small" index={i} />
))
) : !trending?.topics ? null : (
<>
{trending.topics.slice(0, TRENDING_LIMIT).map((topic, i) => (
{trending.topics.slice(0, TRENDING_LIMIT).map(topic => (
<TrendingTopicLink
key={topic.link}
topic={topic}
style={[a.self_start]}
style={a.rounded_full}
onPress={() => {
logger.metric('trendingTopic:click', {context: 'sidebar'})
logEvent('trendingTopic:click', {context: 'sidebar'})
}}>
{({hovered}) => (
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
<Text
style={[
a.text_sm,
a.leading_snug,
t.atoms.text_contrast_low,
{minWidth: 16},
]}>
{i + 1}.
</Text>
<Text
style={[
a.text_sm,
a.leading_snug,
hovered
? [t.atoms.text, a.underline]
: t.atoms.text_contrast_medium,
]}
numberOfLines={1}>
{topic.displayName ?? topic.topic}
</Text>
</View>
<TrendingTopic
size="small"
topic={topic}
style={[
hovered && [
t.atoms.border_contrast_high,
t.atoms.bg_contrast_25,
],
]}
/>
)}
</TrendingTopicLink>
))}
@@ -129,6 +111,7 @@ function Inner() {
confirmButtonCta={_(msg`Hide`)}
onConfirm={onConfirmHide}
/>
<Divider />
</>
)
}
-2
View File
@@ -34,7 +34,6 @@ import {LinkWarningDialog} from '#/components/dialogs/LinkWarning'
import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
import {NuxDialogs} from '#/components/dialogs/nuxs'
import {SigninDialog} from '#/components/dialogs/Signin'
import {GlobalReportDialog} from '#/components/moderation/ReportDialog'
import {
Outlet as PolicyUpdateOverlayPortalOutlet,
usePolicyUpdateContext,
@@ -118,7 +117,6 @@ function ShellInner() {
<LinkWarningDialog />
<Lightbox />
<NuxDialogs />
<GlobalReportDialog />
{/* Until policy update has been completed by the user, don't render anything that is portaled */}
{policyUpdateState.completed && (
-2
View File
@@ -24,7 +24,6 @@ import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
import {NuxDialogs} from '#/components/dialogs/nuxs'
import {SigninDialog} from '#/components/dialogs/Signin'
import {useWelcomeModal} from '#/components/hooks/useWelcomeModal'
import {GlobalReportDialog} from '#/components/moderation/ReportDialog'
import {
Outlet as PolicyUpdateOverlayPortalOutlet,
usePolicyUpdateContext,
@@ -74,7 +73,6 @@ function ShellInner() {
<LinkWarningDialog />
<Lightbox />
<NuxDialogs />
<GlobalReportDialog />
{welcomeModalControl.isOpen && (
<WelcomeModal control={welcomeModalControl} />
+14 -42
View File
@@ -82,20 +82,6 @@
"@atproto/xrpc" "^0.7.6"
"@atproto/xrpc-server" "^0.10.0"
"@atproto/api@^0.18.13":
version "0.18.13"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.18.13.tgz#63eee310e6715752eb87748323cf9ab57dd91e4b"
integrity sha512-CULZ01pSJDltLS/Gc9MMrhFzB6OM3ezyZw7KoeLT/sBfwgA1ddA4mWdTh7DIRosPRigXtA05bnoiCutZbQDo+Q==
dependencies:
"@atproto/common-web" "^0.4.11"
"@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.5", "@atproto/api@^0.18.7":
version "0.18.7"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.18.7.tgz#3175ec8f1909ddcae488183a2180de234e7acce4"
@@ -124,6 +110,20 @@
tlds "^1.234.0"
zod "^3.23.8"
"@atproto/api@^0.18.8":
version "0.18.8"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.18.8.tgz#6df69731005413d8507345829a12abeda787c32d"
integrity sha512-Qo3sGd1N5hdHTaEWUBgptvPkULt2SXnMcWRhveSyctSd/IQwTMyaIH6E62A1SU+8xBSN5QLpoUJNE7iSrYM2Zg==
dependencies:
"@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/aws@^0.2.31":
version "0.2.31"
resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.2.31.tgz#e46d7db34ee57c4f9817269f1e73a7eddba2b9b8"
@@ -208,15 +208,6 @@
pino-http "^8.2.1"
typed-emitter "^2.1.0"
"@atproto/common-web@^0.4.11":
version "0.4.11"
resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.4.11.tgz#eb41dc02c1ea4221388630e193d181fb098186e0"
integrity sha512-VHejNmSABU8/03VrQ3e36AmT5U3UIeio+qSUqCrO1oNgrJcWfGy1rpj0FVtUugWF8Un29+yzkukzWGZfXL70rQ==
dependencies:
"@atproto/lex-data" "0.0.7"
"@atproto/lex-json" "0.0.7"
zod "^3.23.8"
"@atproto/common-web@^0.4.4", "@atproto/common-web@^0.4.6":
version "0.4.6"
resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.4.6.tgz#e32395d44d812610fd99f718b8644308b828d68b"
@@ -415,17 +406,6 @@
uint8arrays "3.0.0"
unicode-segmenter "^0.14.0"
"@atproto/lex-data@0.0.7":
version "0.0.7"
resolved "https://registry.yarnpkg.com/@atproto/lex-data/-/lex-data-0.0.7.tgz#6aa87423f6d47849bec8ff3ca0b00ce93964adc8"
integrity sha512-W/Q5o9o7n2Sv3UywckChu01X5lwQUtaiiOkGJLnRsdkQTyC6813nPgY+p2sG7NwwM+82lu+FUV9fE/Ul3VqaJw==
dependencies:
"@atproto/syntax" "0.4.2"
multiformats "^9.9.0"
tslib "^2.8.1"
uint8arrays "3.0.0"
unicode-segmenter "^0.14.0"
"@atproto/lex-document@0.0.5":
version "0.0.5"
resolved "https://registry.yarnpkg.com/@atproto/lex-document/-/lex-document-0.0.5.tgz#8d4851b351149ba673c1de9c1898c3c9ebd8b4b3"
@@ -451,14 +431,6 @@
"@atproto/lex-data" "0.0.3"
tslib "^2.8.1"
"@atproto/lex-json@0.0.7":
version "0.0.7"
resolved "https://registry.yarnpkg.com/@atproto/lex-json/-/lex-json-0.0.7.tgz#c06e1fc3e06d739bbb74694f5d846055bed37866"
integrity sha512-bjNPD5M/MhLfjNM7tcxuls80UgXpHqxdOxDXEUouAtZQV/nIDhGjmNUvKxOmOgnDsiZRnT2g5y3onrnjH3a44g==
dependencies:
"@atproto/lex-data" "0.0.7"
tslib "^2.8.1"
"@atproto/lex-resolver@0.0.5":
version "0.0.5"
resolved "https://registry.yarnpkg.com/@atproto/lex-resolver/-/lex-resolver-0.0.5.tgz#9d0645c43423cb99b491ca8e658770395d2cd630"