Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b94b819e4 |
@@ -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: >
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"scripts": {
|
||||
"setup": "yarn install",
|
||||
"run": "yarn web --port $CONDUCTOR_PORT"
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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",
|
||||
|
||||
@@ -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
|
||||
@@ -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]}>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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,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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
+229
-297
File diff suppressed because it is too large
Load Diff
+9
-77
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user