Compare commits
59 Commits
tos
...
thread-bug
| Author | SHA1 | Date | |
|---|---|---|---|
| d74d5e9d2c | |||
| 314c380fc4 | |||
| f2892779da | |||
| 5ca665467e | |||
| 4e0be91cff | |||
| a761a2e260 | |||
| 9440068b69 | |||
| 7b2e61bf4d | |||
| 221623f55a | |||
| f4dca5d230 | |||
| 80ce71283b | |||
| 7a334362ae | |||
| b2c56cbd6d | |||
| 275fece3e3 | |||
| eef639ea54 | |||
| 36b2a87a06 | |||
| f001af2fe3 | |||
| d5cc19f282 | |||
| edd205483d | |||
| 485f7ee95b | |||
| bfcdb83536 | |||
| 6cd7dc0f9a | |||
| 338fa4dfbe | |||
| bf869d9185 | |||
| 683d7bf5a7 | |||
| 44cdf32c1f | |||
| 86b53e7bcc | |||
| 3ce14b0c6f | |||
| 9dfddd3c42 | |||
| ef171e2892 | |||
| 17c5cec356 | |||
| f873f3b301 | |||
| a3bb8cca6e | |||
| bb949e4f44 | |||
| 2a6172cbaf | |||
| 4d1a03b99e | |||
| 52637472e6 | |||
| 23a797cfab | |||
| 22a8b63dcf | |||
| 2ba2d9e5b5 | |||
| 93c4719a21 | |||
| f708e88410 | |||
| 88856b0ff7 | |||
| 08a8324290 | |||
| 11c9931fd2 | |||
| c0593e4979 | |||
| 39e775a376 | |||
| d84d54bae3 | |||
| 0a2b1fcfb8 | |||
| cb54082eda | |||
| 20ac2e2733 | |||
| c103687d66 | |||
| dbb9ff6918 | |||
| 909ddc86e1 | |||
| 75094f4860 | |||
| 328aa2be94 | |||
| 9153474e59 | |||
| 4c8b30df19 | |||
| 73ce358052 |
@@ -75,6 +75,7 @@ jobs:
|
||||
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
|
||||
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
|
||||
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
|
||||
echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env
|
||||
echo "$json" > google-services.json
|
||||
|
||||
- name: 🏗️ EAS Build
|
||||
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
|
||||
- uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: '16.2'
|
||||
xcode-version: '16.4'
|
||||
|
||||
- name: ☕️ Setup Cocoapods
|
||||
uses: maxim-lobanov/setup-cocoapods@v1
|
||||
@@ -87,6 +87,7 @@ jobs:
|
||||
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
|
||||
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
|
||||
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
|
||||
echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env
|
||||
echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json
|
||||
|
||||
- name: 🏗️ EAS Build
|
||||
|
||||
@@ -116,6 +116,7 @@ jobs:
|
||||
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
|
||||
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
|
||||
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
|
||||
echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env
|
||||
echo "$json" > google-services.json
|
||||
|
||||
- name: 🏗️ Create Bundle
|
||||
@@ -222,6 +223,7 @@ jobs:
|
||||
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
|
||||
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
|
||||
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
|
||||
echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env
|
||||
echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json
|
||||
|
||||
- name: 🏗️ EAS Build
|
||||
@@ -313,6 +315,7 @@ jobs:
|
||||
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
|
||||
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
|
||||
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
|
||||
echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env
|
||||
echo "$json" > google-services.json
|
||||
|
||||
- name: 🏗️ EAS Build
|
||||
|
||||
@@ -165,6 +165,7 @@ jobs:
|
||||
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
|
||||
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
|
||||
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
|
||||
echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env
|
||||
echo "$json" > google-services.json
|
||||
|
||||
- name: 🏗️ Create Bundle
|
||||
|
||||
@@ -50,6 +50,7 @@ jobs:
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git merge --no-edit ${{ github.head_ref }}
|
||||
yarn install
|
||||
yarn intl:build
|
||||
|
||||
- name: 🔦 Generate stats file for PR
|
||||
run: |
|
||||
@@ -73,6 +74,7 @@ jobs:
|
||||
if: ${{ !steps.get-base-stats.outputs.cache-hit }}
|
||||
run: |
|
||||
yarn install
|
||||
yarn intl:build
|
||||
yarn generate-webpack-stats-file
|
||||
mv stats.json stats-base.json
|
||||
|
||||
@@ -143,4 +145,3 @@ jobs:
|
||||
with:
|
||||
header: fingerprint-diff
|
||||
delete: true
|
||||
|
||||
|
||||
@@ -28,6 +28,9 @@ appId: xyz.blueskyweb.app
|
||||
- hideKeyboard
|
||||
- tapOn:
|
||||
id: "nextBtn"
|
||||
- tapOn:
|
||||
text: "Not Now"
|
||||
optional: true
|
||||
- inputText: "e2e-test"
|
||||
- tapOn:
|
||||
id: "nextBtn"
|
||||
|
||||
@@ -23,4 +23,7 @@ appId: xyz.blueskyweb.app
|
||||
id: "loginPasswordInput"
|
||||
- inputText: "hunter2"
|
||||
- pressKey: Enter
|
||||
- tapOn:
|
||||
text: "Not Now"
|
||||
optional: true
|
||||
- assertVisible: "Following"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, {ReactNode} from 'react'
|
||||
import {View, ScrollView, Modal, FlatList, TextInput} from 'react-native'
|
||||
import React, {type ReactNode} from 'react'
|
||||
import {FlatList, Modal, ScrollView, TextInput, View} from 'react-native'
|
||||
|
||||
const BottomSheetModalContext = React.createContext(null)
|
||||
BottomSheetModalContext.displayName = 'BottomSheetModalContext'
|
||||
|
||||
const BottomSheetModalProvider = (props: any) => {
|
||||
return <BottomSheetModalContext.Provider {...props} value={{}} />
|
||||
@@ -47,13 +48,13 @@ export {useBottomSheetInternal}
|
||||
export {useBottomSheetDynamicSnapPoints}
|
||||
|
||||
export {
|
||||
BottomSheetModalProvider,
|
||||
BottomSheetBackdrop,
|
||||
BottomSheetFlatList,
|
||||
BottomSheetFooter,
|
||||
BottomSheetHandle,
|
||||
BottomSheetModal,
|
||||
BottomSheetFooter,
|
||||
BottomSheetModalProvider,
|
||||
BottomSheetScrollView,
|
||||
BottomSheetFlatList,
|
||||
BottomSheetTextInput,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {IsValidHandle, validateServiceHandle} from '#/lib/strings/handles'
|
||||
import {type IsValidHandle, validateServiceHandle} from '#/lib/strings/handles'
|
||||
|
||||
describe('handle validation', () => {
|
||||
const valid = [
|
||||
@@ -18,17 +18,17 @@ describe('handle validation', () => {
|
||||
})
|
||||
|
||||
const invalid = [
|
||||
['al', 'bsky.social', 'frontLength'],
|
||||
['al', 'bsky.social', 'frontLengthNotTooShort'],
|
||||
['-alice', 'bsky.social', 'hyphenStartOrEnd'],
|
||||
['alice-', 'bsky.social', 'hyphenStartOrEnd'],
|
||||
['%%%', 'bsky.social', 'handleChars'],
|
||||
['1234567890123456789', 'bsky.social', 'frontLength'],
|
||||
['1234567890123456789', 'bsky.social', 'frontLengthNotTooLong'],
|
||||
[
|
||||
'1234567890123456789',
|
||||
'my-custom-pds-with-long-name.social',
|
||||
'frontLength',
|
||||
'frontLengthNotTooLong',
|
||||
],
|
||||
['al', 'my-custom-pds-with-long-name.social', 'frontLength'],
|
||||
['al', 'my-custom-pds-with-long-name.social', 'frontLengthNotTooShort'],
|
||||
['a'.repeat(300), 'toolong.com', 'totalLength'],
|
||||
] satisfies [string, string, keyof IsValidHandle][]
|
||||
it.each(invalid)(
|
||||
|
||||
+6
-3
@@ -234,8 +234,9 @@ module.exports = function (_config) {
|
||||
],
|
||||
'./plugins/starterPackAppClipExtension/withStarterPackAppClip.js',
|
||||
'./plugins/withGradleJVMHeapSizeIncrease.js',
|
||||
'./plugins/withAndroidManifestPlugin.js',
|
||||
'./plugins/withAndroidManifestLargeHeapPlugin.js',
|
||||
'./plugins/withAndroidManifestFCMIconPlugin.js',
|
||||
'./plugins/withAndroidManifestIntentQueriesPlugin.js',
|
||||
'./plugins/withAndroidStylesAccentColorPlugin.js',
|
||||
'./plugins/withAndroidDayNightThemePlugin.js',
|
||||
'./plugins/withAndroidNoJitpackPlugin.js',
|
||||
@@ -250,10 +251,12 @@ module.exports = function (_config) {
|
||||
// Android only
|
||||
'./assets/fonts/inter/Inter-Regular.otf',
|
||||
'./assets/fonts/inter/Inter-Italic.otf',
|
||||
'./assets/fonts/inter/Inter-Medium.otf',
|
||||
'./assets/fonts/inter/Inter-MediumItalic.otf',
|
||||
'./assets/fonts/inter/Inter-SemiBold.otf',
|
||||
'./assets/fonts/inter/Inter-SemiBoldItalic.otf',
|
||||
'./assets/fonts/inter/Inter-ExtraBold.otf',
|
||||
'./assets/fonts/inter/Inter-ExtraBoldItalic.otf',
|
||||
'./assets/fonts/inter/Inter-Bold.otf',
|
||||
'./assets/fonts/inter/Inter-BoldItalic.otf',
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M15.793 10.293a1 1 0 0 1 1.338-.068l.076.068 3.293 3.293a2 2 0 0 1 .138 2.677l-.138.151-3.293 3.293a1 1 0 1 1-1.414-1.414L18.086 16H8a5 5 0 0 1-5-5V5a1 1 0 0 1 2 0v6a3 3 0 0 0 3 3h10.086l-2.293-2.293-.068-.076a1 1 0 0 1 .068-1.338Z"/></svg>
|
||||
|
After Width: | Height: | Size: 334 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 250 B |
@@ -8,6 +8,7 @@
|
||||
Preconnect to essential domains
|
||||
-->
|
||||
<link rel="preconnect" href="https://bsky.social">
|
||||
<link rel="preconnect" href="https://go.bsky.app">
|
||||
<title>{%- block head_title -%}Bluesky{%- endblock -%}</title>
|
||||
|
||||
<!-- Hello Humans! API docs at https://atproto.com -->
|
||||
|
||||
@@ -43,7 +43,7 @@ class ViewController: UIViewController, WKScriptMessageHandler, WKNavigationDele
|
||||
let payload = try? JSONDecoder().decode(WebViewActionPayload.self, from: data) else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
switch payload.action {
|
||||
case .present:
|
||||
self.presentAppStoreOverlay()
|
||||
@@ -65,18 +65,18 @@ class ViewController: UIViewController, WKScriptMessageHandler, WKNavigationDele
|
||||
guard let url = navigationAction.request.url else {
|
||||
return .allow
|
||||
}
|
||||
|
||||
|
||||
// Store the previous one to compare later, but only set starterPackUrl when we find the right one
|
||||
prevUrl = url
|
||||
// pathComponents starts with "/" as the first component, then each path name. so...
|
||||
// ["/", "start", "name", "rkey"]
|
||||
if isStarterPackUrl(url){
|
||||
if isStarterPackUrl(url) {
|
||||
self.starterPackUrl = url
|
||||
}
|
||||
|
||||
|
||||
return .allow
|
||||
}
|
||||
|
||||
|
||||
func isStarterPackUrl(_ url: URL) -> Bool {
|
||||
var host: String?
|
||||
if #available(iOS 16.0, *) {
|
||||
@@ -84,11 +84,11 @@ class ViewController: UIViewController, WKScriptMessageHandler, WKNavigationDele
|
||||
} else {
|
||||
host = url.host
|
||||
}
|
||||
|
||||
|
||||
switch host {
|
||||
case "bsky.app":
|
||||
if url.pathComponents.count == 4,
|
||||
(url.pathComponents[1] == "start" || url.pathComponents[1] == "starter-pack") {
|
||||
url.pathComponents[1] == "start" || url.pathComponents[1] == "starter-pack" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
+8
-8
@@ -19,7 +19,6 @@ import expo.modules.kotlin.AppContext
|
||||
import expo.modules.kotlin.viewevent.EventDispatcher
|
||||
import expo.modules.kotlin.views.ExpoView
|
||||
|
||||
|
||||
class BottomSheetView(
|
||||
context: Context,
|
||||
appContext: AppContext,
|
||||
@@ -31,12 +30,14 @@ class BottomSheetView(
|
||||
private lateinit var dialogRootViewGroup: DialogRootViewGroup
|
||||
private var eventDispatcher: EventDispatcher? = null
|
||||
|
||||
private val rawScreenHeight = context.resources.displayMetrics.heightPixels.toFloat()
|
||||
private val rawScreenHeight =
|
||||
context.resources.displayMetrics.heightPixels
|
||||
.toFloat()
|
||||
private val safeScreenHeight = (rawScreenHeight - getNavigationBarHeight()).toFloat()
|
||||
|
||||
private fun getNavigationBarHeight(): Int {
|
||||
val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android")
|
||||
return if (resourceId > 0) resources.getDimensionPixelSize(resourceId) else 0
|
||||
val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android")
|
||||
return if (resourceId > 0) resources.getDimensionPixelSize(resourceId) else 0
|
||||
}
|
||||
|
||||
private val onAttemptDismiss by EventDispatcher()
|
||||
@@ -45,7 +46,7 @@ class BottomSheetView(
|
||||
|
||||
// Props
|
||||
var disableDrag = false
|
||||
set (value) {
|
||||
set(value) {
|
||||
field = value
|
||||
this.setDraggable(!value)
|
||||
}
|
||||
@@ -157,8 +158,8 @@ class BottomSheetView(
|
||||
|
||||
// Presentation
|
||||
|
||||
private fun getHalfExpandedRatio(contentHeight: Float): Float {
|
||||
return when {
|
||||
private fun getHalfExpandedRatio(contentHeight: Float): Float =
|
||||
when {
|
||||
// Full height sheets
|
||||
contentHeight >= safeScreenHeight -> 0.99f
|
||||
// Medium height sheets (>50% but <100%)
|
||||
@@ -168,7 +169,6 @@ class BottomSheetView(
|
||||
else ->
|
||||
this.clampRatio(this.getTargetHeight() / rawScreenHeight)
|
||||
}
|
||||
}
|
||||
|
||||
private fun present() {
|
||||
if (this.isOpen || this.isOpening || this.isClosing) return
|
||||
|
||||
+4
-1
@@ -139,7 +139,10 @@ class DialogRootViewGroup(
|
||||
return super.onHoverEvent(event)
|
||||
}
|
||||
|
||||
override fun onChildStartedNativeGesture(childView: View?, ev: MotionEvent) {
|
||||
override fun onChildStartedNativeGesture(
|
||||
childView: View?,
|
||||
ev: MotionEvent,
|
||||
) {
|
||||
eventDispatcher?.let { jSTouchDispatcher.onChildStartedNativeGesture(ev, it) }
|
||||
jSPointerDispatcher?.onChildStartedNativeGesture(childView, ev, eventDispatcher)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {createPortalGroup_INTERNAL} from './lib/Portal'
|
||||
type PortalContext = React.ElementType<{children: React.ReactNode}>
|
||||
|
||||
export const Context = React.createContext({} as PortalContext)
|
||||
Context.displayName = 'BottomSheetPortalContext'
|
||||
|
||||
export const useBottomSheetPortal_INTERNAL = () => React.useContext(Context)
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ export function createPortalGroup_INTERNAL() {
|
||||
append: () => {},
|
||||
remove: () => {},
|
||||
})
|
||||
Context.displayName = 'BottomSheetPortalContext'
|
||||
|
||||
function Provider(props: React.PropsWithChildren<{}>) {
|
||||
const map = React.useRef<ComponentMap>({})
|
||||
|
||||
+60
-60
@@ -16,8 +16,8 @@ import expo.modules.kotlin.viewevent.EventDispatcher
|
||||
import expo.modules.kotlin.views.ExpoView
|
||||
|
||||
class GifView(
|
||||
context: Context,
|
||||
appContext: AppContext,
|
||||
context: Context,
|
||||
appContext: AppContext,
|
||||
) : ExpoView(context, appContext) {
|
||||
// Events
|
||||
private val onPlayerStateChange by EventDispatcher()
|
||||
@@ -82,65 +82,65 @@ class GifView(
|
||||
}
|
||||
|
||||
this.webpRequest =
|
||||
glide.load(source)
|
||||
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
||||
.skipMemoryCache(false)
|
||||
.listener(
|
||||
object : RequestListener<Drawable> {
|
||||
override fun onResourceReady(
|
||||
resource: Drawable,
|
||||
model: Any,
|
||||
target: Target<Drawable>?,
|
||||
dataSource: DataSource,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
placeholderRequest?.let { glide.clear(it) }
|
||||
return false
|
||||
}
|
||||
glide
|
||||
.load(source)
|
||||
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
||||
.skipMemoryCache(false)
|
||||
.listener(
|
||||
object : RequestListener<Drawable> {
|
||||
override fun onResourceReady(
|
||||
resource: Drawable,
|
||||
model: Any,
|
||||
target: Target<Drawable>?,
|
||||
dataSource: DataSource,
|
||||
isFirstResource: Boolean,
|
||||
): Boolean {
|
||||
placeholderRequest?.let { glide.clear(it) }
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onLoadFailed(
|
||||
e: GlideException?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>,
|
||||
isFirstResource: Boolean
|
||||
): Boolean = true
|
||||
}
|
||||
)
|
||||
.into(this.imageView)
|
||||
override fun onLoadFailed(
|
||||
e: GlideException?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>,
|
||||
isFirstResource: Boolean,
|
||||
): Boolean = true
|
||||
},
|
||||
).into(this.imageView)
|
||||
|
||||
if (this.imageView.drawable == null || this.imageView.drawable !is Animatable) {
|
||||
this.placeholderRequest =
|
||||
glide.load(placeholderSource)
|
||||
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
||||
// Let's not bloat the memory cache with placeholders
|
||||
.skipMemoryCache(true)
|
||||
.listener(
|
||||
object : RequestListener<Drawable> {
|
||||
override fun onResourceReady(
|
||||
resource: Drawable,
|
||||
model: Any,
|
||||
target: Target<Drawable>?,
|
||||
dataSource: DataSource,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
// Incase this request finishes after the webp, let's just not set
|
||||
// the drawable. This shouldn't happen because the request should
|
||||
// get cancelled
|
||||
if (imageView.drawable == null) {
|
||||
imageView.setImageDrawable(resource)
|
||||
}
|
||||
return true
|
||||
}
|
||||
glide
|
||||
.load(placeholderSource)
|
||||
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
||||
// Let's not bloat the memory cache with placeholders
|
||||
.skipMemoryCache(true)
|
||||
.listener(
|
||||
object : RequestListener<Drawable> {
|
||||
override fun onResourceReady(
|
||||
resource: Drawable,
|
||||
model: Any,
|
||||
target: Target<Drawable>?,
|
||||
dataSource: DataSource,
|
||||
isFirstResource: Boolean,
|
||||
): Boolean {
|
||||
// Incase this request finishes after the webp, let's just not set
|
||||
// the drawable. This shouldn't happen because the request should
|
||||
// get cancelled
|
||||
if (imageView.drawable == null) {
|
||||
imageView.setImageDrawable(resource)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onLoadFailed(
|
||||
e: GlideException?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>,
|
||||
isFirstResource: Boolean
|
||||
): Boolean = true
|
||||
},
|
||||
)
|
||||
.submit()
|
||||
override fun onLoadFailed(
|
||||
e: GlideException?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>,
|
||||
isFirstResource: Boolean,
|
||||
): Boolean = true
|
||||
},
|
||||
).submit()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,10 +174,10 @@ class GifView(
|
||||
|
||||
fun firePlayerStateChange() {
|
||||
onPlayerStateChange(
|
||||
mapOf(
|
||||
"isPlaying" to this.isPlaying,
|
||||
"isLoaded" to this.isLoaded,
|
||||
),
|
||||
mapOf(
|
||||
"isPlaying" to this.isPlaying,
|
||||
"isLoaded" to this.isLoaded,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+6
-5
@@ -5,11 +5,12 @@ import expo.modules.kotlin.modules.ModuleDefinition
|
||||
import java.net.URL
|
||||
|
||||
class EmojiPickerModule : Module() {
|
||||
override fun definition() = ModuleDefinition {
|
||||
Name("EmojiPicker")
|
||||
override fun definition() =
|
||||
ModuleDefinition {
|
||||
Name("EmojiPicker")
|
||||
|
||||
View(EmojiPickerModuleView::class) {
|
||||
Events("onEmojiSelected")
|
||||
}
|
||||
View(EmojiPickerModuleView::class) {
|
||||
Events("onEmojiSelected")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+27
-25
@@ -8,33 +8,35 @@ import expo.modules.kotlin.AppContext
|
||||
import expo.modules.kotlin.viewevent.EventDispatcher
|
||||
import expo.modules.kotlin.views.ExpoView
|
||||
|
||||
|
||||
@SuppressLint("ViewConstructor")
|
||||
class EmojiPickerModuleView(context: Context, appContext: AppContext) :
|
||||
ExpoView(context, appContext) {
|
||||
private var emojiView: EmojiPickerView = EmojiPickerView(context)
|
||||
private val onEmojiSelected by EventDispatcher()
|
||||
class EmojiPickerModuleView(
|
||||
context: Context,
|
||||
appContext: AppContext,
|
||||
) : ExpoView(context, appContext) {
|
||||
private var emojiView: EmojiPickerView = EmojiPickerView(context)
|
||||
private val onEmojiSelected by EventDispatcher()
|
||||
|
||||
init {
|
||||
setupView()
|
||||
init {
|
||||
setupView()
|
||||
}
|
||||
|
||||
private fun setupView() {
|
||||
addView(
|
||||
emojiView,
|
||||
LayoutParams(
|
||||
LayoutParams.MATCH_PARENT,
|
||||
LayoutParams.MATCH_PARENT,
|
||||
),
|
||||
)
|
||||
|
||||
emojiView.setOnEmojiPickedListener { emoji ->
|
||||
onEmojiSelected(mapOf("emoji" to emoji.emoji))
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupView() {
|
||||
addView(
|
||||
emojiView, LayoutParams(
|
||||
LayoutParams.MATCH_PARENT,
|
||||
LayoutParams.MATCH_PARENT
|
||||
)
|
||||
)
|
||||
|
||||
emojiView.setOnEmojiPickedListener { emoji ->
|
||||
onEmojiSelected(mapOf("emoji" to emoji.emoji))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration?) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
removeView(emojiView)
|
||||
setupView()
|
||||
}
|
||||
override fun onConfigurationChanged(newConfig: Configuration?) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
removeView(emojiView)
|
||||
setupView()
|
||||
}
|
||||
}
|
||||
|
||||
+9
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bsky.app",
|
||||
"version": "1.106.0",
|
||||
"version": "1.107.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
@@ -20,7 +20,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "is-ci || husky install",
|
||||
"postinstall": "patch-package && yarn intl:compile",
|
||||
"postinstall": "patch-package && yarn intl:compile-if-needed",
|
||||
"prebuild": "expo prebuild --clean",
|
||||
"android": "expo run:android",
|
||||
"android:prod": "expo run:android --variant release",
|
||||
@@ -58,6 +58,7 @@
|
||||
"intl:extract": "lingui extract --clean --locale en",
|
||||
"intl:extract:all": "lingui extract --clean",
|
||||
"intl:compile": "lingui compile",
|
||||
"intl:compile-if-needed": "is-ci || [ -f src/locale/locales/en/messages.js ] || yarn intl:compile",
|
||||
"intl:pull": "crowdin download translations --verbose -b main",
|
||||
"intl:push": "crowdin push translations --verbose -b main",
|
||||
"nuke": "rm -rf ./node_modules && rm -rf ./ios && rm -rf ./android",
|
||||
@@ -70,7 +71,7 @@
|
||||
"icons:optimize": "svgo -f ./assets/icons"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.15.26",
|
||||
"@atproto/api": "^0.16.2",
|
||||
"@bitdrift/react-native": "^0.6.8",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||
@@ -146,6 +147,7 @@
|
||||
"expo-image-crop-tool": "^0.1.8",
|
||||
"expo-image-manipulator": "~13.1.7",
|
||||
"expo-image-picker": "~16.1.4",
|
||||
"expo-intent-launcher": "^12.1.5",
|
||||
"expo-linear-gradient": "~14.1.5",
|
||||
"expo-linking": "~7.1.5",
|
||||
"expo-localization": "~16.1.5",
|
||||
@@ -186,6 +188,7 @@
|
||||
"react-native": "^0.79.3",
|
||||
"react-native-compressor": "^1.11.0",
|
||||
"react-native-date-picker": "^5.0.12",
|
||||
"react-native-device-attest": "^0.1.6",
|
||||
"react-native-drawer-layout": "^4.1.8",
|
||||
"react-native-edge-to-edge": "^1.6.0",
|
||||
"react-native-gesture-handler": "2.25.0",
|
||||
@@ -211,6 +214,8 @@
|
||||
"react-remove-scroll-bar": "^2.3.8",
|
||||
"react-responsive": "^9.0.2",
|
||||
"react-textarea-autosize": "^8.5.3",
|
||||
"sonner": "^2.0.7",
|
||||
"sonner-native": "^0.21.0",
|
||||
"statsig-react-native-expo": "^4.6.1",
|
||||
"tippy.js": "^6.3.7",
|
||||
"tlds": "^1.234.0",
|
||||
@@ -219,7 +224,7 @@
|
||||
"zod": "^3.20.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@atproto/dev-env": "^0.3.155",
|
||||
"@atproto/dev-env": "^0.3.160",
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/preset-env": "^7.26.0",
|
||||
"@babel/runtime": "^7.26.0",
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
const {withAndroidManifest} = require('@expo/config-plugins')
|
||||
|
||||
const withProcessTextQuery = config =>
|
||||
// eslint-disable-next-line no-shadow
|
||||
withAndroidManifest(config, config => {
|
||||
const manifest = config.modResults.manifest
|
||||
|
||||
// Ensure <queries> stub exists
|
||||
if (!manifest.queries) manifest.queries = [{}]
|
||||
const queries = manifest.queries[0]
|
||||
|
||||
queries.intent = queries.intent || []
|
||||
|
||||
const exists = queries.intent.some(
|
||||
i =>
|
||||
i.action?.[0]?.$?.['android:name'] ===
|
||||
'android.intent.action.PROCESS_TEXT',
|
||||
)
|
||||
|
||||
if (!exists) {
|
||||
queries.intent.push({
|
||||
action: [{$: {'android:name': 'android.intent.action.PROCESS_TEXT'}}],
|
||||
data: [{$: {'android:mimeType': 'text/plain'}}],
|
||||
})
|
||||
}
|
||||
|
||||
return config
|
||||
})
|
||||
|
||||
module.exports = withProcessTextQuery
|
||||
+51
-43
@@ -29,6 +29,7 @@ import {Provider as A11yProvider} from '#/state/a11y'
|
||||
import {Provider as AgeAssuranceProvider} from '#/state/ageAssurance'
|
||||
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
|
||||
import {Provider as DialogStateProvider} from '#/state/dialogs'
|
||||
import {Provider as EmailVerificationProvider} from '#/state/email-verification'
|
||||
import {listenSessionDropped} from '#/state/events'
|
||||
import {
|
||||
beginResolveGeolocation,
|
||||
@@ -70,8 +71,10 @@ import {Provider as ContextMenuProvider} from '#/components/ContextMenu'
|
||||
import {NuxDialogs} from '#/components/dialogs/nuxs'
|
||||
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
||||
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
|
||||
import {Provider as PolicyUpdateOverlayProvider} from '#/components/PolicyUpdateOverlay'
|
||||
import {Provider as PortalProvider} from '#/components/Portal'
|
||||
import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
||||
import {ToastOutlet} from '#/components/Toast'
|
||||
import {Splash} from '#/Splash'
|
||||
import {BottomSheetProvider} from '../modules/bottom-sheet'
|
||||
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||
@@ -137,49 +140,54 @@ function InnerApp() {
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<QueryProvider currentDid={currentAccount?.did}>
|
||||
<StatsigProvider>
|
||||
<AgeAssuranceProvider>
|
||||
<ComposerProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<HiddenRepliesProvider>
|
||||
<HomeBadgeProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<ProgressGuideProvider>
|
||||
<ServiceAccountManager>
|
||||
<HideBottomBarBorderProvider>
|
||||
<GestureHandlerRootView
|
||||
style={s.h100pct}>
|
||||
<GlobalGestureEventsProvider>
|
||||
<IntentDialogProvider>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
</IntentDialogProvider>
|
||||
</GlobalGestureEventsProvider>
|
||||
</GestureHandlerRootView>
|
||||
</HideBottomBarBorderProvider>
|
||||
</ServiceAccountManager>
|
||||
</ProgressGuideProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</HomeBadgeProvider>
|
||||
</HiddenRepliesProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</ComposerProvider>
|
||||
</AgeAssuranceProvider>
|
||||
</StatsigProvider>
|
||||
<PolicyUpdateOverlayProvider>
|
||||
<StatsigProvider>
|
||||
<AgeAssuranceProvider>
|
||||
<ComposerProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<HiddenRepliesProvider>
|
||||
<HomeBadgeProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<ProgressGuideProvider>
|
||||
<ServiceAccountManager>
|
||||
<EmailVerificationProvider>
|
||||
<HideBottomBarBorderProvider>
|
||||
<GestureHandlerRootView
|
||||
style={s.h100pct}>
|
||||
<GlobalGestureEventsProvider>
|
||||
<IntentDialogProvider>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
<ToastOutlet />
|
||||
</IntentDialogProvider>
|
||||
</GlobalGestureEventsProvider>
|
||||
</GestureHandlerRootView>
|
||||
</HideBottomBarBorderProvider>
|
||||
</EmailVerificationProvider>
|
||||
</ServiceAccountManager>
|
||||
</ProgressGuideProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</HomeBadgeProvider>
|
||||
</HiddenRepliesProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</ComposerProvider>
|
||||
</AgeAssuranceProvider>
|
||||
</StatsigProvider>
|
||||
</PolicyUpdateOverlayProvider>
|
||||
</QueryProvider>
|
||||
</React.Fragment>
|
||||
</VideoVolumeProvider>
|
||||
|
||||
+47
-41
@@ -18,6 +18,7 @@ import {Provider as A11yProvider} from '#/state/a11y'
|
||||
import {Provider as AgeAssuranceProvider} from '#/state/ageAssurance'
|
||||
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
|
||||
import {Provider as DialogStateProvider} from '#/state/dialogs'
|
||||
import {Provider as EmailVerificationProvider} from '#/state/email-verification'
|
||||
import {listenSessionDropped} from '#/state/events'
|
||||
import {
|
||||
beginResolveGeolocation,
|
||||
@@ -57,10 +58,11 @@ import {Provider as ContextMenuProvider} from '#/components/ContextMenu'
|
||||
import {NuxDialogs} from '#/components/dialogs/nuxs'
|
||||
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
||||
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
|
||||
import {Provider as PolicyUpdateOverlayProvider} from '#/components/PolicyUpdateOverlay'
|
||||
import {Provider as PortalProvider} from '#/components/Portal'
|
||||
import {Provider as ActiveVideoProvider} from '#/components/Post/Embed/VideoEmbed/ActiveVideoWebContext'
|
||||
import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
||||
import {ToastContainer} from '#/components/Toast'
|
||||
import {ToastOutlet} from '#/components/Toast'
|
||||
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||
import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottomBarBorder'
|
||||
|
||||
@@ -117,47 +119,51 @@ function InnerApp() {
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<QueryProvider currentDid={currentAccount?.did}>
|
||||
<StatsigProvider>
|
||||
<AgeAssuranceProvider>
|
||||
<ComposerProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<HiddenRepliesProvider>
|
||||
<HomeBadgeProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<SafeAreaProvider>
|
||||
<ProgressGuideProvider>
|
||||
<ServiceConfigProvider>
|
||||
<HideBottomBarBorderProvider>
|
||||
<IntentDialogProvider>
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
</IntentDialogProvider>
|
||||
</HideBottomBarBorderProvider>
|
||||
</ServiceConfigProvider>
|
||||
</ProgressGuideProvider>
|
||||
</SafeAreaProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</HomeBadgeProvider>
|
||||
</HiddenRepliesProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</ComposerProvider>
|
||||
</AgeAssuranceProvider>
|
||||
</StatsigProvider>
|
||||
<PolicyUpdateOverlayProvider>
|
||||
<StatsigProvider>
|
||||
<AgeAssuranceProvider>
|
||||
<ComposerProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<HiddenRepliesProvider>
|
||||
<HomeBadgeProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<SafeAreaProvider>
|
||||
<ProgressGuideProvider>
|
||||
<ServiceConfigProvider>
|
||||
<EmailVerificationProvider>
|
||||
<HideBottomBarBorderProvider>
|
||||
<IntentDialogProvider>
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
<ToastOutlet />
|
||||
</IntentDialogProvider>
|
||||
</HideBottomBarBorderProvider>
|
||||
</EmailVerificationProvider>
|
||||
</ServiceConfigProvider>
|
||||
</ProgressGuideProvider>
|
||||
</SafeAreaProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</HomeBadgeProvider>
|
||||
</HiddenRepliesProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</ComposerProvider>
|
||||
</AgeAssuranceProvider>
|
||||
</StatsigProvider>
|
||||
</PolicyUpdateOverlayProvider>
|
||||
</QueryProvider>
|
||||
<ToastContainer />
|
||||
</React.Fragment>
|
||||
</ActiveVideoProvider>
|
||||
</VideoVolumeProvider>
|
||||
|
||||
@@ -332,6 +332,9 @@ export const atoms = {
|
||||
font_normal: {
|
||||
fontWeight: tokens.fontWeight.normal,
|
||||
},
|
||||
font_medium: {
|
||||
fontWeight: tokens.fontWeight.medium,
|
||||
},
|
||||
font_bold: {
|
||||
fontWeight: tokens.fontWeight.bold,
|
||||
},
|
||||
@@ -979,6 +982,12 @@ export const atoms = {
|
||||
hidden: {
|
||||
display: 'none',
|
||||
},
|
||||
inline: web({
|
||||
display: 'inline',
|
||||
}),
|
||||
block: web({
|
||||
display: 'block',
|
||||
}),
|
||||
|
||||
/*
|
||||
* Transition
|
||||
|
||||
+6
-6
@@ -1,7 +1,7 @@
|
||||
import {TextStyle} from 'react-native'
|
||||
import {type TextStyle} from 'react-native'
|
||||
|
||||
import {isAndroid, isWeb} from '#/platform/detection'
|
||||
import {Device, device} from '#/storage'
|
||||
import {type Device, device} from '#/storage'
|
||||
|
||||
const WEB_FONT_FAMILIES = `system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"`
|
||||
|
||||
@@ -43,11 +43,11 @@ export function applyFonts(style: TextStyle, fontFamily: 'system' | 'theme') {
|
||||
style.fontFamily =
|
||||
{
|
||||
400: 'Inter-Regular',
|
||||
500: 'Inter-Regular',
|
||||
500: 'Inter-Medium',
|
||||
600: 'Inter-SemiBold',
|
||||
700: 'Inter-SemiBold',
|
||||
800: 'Inter-ExtraBold',
|
||||
900: 'Inter-ExtraBold',
|
||||
700: 'Inter-Bold',
|
||||
800: 'Inter-Bold',
|
||||
900: 'Inter-Bold',
|
||||
}[String(style.fontWeight || '400')] || 'Inter-Regular'
|
||||
|
||||
if (style.fontStyle === 'italic') {
|
||||
|
||||
+3
-2
@@ -8,9 +8,9 @@ import {
|
||||
setFontScale as persistFontScale,
|
||||
} from '#/alf/fonts'
|
||||
import {createThemes, defaultTheme} from '#/alf/themes'
|
||||
import {Theme, ThemeName} from '#/alf/types'
|
||||
import {type Theme, type ThemeName} from '#/alf/types'
|
||||
import {BLUE_HUE, GREEN_HUE, RED_HUE} from '#/alf/util/colorGeneration'
|
||||
import {Device} from '#/storage'
|
||||
import {type Device} from '#/storage'
|
||||
|
||||
export {atoms} from '#/alf/atoms'
|
||||
export * from '#/alf/breakpoints'
|
||||
@@ -61,6 +61,7 @@ export const Context = React.createContext<Alf>({
|
||||
},
|
||||
flags: {},
|
||||
})
|
||||
Context.displayName = 'AlfContext'
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
|
||||
@@ -53,6 +53,7 @@ export const borderRadius = {
|
||||
*/
|
||||
export const fontWeight = {
|
||||
normal: '400',
|
||||
medium: '500',
|
||||
bold: '600',
|
||||
heavy: '800',
|
||||
} as const
|
||||
|
||||
@@ -11,9 +11,11 @@ export function DO_NOT_USE() {
|
||||
return useFonts({
|
||||
'Inter-Regular': require('../../../assets/fonts/inter/Inter-Regular.otf'),
|
||||
'Inter-Italic': require('../../../assets/fonts/inter/Inter-Italic.otf'),
|
||||
'Inter-Bold': require('../../../assets/fonts/inter/Inter-SemiBold.otf'),
|
||||
'Inter-BoldItalic': require('../../../assets/fonts/inter/Inter-SemiBoldItalic.otf'),
|
||||
'Inter-Black': require('../../../assets/fonts/inter/Inter-ExtraBold.otf'),
|
||||
'Inter-BlackItalic': require('../../../assets/fonts/inter/Inter-ExtraBoldItalic.otf'),
|
||||
'Inter-Medium': require('../../../assets/fonts/inter/Inter-Medium.otf'),
|
||||
'Inter-MediumItalic': require('../../../assets/fonts/inter/Inter-MediumItalic.otf'),
|
||||
'Inter-SemiBold': require('../../../assets/fonts/inter/Inter-SemiBold.otf'),
|
||||
'Inter-SemiBoldItalic': require('../../../assets/fonts/inter/Inter-SemiBoldItalic.otf'),
|
||||
'Inter-Bold': require('../../../assets/fonts/inter/Inter-Bold.otf'),
|
||||
'Inter-BoldItalic': require('../../../assets/fonts/inter/Inter-BoldItalic.otf'),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ type Context = {
|
||||
const Context = createContext<Context>({
|
||||
type: 'info',
|
||||
})
|
||||
Context.displayName = 'AdmonitionContext'
|
||||
|
||||
export function Icon() {
|
||||
const t = useTheme()
|
||||
|
||||
+110
-66
@@ -37,7 +37,8 @@ export type ButtonColor =
|
||||
| 'secondary'
|
||||
| 'secondary_inverted'
|
||||
| 'negative'
|
||||
| 'negative_secondary'
|
||||
| 'primary_subtle'
|
||||
| 'negative_subtle'
|
||||
export type ButtonSize = 'tiny' | 'small' | 'large'
|
||||
export type ButtonShape = 'round' | 'square' | 'default'
|
||||
export type VariantProps = {
|
||||
@@ -108,6 +109,7 @@ const Context = React.createContext<VariantProps & ButtonState>({
|
||||
pressed: false,
|
||||
disabled: false,
|
||||
})
|
||||
Context.displayName = 'ButtonContext'
|
||||
|
||||
export function useButtonContext() {
|
||||
return React.useContext(Context)
|
||||
@@ -233,19 +235,15 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({
|
||||
backgroundColor: select(t.name, {
|
||||
light: t.palette.primary_700,
|
||||
dim: t.palette.primary_300,
|
||||
dark: t.palette.primary_300,
|
||||
}),
|
||||
backgroundColor: t.palette.primary_200,
|
||||
})
|
||||
}
|
||||
} else if (color === 'secondary') {
|
||||
if (!disabled) {
|
||||
baseStyles.push(t.atoms.bg_contrast_25)
|
||||
hoverStyles.push(t.atoms.bg_contrast_50)
|
||||
hoverStyles.push(t.atoms.bg_contrast_100)
|
||||
} else {
|
||||
baseStyles.push(t.atoms.bg_contrast_100)
|
||||
baseStyles.push(t.atoms.bg_contrast_50)
|
||||
}
|
||||
} else if (color === 'secondary_inverted') {
|
||||
if (!disabled) {
|
||||
@@ -253,7 +251,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
backgroundColor: t.palette.contrast_900,
|
||||
})
|
||||
hoverStyles.push({
|
||||
backgroundColor: t.palette.contrast_950,
|
||||
backgroundColor: t.palette.contrast_975,
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({
|
||||
@@ -269,15 +267,36 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
backgroundColor: t.palette.negative_600,
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({
|
||||
backgroundColor: t.palette.negative_700,
|
||||
})
|
||||
}
|
||||
} else if (color === 'primary_subtle') {
|
||||
if (!disabled) {
|
||||
baseStyles.push({
|
||||
backgroundColor: select(t.name, {
|
||||
light: t.palette.negative_700,
|
||||
dim: t.palette.negative_300,
|
||||
dark: t.palette.negative_300,
|
||||
light: t.palette.primary_50,
|
||||
dim: t.palette.primary_100,
|
||||
dark: t.palette.primary_100,
|
||||
}),
|
||||
})
|
||||
hoverStyles.push({
|
||||
backgroundColor: select(t.name, {
|
||||
light: t.palette.primary_100,
|
||||
dim: t.palette.primary_200,
|
||||
dark: t.palette.primary_200,
|
||||
}),
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({
|
||||
backgroundColor: select(t.name, {
|
||||
light: t.palette.primary_25,
|
||||
dim: t.palette.primary_50,
|
||||
dark: t.palette.primary_50,
|
||||
}),
|
||||
})
|
||||
}
|
||||
} else if (color === 'negative_secondary') {
|
||||
} else if (color === 'negative_subtle') {
|
||||
if (!disabled) {
|
||||
baseStyles.push({
|
||||
backgroundColor: select(t.name, {
|
||||
@@ -296,7 +315,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
} else {
|
||||
baseStyles.push({
|
||||
backgroundColor: select(t.name, {
|
||||
light: t.palette.negative_100,
|
||||
light: t.palette.negative_25,
|
||||
dim: t.palette.negative_50,
|
||||
dark: t.palette.negative_50,
|
||||
}),
|
||||
@@ -407,7 +426,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if (color === 'negative_secondary') {
|
||||
} else if (color === 'negative_subtle') {
|
||||
if (variant === 'outline') {
|
||||
baseStyles.push(a.border, t.atoms.bg, {
|
||||
borderWidth: 1,
|
||||
@@ -442,44 +461,48 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
if (shape === 'default') {
|
||||
if (size === 'large') {
|
||||
baseStyles.push({
|
||||
paddingVertical: 13,
|
||||
paddingHorizontal: 20,
|
||||
borderRadius: 8,
|
||||
gap: 8,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 25,
|
||||
borderRadius: 10,
|
||||
gap: 3,
|
||||
})
|
||||
} else if (size === 'small') {
|
||||
baseStyles.push({
|
||||
paddingVertical: 9,
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 6,
|
||||
gap: 6,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 13,
|
||||
borderRadius: 8,
|
||||
gap: 3,
|
||||
})
|
||||
} else if (size === 'tiny') {
|
||||
baseStyles.push({
|
||||
paddingVertical: 4,
|
||||
paddingHorizontal: 8,
|
||||
borderRadius: 4,
|
||||
gap: 4,
|
||||
paddingVertical: 5,
|
||||
paddingHorizontal: 9,
|
||||
borderRadius: 6,
|
||||
gap: 2,
|
||||
})
|
||||
}
|
||||
} else if (shape === 'round' || shape === 'square') {
|
||||
/*
|
||||
* These sizes match the actual rendered size on screen, based on
|
||||
* Chrome's web inspector
|
||||
*/
|
||||
if (size === 'large') {
|
||||
if (shape === 'round') {
|
||||
baseStyles.push({height: 46, width: 46})
|
||||
baseStyles.push({height: 44, width: 44})
|
||||
} else {
|
||||
baseStyles.push({height: 44, width: 44})
|
||||
}
|
||||
} else if (size === 'small') {
|
||||
if (shape === 'round') {
|
||||
baseStyles.push({height: 34, width: 34})
|
||||
baseStyles.push({height: 33, width: 33})
|
||||
} else {
|
||||
baseStyles.push({height: 34, width: 34})
|
||||
baseStyles.push({height: 33, width: 33})
|
||||
}
|
||||
} else if (size === 'tiny') {
|
||||
if (shape === 'round') {
|
||||
baseStyles.push({height: 22, width: 22})
|
||||
baseStyles.push({height: 25, width: 25})
|
||||
} else {
|
||||
baseStyles.push({height: 21, width: 21})
|
||||
baseStyles.push({height: 25, width: 25})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -487,7 +510,9 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
baseStyles.push(a.rounded_full)
|
||||
} else if (shape === 'square') {
|
||||
if (size === 'tiny') {
|
||||
baseStyles.push(a.rounded_xs)
|
||||
baseStyles.push({
|
||||
borderRadius: 6,
|
||||
})
|
||||
} else {
|
||||
baseStyles.push(a.rounded_sm)
|
||||
}
|
||||
@@ -531,6 +556,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
a.curve_continuous,
|
||||
flattenedBaseStyles,
|
||||
...(state.hovered || state.pressed
|
||||
? [hoverStyles, flatten(hoverStyleProp)]
|
||||
@@ -567,51 +593,70 @@ export function useSharedButtonTextStyles() {
|
||||
if (!disabled) {
|
||||
baseStyles.push({color: t.palette.white})
|
||||
} else {
|
||||
baseStyles.push({color: t.palette.white, opacity: 0.5})
|
||||
baseStyles.push({
|
||||
color: select(t.name, {
|
||||
light: t.palette.white,
|
||||
dim: t.atoms.text_inverted.color,
|
||||
dark: t.atoms.text_inverted.color,
|
||||
}),
|
||||
})
|
||||
}
|
||||
} else if (color === 'secondary') {
|
||||
if (!disabled) {
|
||||
baseStyles.push({
|
||||
color: t.palette.contrast_700,
|
||||
})
|
||||
baseStyles.push(t.atoms.text_contrast_medium)
|
||||
} else {
|
||||
baseStyles.push({
|
||||
color: t.palette.contrast_400,
|
||||
color: t.palette.contrast_300,
|
||||
})
|
||||
}
|
||||
} else if (color === 'secondary_inverted') {
|
||||
if (!disabled) {
|
||||
baseStyles.push({
|
||||
color: t.palette.contrast_50,
|
||||
})
|
||||
baseStyles.push(t.atoms.text_inverted)
|
||||
} else {
|
||||
baseStyles.push({
|
||||
color: t.palette.contrast_400,
|
||||
color: t.palette.contrast_300,
|
||||
})
|
||||
}
|
||||
} else if (color === 'negative') {
|
||||
if (!disabled) {
|
||||
baseStyles.push({color: t.palette.white})
|
||||
} else {
|
||||
baseStyles.push({color: t.palette.white, opacity: 0.5})
|
||||
baseStyles.push({color: t.palette.negative_300})
|
||||
}
|
||||
} else if (color === 'negative_secondary') {
|
||||
} else if (color === 'primary_subtle') {
|
||||
if (!disabled) {
|
||||
baseStyles.push({
|
||||
color: select(t.name, {
|
||||
light: t.palette.negative_500,
|
||||
dim: t.palette.negative_950,
|
||||
dark: t.palette.negative_900,
|
||||
light: t.palette.primary_600,
|
||||
dim: t.palette.primary_800,
|
||||
dark: t.palette.primary_800,
|
||||
}),
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({
|
||||
color: select(t.name, {
|
||||
light: t.palette.negative_500,
|
||||
dim: t.palette.negative_700,
|
||||
dark: t.palette.negative_700,
|
||||
light: t.palette.primary_200,
|
||||
dim: t.palette.primary_200,
|
||||
dark: t.palette.primary_200,
|
||||
}),
|
||||
})
|
||||
}
|
||||
} else if (color === 'negative_subtle') {
|
||||
if (!disabled) {
|
||||
baseStyles.push({
|
||||
color: select(t.name, {
|
||||
light: t.palette.negative_600,
|
||||
dim: t.palette.negative_800,
|
||||
dark: t.palette.negative_800,
|
||||
}),
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({
|
||||
color: select(t.name, {
|
||||
light: t.palette.negative_200,
|
||||
dim: t.palette.negative_200,
|
||||
dark: t.palette.negative_200,
|
||||
}),
|
||||
opacity: 0.5,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -693,7 +738,7 @@ export function useSharedButtonTextStyles() {
|
||||
baseStyles.push({color: t.palette.negative_400, opacity: 0.5})
|
||||
}
|
||||
}
|
||||
} else if (color === 'negative_secondary') {
|
||||
} else if (color === 'negative_subtle') {
|
||||
if (variant === 'outline') {
|
||||
if (!disabled) {
|
||||
baseStyles.push({color: t.palette.negative_400})
|
||||
@@ -714,11 +759,11 @@ export function useSharedButtonTextStyles() {
|
||||
}
|
||||
|
||||
if (size === 'large') {
|
||||
baseStyles.push(a.text_md, a.leading_tight)
|
||||
baseStyles.push(a.text_md, a.leading_snug, a.font_medium)
|
||||
} else if (size === 'small') {
|
||||
baseStyles.push(a.text_sm, a.leading_tight)
|
||||
baseStyles.push(a.text_sm, a.leading_snug, a.font_medium)
|
||||
} else if (size === 'tiny') {
|
||||
baseStyles.push(a.text_xs, a.leading_tight)
|
||||
baseStyles.push(a.text_xs, a.leading_snug, a.font_medium)
|
||||
}
|
||||
|
||||
return StyleSheet.flatten(baseStyles)
|
||||
@@ -729,7 +774,7 @@ export function ButtonText({children, style, ...rest}: ButtonTextProps) {
|
||||
const textStyles = useSharedButtonTextStyles()
|
||||
|
||||
return (
|
||||
<Text {...rest} style={[a.font_bold, a.text_center, textStyles, style]}>
|
||||
<Text {...rest} style={[a.text_center, textStyles, style]}>
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
@@ -737,14 +782,16 @@ export function ButtonText({children, style, ...rest}: ButtonTextProps) {
|
||||
|
||||
export function ButtonIcon({
|
||||
icon: Comp,
|
||||
position,
|
||||
size,
|
||||
}: {
|
||||
icon: React.ComponentType<SVGIconProps>
|
||||
/**
|
||||
* @deprecated no longer needed
|
||||
*/
|
||||
position?: 'left' | 'right'
|
||||
size?: SVGIconProps['size']
|
||||
}) {
|
||||
const {size: buttonSize, disabled} = useButtonContext()
|
||||
const {size: buttonSize} = useButtonContext()
|
||||
const textStyles = useSharedButtonTextStyles()
|
||||
const {iconSize, iconContainerSize} = React.useMemo(() => {
|
||||
/**
|
||||
@@ -753,7 +800,7 @@ export function ButtonIcon({
|
||||
const iconSizeShorthand =
|
||||
size ??
|
||||
(({
|
||||
large: 'sm',
|
||||
large: 'md',
|
||||
small: 'sm',
|
||||
tiny: 'xs',
|
||||
}[buttonSize || 'small'] || 'sm') as Exclude<
|
||||
@@ -768,7 +815,7 @@ export function ButtonIcon({
|
||||
const iconSize = {
|
||||
xs: 12,
|
||||
sm: 16,
|
||||
md: 20,
|
||||
md: 18,
|
||||
lg: 24,
|
||||
xl: 28,
|
||||
'2xl': 32,
|
||||
@@ -779,9 +826,9 @@ export function ButtonIcon({
|
||||
* don't increase button size
|
||||
*/
|
||||
const iconContainerSize = {
|
||||
large: 18,
|
||||
small: 16,
|
||||
tiny: 13,
|
||||
large: 20,
|
||||
small: 17,
|
||||
tiny: 15,
|
||||
}[buttonSize || 'small']
|
||||
|
||||
return {
|
||||
@@ -797,9 +844,6 @@ export function ButtonIcon({
|
||||
{
|
||||
width: iconContainerSize,
|
||||
height: iconContainerSize,
|
||||
opacity: disabled ? 0.7 : 1,
|
||||
marginLeft: position === 'left' ? -2 : 0,
|
||||
marginRight: position === 'right' ? -2 : 0,
|
||||
},
|
||||
]}>
|
||||
<View
|
||||
|
||||
@@ -7,10 +7,13 @@ import {
|
||||
} from '#/components/ContextMenu/types'
|
||||
|
||||
export const Context = React.createContext<ContextType | null>(null)
|
||||
Context.displayName = 'ContextMenuContext'
|
||||
|
||||
export const MenuContext = React.createContext<MenuContextType | null>(null)
|
||||
MenuContext.displayName = 'ContextMenuMenuContext'
|
||||
|
||||
export const ItemContext = React.createContext<ItemContextType | null>(null)
|
||||
ItemContext.displayName = 'ContextMenuItemContext'
|
||||
|
||||
export function useContextMenuContext() {
|
||||
const context = React.useContext(Context)
|
||||
|
||||
@@ -23,6 +23,7 @@ export const Context = createContext<DialogContextProps>({
|
||||
setDisableDrag: () => {},
|
||||
isWithinDialog: false,
|
||||
})
|
||||
Context.displayName = 'DialogContext'
|
||||
|
||||
export function useDialogContext() {
|
||||
return useContext(Context)
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import {
|
||||
Children,
|
||||
cloneElement,
|
||||
isValidElement,
|
||||
type ReactElement,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import {
|
||||
AccessibilityInfo,
|
||||
findNodeHandle,
|
||||
Pressable,
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useA11y} from '#/state/a11y'
|
||||
|
||||
/**
|
||||
* Conditionally wraps children in a `FocusTrap` component based on whether
|
||||
* screen reader support is enabled. THIS SHOULD BE USED SPARINGLY, only when
|
||||
* no better option is available.
|
||||
*/
|
||||
export function FocusScope({children}: {children: ReactNode}) {
|
||||
const {screenReaderEnabled} = useA11y()
|
||||
|
||||
return screenReaderEnabled ? <FocusTrap>{children}</FocusTrap> : children
|
||||
}
|
||||
|
||||
/**
|
||||
* `FocusTrap` is intended as a last-ditch effort to ensure that users keep
|
||||
* focus within a certain section of the app, like an overlay.
|
||||
*
|
||||
* It works by placing "guards" at the start and end of the active content.
|
||||
* Then when the user reaches either of those guards, it will announce that
|
||||
* they have reached the start or end of the content and tell them how to
|
||||
* remain within the active content section.
|
||||
*/
|
||||
function FocusTrap({children}: {children: ReactNode}) {
|
||||
const {_} = useLingui()
|
||||
const child = useRef<View>(null)
|
||||
|
||||
/*
|
||||
* Here we add a ref to the first child of this component. This currently
|
||||
* overrides any ref already on that first child, so we throw an error here
|
||||
* to prevent us from ever accidentally doing this.
|
||||
*/
|
||||
const decoratedChildren = useMemo(() => {
|
||||
return Children.toArray(children).map((node, i) => {
|
||||
if (i === 0 && isValidElement(node)) {
|
||||
const n = node as ReactElement<any>
|
||||
if (n.props.ref !== undefined) {
|
||||
throw new Error(
|
||||
'FocusScope needs to override the ref on its first child.',
|
||||
)
|
||||
}
|
||||
return cloneElement(n, {
|
||||
...n.props,
|
||||
ref: child,
|
||||
})
|
||||
}
|
||||
return node
|
||||
})
|
||||
}, [children])
|
||||
|
||||
const focusNode = useCallback((ref: View | null) => {
|
||||
if (!ref) return
|
||||
const node = findNodeHandle(ref)
|
||||
if (node) {
|
||||
AccessibilityInfo.setAccessibilityFocus(node)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
focusNode(child.current)
|
||||
}, 1e3)
|
||||
}, [focusNode])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Pressable
|
||||
accessible
|
||||
accessibilityLabel={_(
|
||||
msg`You've reached the start of the active content.`,
|
||||
)}
|
||||
accessibilityHint={_(
|
||||
msg`Please go back, or activate this element to return to the start of the active content.`,
|
||||
)}
|
||||
accessibilityActions={[{name: 'activate', label: 'activate'}]}
|
||||
onAccessibilityAction={event => {
|
||||
switch (event.nativeEvent.actionName) {
|
||||
case 'activate': {
|
||||
focusNode(child.current)
|
||||
}
|
||||
}
|
||||
}}>
|
||||
<Noop />
|
||||
</Pressable>
|
||||
<View
|
||||
/**
|
||||
* This property traps focus effectively on iOS, but not on Android.
|
||||
*/
|
||||
accessibilityViewIsModal>
|
||||
{decoratedChildren}
|
||||
</View>
|
||||
<Pressable
|
||||
accessibilityLabel={_(
|
||||
msg`You've reached the end of the active content.`,
|
||||
)}
|
||||
accessibilityHint={_(
|
||||
msg`Please go back, or activate this element to return to the start of the active content.`,
|
||||
)}
|
||||
accessibilityActions={[{name: 'activate', label: 'activate'}]}
|
||||
onAccessibilityAction={event => {
|
||||
switch (event.nativeEvent.actionName) {
|
||||
case 'activate': {
|
||||
focusNode(child.current)
|
||||
}
|
||||
}
|
||||
}}>
|
||||
<Noop />
|
||||
</Pressable>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function Noop() {
|
||||
return (
|
||||
<Text
|
||||
accessible={false}
|
||||
style={{
|
||||
height: 1,
|
||||
opacity: 0,
|
||||
}}>
|
||||
{' '}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import {type ReactNode} from 'react'
|
||||
import {FocusScope as RadixFocusScope} from 'radix-ui/internal'
|
||||
|
||||
/*
|
||||
* The web version of the FocusScope component is a proper implementation, we
|
||||
* use this in Dialogs and such already. It's here as a convenient counterpart
|
||||
* to the hacky native solution.
|
||||
*/
|
||||
export function FocusScope({children}: {children: ReactNode}) {
|
||||
return (
|
||||
<RadixFocusScope.FocusScope loop asChild trapped>
|
||||
{children}
|
||||
</RadixFocusScope.FocusScope>
|
||||
)
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import {createContext, useContext, useMemo} from 'react'
|
||||
import {View} from 'react-native'
|
||||
|
||||
import {atoms as a, ViewStyleProp} from '#/alf'
|
||||
import {atoms as a, type ViewStyleProp} from '#/alf'
|
||||
|
||||
const Context = createContext({
|
||||
gap: 0,
|
||||
})
|
||||
Context.displayName = 'GridContext'
|
||||
|
||||
export function Row({
|
||||
children,
|
||||
|
||||
@@ -77,6 +77,7 @@ export function Outer({
|
||||
}
|
||||
|
||||
const AlignmentContext = createContext<'platform' | 'left'>('platform')
|
||||
AlignmentContext.displayName = 'AlignmentContext'
|
||||
|
||||
export function Content({
|
||||
children,
|
||||
|
||||
@@ -3,3 +3,4 @@ import React from 'react'
|
||||
export const ScrollbarOffsetContext = React.createContext({
|
||||
isWithinOffsetView: false,
|
||||
})
|
||||
ScrollbarOffsetContext.displayName = 'ScrollbarOffsetContext'
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export function LockScroll() {
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import {RemoveScrollBar} from 'react-remove-scroll-bar'
|
||||
|
||||
export const LockScroll = RemoveScrollBar
|
||||
@@ -3,8 +3,10 @@ import React from 'react'
|
||||
import {type ContextType, type ItemContextType} from '#/components/Menu/types'
|
||||
|
||||
export const Context = React.createContext<ContextType | null>(null)
|
||||
Context.displayName = 'MenuContext'
|
||||
|
||||
export const ItemContext = React.createContext<ItemContextType | null>(null)
|
||||
ItemContext.displayName = 'MenuItemContext'
|
||||
|
||||
export function useMenuContext() {
|
||||
const context = React.useContext(Context)
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import {View} from 'react-native'
|
||||
import {Trans} from '@lingui/macro'
|
||||
|
||||
import {Logo} from '#/view/icons/Logo'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function Badge() {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<View style={[a.align_start]}>
|
||||
<View
|
||||
style={[
|
||||
a.pl_md,
|
||||
a.pr_lg,
|
||||
a.py_sm,
|
||||
a.rounded_full,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.gap_xs,
|
||||
{
|
||||
backgroundColor: t.palette.primary_25,
|
||||
},
|
||||
]}>
|
||||
<Logo fill={t.palette.primary_600} width={14} />
|
||||
<Text
|
||||
style={[
|
||||
a.font_bold,
|
||||
{
|
||||
color: t.palette.primary_600,
|
||||
},
|
||||
]}>
|
||||
<Trans>Announcement</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import {type ReactNode} from 'react'
|
||||
import {ScrollView, View} from 'react-native'
|
||||
import {
|
||||
useSafeAreaFrame,
|
||||
useSafeAreaInsets,
|
||||
} from 'react-native-safe-area-context'
|
||||
import {LinearGradient} from 'expo-linear-gradient'
|
||||
|
||||
import {isAndroid, isNative} from '#/platform/detection'
|
||||
import {useA11y} from '#/state/a11y'
|
||||
import {atoms as a, flatten, useBreakpoints, useTheme, web} from '#/alf'
|
||||
import {transparentifyColor} from '#/alf/util/colorGeneration'
|
||||
import {FocusScope} from '#/components/FocusScope'
|
||||
import {LockScroll} from '#/components/LockScroll'
|
||||
|
||||
const GUTTER = 24
|
||||
|
||||
export function Overlay({
|
||||
children,
|
||||
label,
|
||||
}: {
|
||||
children: ReactNode
|
||||
label: string
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {gtPhone} = useBreakpoints()
|
||||
const {reduceMotionEnabled} = useA11y()
|
||||
const insets = useSafeAreaInsets()
|
||||
const frame = useSafeAreaFrame()
|
||||
|
||||
return (
|
||||
<>
|
||||
<LockScroll />
|
||||
|
||||
<View style={[a.fixed, a.inset_0, !reduceMotionEnabled && a.fade_in]}>
|
||||
{gtPhone ? (
|
||||
<View style={[a.absolute, a.inset_0, {opacity: 0.8}]}>
|
||||
<View
|
||||
style={[
|
||||
a.fixed,
|
||||
a.inset_0,
|
||||
{backgroundColor: t.palette.black},
|
||||
!reduceMotionEnabled && a.fade_in,
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<LinearGradient
|
||||
colors={[
|
||||
transparentifyColor(t.atoms.bg.backgroundColor, 0),
|
||||
t.atoms.bg.backgroundColor,
|
||||
t.atoms.bg.backgroundColor,
|
||||
]}
|
||||
start={[0.5, 0]}
|
||||
end={[0.5, 1]}
|
||||
style={[a.absolute, a.inset_0]}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
style={[
|
||||
a.z_10,
|
||||
gtPhone &&
|
||||
web({
|
||||
paddingHorizontal: GUTTER,
|
||||
paddingVertical: '10vh',
|
||||
}),
|
||||
]}
|
||||
contentContainerStyle={[a.align_center]}>
|
||||
{/**
|
||||
* This is needed to prevent centered dialogs from overflowing
|
||||
* above the screen, and provides a "natural" centering so that
|
||||
* stacked dialogs appear relatively aligned.
|
||||
*/}
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.z_20,
|
||||
a.align_center,
|
||||
!gtPhone && [a.justify_end, {minHeight: frame.height}],
|
||||
isNative && [
|
||||
{
|
||||
paddingBottom: Math.max(insets.bottom, a.p_2xl.padding),
|
||||
},
|
||||
],
|
||||
]}>
|
||||
{!gtPhone && (
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.w_full,
|
||||
{
|
||||
minHeight: Math.max(insets.top, a.p_2xl.padding),
|
||||
},
|
||||
]}>
|
||||
<LinearGradient
|
||||
colors={[
|
||||
transparentifyColor(t.atoms.bg.backgroundColor, 0),
|
||||
t.atoms.bg.backgroundColor,
|
||||
]}
|
||||
start={[0.5, 0]}
|
||||
end={[0.5, 1]}
|
||||
style={[a.absolute, a.inset_0]}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<FocusScope>
|
||||
<View
|
||||
accessible={isAndroid}
|
||||
role="dialog"
|
||||
aria-role="dialog"
|
||||
aria-label={label}
|
||||
style={flatten([
|
||||
a.relative,
|
||||
a.w_full,
|
||||
a.p_2xl,
|
||||
t.atoms.bg,
|
||||
!reduceMotionEnabled && a.zoom_fade_in,
|
||||
gtPhone && [
|
||||
a.rounded_md,
|
||||
a.border,
|
||||
t.atoms.shadow_lg,
|
||||
t.atoms.border_contrast_low,
|
||||
web({
|
||||
maxWidth: 420,
|
||||
}),
|
||||
],
|
||||
])}>
|
||||
{children}
|
||||
</View>
|
||||
</FocusScope>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import {createPortalGroup} from '#/components/Portal'
|
||||
|
||||
const portalGroup = createPortalGroup()
|
||||
|
||||
export const Provider = portalGroup.Provider
|
||||
export const Portal = portalGroup.Portal
|
||||
export const Outlet = portalGroup.Outlet
|
||||
@@ -0,0 +1,195 @@
|
||||
import {describe, test} from '@jest/globals'
|
||||
|
||||
import {
|
||||
computeCompletedState,
|
||||
syncCompletedState,
|
||||
} from '#/components/PolicyUpdateOverlay/usePolicyUpdateState'
|
||||
|
||||
jest.mock('../../../state/queries/nuxs')
|
||||
|
||||
describe('computeCompletedState', () => {
|
||||
test(`initial state`, () => {
|
||||
const completed = computeCompletedState({
|
||||
nuxIsReady: false,
|
||||
nuxIsCompleted: false,
|
||||
nuxIsOptimisticallyCompleted: false,
|
||||
completedForDevice: undefined,
|
||||
})
|
||||
|
||||
expect(completed).toBe(true)
|
||||
})
|
||||
|
||||
test(`nux loaded state`, () => {
|
||||
const completed = computeCompletedState({
|
||||
nuxIsReady: true,
|
||||
nuxIsCompleted: false,
|
||||
nuxIsOptimisticallyCompleted: false,
|
||||
completedForDevice: undefined,
|
||||
})
|
||||
|
||||
expect(completed).toBe(false)
|
||||
})
|
||||
|
||||
test(`nux saving state`, () => {
|
||||
const completed = computeCompletedState({
|
||||
nuxIsReady: true,
|
||||
nuxIsCompleted: false,
|
||||
nuxIsOptimisticallyCompleted: true,
|
||||
completedForDevice: undefined,
|
||||
})
|
||||
|
||||
expect(completed).toBe(true)
|
||||
})
|
||||
|
||||
test(`nux is completed`, () => {
|
||||
const completed = computeCompletedState({
|
||||
nuxIsReady: true,
|
||||
nuxIsCompleted: true,
|
||||
nuxIsOptimisticallyCompleted: false,
|
||||
completedForDevice: undefined,
|
||||
})
|
||||
|
||||
expect(completed).toBe(true)
|
||||
})
|
||||
|
||||
test(`initial state, but already completed for device`, () => {
|
||||
const completed = computeCompletedState({
|
||||
nuxIsReady: false,
|
||||
nuxIsCompleted: false,
|
||||
nuxIsOptimisticallyCompleted: false,
|
||||
completedForDevice: true,
|
||||
})
|
||||
|
||||
expect(completed).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('syncCompletedState', () => {
|
||||
describe('!nuxIsReady', () => {
|
||||
test(`!completedForDevice, no-op`, () => {
|
||||
const save = jest.fn()
|
||||
const setCompletedForDevice = jest.fn()
|
||||
syncCompletedState({
|
||||
nuxIsReady: false,
|
||||
nuxIsCompleted: false,
|
||||
nuxIsOptimisticallyCompleted: false,
|
||||
completedForDevice: false,
|
||||
save,
|
||||
setCompletedForDevice,
|
||||
})
|
||||
|
||||
expect(save).not.toHaveBeenCalled()
|
||||
expect(setCompletedForDevice).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test(`completedForDevice, no-op`, () => {
|
||||
const save = jest.fn()
|
||||
const setCompletedForDevice = jest.fn()
|
||||
syncCompletedState({
|
||||
nuxIsReady: false,
|
||||
nuxIsCompleted: false,
|
||||
nuxIsOptimisticallyCompleted: false,
|
||||
completedForDevice: true,
|
||||
save,
|
||||
setCompletedForDevice,
|
||||
})
|
||||
|
||||
expect(save).not.toHaveBeenCalled()
|
||||
expect(setCompletedForDevice).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('nuxIsReady', () => {
|
||||
describe(`!nuxIsCompleted`, () => {
|
||||
describe(`!nuxIsOptimisticallyCompleted`, () => {
|
||||
test(`!completedForDevice, no-op`, () => {
|
||||
const save = jest.fn()
|
||||
const setCompletedForDevice = jest.fn()
|
||||
syncCompletedState({
|
||||
nuxIsReady: true,
|
||||
nuxIsCompleted: false,
|
||||
nuxIsOptimisticallyCompleted: false,
|
||||
completedForDevice: false,
|
||||
save,
|
||||
setCompletedForDevice,
|
||||
})
|
||||
|
||||
expect(save).not.toHaveBeenCalled()
|
||||
expect(setCompletedForDevice).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test(`completedForDevice, syncs to server`, () => {
|
||||
const save = jest.fn()
|
||||
const setCompletedForDevice = jest.fn()
|
||||
syncCompletedState({
|
||||
nuxIsReady: true,
|
||||
nuxIsCompleted: false,
|
||||
nuxIsOptimisticallyCompleted: false,
|
||||
completedForDevice: true,
|
||||
save,
|
||||
setCompletedForDevice,
|
||||
})
|
||||
|
||||
expect(save).toHaveBeenCalled()
|
||||
expect(setCompletedForDevice).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Catches the case where we already called `save` to sync device state
|
||||
* to server, thus `nuxIsOptimisticallyCompleted` is true.
|
||||
*/
|
||||
describe(`nuxIsOptimisticallyCompleted`, () => {
|
||||
test(`completedForDevice, no-op`, () => {
|
||||
const save = jest.fn()
|
||||
const setCompletedForDevice = jest.fn()
|
||||
syncCompletedState({
|
||||
nuxIsReady: true,
|
||||
nuxIsCompleted: false,
|
||||
nuxIsOptimisticallyCompleted: true,
|
||||
completedForDevice: true,
|
||||
save,
|
||||
setCompletedForDevice,
|
||||
})
|
||||
|
||||
expect(save).not.toHaveBeenCalled()
|
||||
expect(setCompletedForDevice).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe(`nuxIsCompleted`, () => {
|
||||
test(`!completedForDevice, syncs to device`, () => {
|
||||
const save = jest.fn()
|
||||
const setCompletedForDevice = jest.fn()
|
||||
syncCompletedState({
|
||||
nuxIsReady: true,
|
||||
nuxIsCompleted: true,
|
||||
nuxIsOptimisticallyCompleted: false,
|
||||
completedForDevice: false,
|
||||
save,
|
||||
setCompletedForDevice,
|
||||
})
|
||||
|
||||
expect(save).not.toHaveBeenCalled()
|
||||
expect(setCompletedForDevice).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test(`completedForDevice, no-op`, () => {
|
||||
const save = jest.fn()
|
||||
const setCompletedForDevice = jest.fn()
|
||||
syncCompletedState({
|
||||
nuxIsReady: true,
|
||||
nuxIsCompleted: true,
|
||||
nuxIsOptimisticallyCompleted: false,
|
||||
completedForDevice: true,
|
||||
save,
|
||||
setCompletedForDevice,
|
||||
})
|
||||
|
||||
expect(save).not.toHaveBeenCalled()
|
||||
expect(setCompletedForDevice).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
import {ID} from '#/components/PolicyUpdateOverlay/updates/202508/config'
|
||||
|
||||
/**
|
||||
* The singulary active update ID. This is configured here to ensure that
|
||||
* the relationship is clear.
|
||||
*/
|
||||
export const ACTIVE_UPDATE_ID = ID
|
||||
@@ -0,0 +1,68 @@
|
||||
import {
|
||||
createContext,
|
||||
type ReactNode,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
|
||||
import {useSession} from '#/state/session'
|
||||
import {Provider as PortalProvider} from '#/components/PolicyUpdateOverlay/Portal'
|
||||
import {
|
||||
type PolicyUpdateState,
|
||||
usePolicyUpdateState,
|
||||
} from '#/components/PolicyUpdateOverlay/usePolicyUpdateState'
|
||||
|
||||
const Context = createContext<{
|
||||
state: PolicyUpdateState
|
||||
setIsReadyToShowOverlay: () => void
|
||||
}>({
|
||||
state: {
|
||||
completed: true,
|
||||
complete: () => {},
|
||||
},
|
||||
/**
|
||||
* Although our data will be ready to go when the app shell mounts, we don't
|
||||
* want to show the overlay until we actually render it, which happens after
|
||||
* sigin/signup/onboarding in `createNativeStackNavigatorWithAuth`.
|
||||
*/
|
||||
setIsReadyToShowOverlay: () => {},
|
||||
})
|
||||
Context.displayName = 'PolicyUpdateOverlayContext'
|
||||
|
||||
export function usePolicyUpdateContext() {
|
||||
const context = useContext(Context)
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'usePolicyUpdateContext must be used within a PolicyUpdateProvider',
|
||||
)
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
export function Provider({children}: {children?: ReactNode}) {
|
||||
const {hasSession} = useSession()
|
||||
const [isReadyToShowOverlay, setIsReadyToShowOverlay] = useState(false)
|
||||
const state = usePolicyUpdateState({
|
||||
// only enable the policy update overlay in non-test environments
|
||||
enabled:
|
||||
isReadyToShowOverlay && hasSession && process.env.NODE_ENV !== 'test',
|
||||
})
|
||||
|
||||
const ctx = useMemo(
|
||||
() => ({
|
||||
state,
|
||||
setIsReadyToShowOverlay() {
|
||||
if (isReadyToShowOverlay) return
|
||||
setIsReadyToShowOverlay(true)
|
||||
},
|
||||
}),
|
||||
[state, isReadyToShowOverlay, setIsReadyToShowOverlay],
|
||||
)
|
||||
|
||||
return (
|
||||
<PortalProvider>
|
||||
<Context.Provider value={ctx}>{children}</Context.Provider>
|
||||
</PortalProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import {useEffect} from 'react'
|
||||
import {View} from 'react-native'
|
||||
|
||||
import {isIOS} from '#/platform/detection'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {FullWindowOverlay} from '#/components/FullWindowOverlay'
|
||||
import {usePolicyUpdateContext} from '#/components/PolicyUpdateOverlay/context'
|
||||
import {Portal} from '#/components/PolicyUpdateOverlay/Portal'
|
||||
import {Content} from '#/components/PolicyUpdateOverlay/updates/202508'
|
||||
|
||||
export {Provider} from '#/components/PolicyUpdateOverlay/context'
|
||||
export {usePolicyUpdateContext} from '#/components/PolicyUpdateOverlay/context'
|
||||
export {Outlet} from '#/components/PolicyUpdateOverlay/Portal'
|
||||
|
||||
export function PolicyUpdateOverlay() {
|
||||
const {state, setIsReadyToShowOverlay} = usePolicyUpdateContext()
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Tell the context that we are ready to show the overlay.
|
||||
*/
|
||||
setIsReadyToShowOverlay()
|
||||
}, [setIsReadyToShowOverlay])
|
||||
|
||||
/*
|
||||
* See `window.clearNux` example in `/state/queries/nuxs` for a way to clear
|
||||
* NUX state for local testing and debugging.
|
||||
*/
|
||||
|
||||
if (state.completed) return null
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<FullWindowOverlay>
|
||||
<View
|
||||
style={[
|
||||
a.fixed,
|
||||
a.inset_0,
|
||||
// setting a zIndex when using FullWindowOverlay on iOS
|
||||
// means the taps pass straight through to the underlying content (???)
|
||||
// so don't set it on iOS. FullWindowOverlay already does the job.
|
||||
!isIOS && {zIndex: 9999},
|
||||
]}>
|
||||
<Content state={state} />
|
||||
</View>
|
||||
</FullWindowOverlay>
|
||||
</Portal>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import {Logger} from '#/logger'
|
||||
|
||||
export const logger = Logger.create(Logger.Context.PolicyUpdate)
|
||||
@@ -0,0 +1,7 @@
|
||||
/*
|
||||
* Keep this file separate to avoid import issues.
|
||||
*/
|
||||
|
||||
import {Nux} from '#/state/queries/nuxs'
|
||||
|
||||
export const ID = Nux.PolicyUpdate202508
|
||||
@@ -0,0 +1,190 @@
|
||||
import {useCallback} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {isAndroid} from '#/platform/detection'
|
||||
import {useA11y} from '#/state/a11y'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import {InlineLinkText, Link} from '#/components/Link'
|
||||
import {Badge} from '#/components/PolicyUpdateOverlay/Badge'
|
||||
import {Overlay} from '#/components/PolicyUpdateOverlay/Overlay'
|
||||
import {type PolicyUpdateState} from '#/components/PolicyUpdateOverlay/usePolicyUpdateState'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function Content({state}: {state: PolicyUpdateState}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {screenReaderEnabled} = useA11y()
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
state.complete()
|
||||
}, [state])
|
||||
|
||||
const linkStyle = [a.text_md]
|
||||
const links = {
|
||||
terms: {
|
||||
overridePresentation: false,
|
||||
to: `https://bsky.social/about/support/tos`,
|
||||
label: _(msg`Terms of Service`),
|
||||
},
|
||||
privacy: {
|
||||
overridePresentation: false,
|
||||
to: `https://bsky.social/about/support/privacy-policy`,
|
||||
label: _(msg`Privacy Policy`),
|
||||
},
|
||||
copyright: {
|
||||
overridePresentation: false,
|
||||
to: `https://bsky.social/about/support/copyright`,
|
||||
label: _(msg`Copyright Policy`),
|
||||
},
|
||||
guidelines: {
|
||||
overridePresentation: false,
|
||||
to: `https://bsky.social/about/support/community-guidelines`,
|
||||
label: _(msg`Community Guidelines`),
|
||||
},
|
||||
blog: {
|
||||
overridePresentation: false,
|
||||
to: `https://bsky.social/about/blog/08-14-2025-updated-terms-and-policies`,
|
||||
label: _(msg`Our blog post`),
|
||||
},
|
||||
}
|
||||
const linkButtonStyles = {
|
||||
overridePresentation: false,
|
||||
color: 'secondary',
|
||||
size: 'small',
|
||||
} as const
|
||||
|
||||
const label = isAndroid
|
||||
? _(
|
||||
msg`We’re updating our Terms of Service, Privacy Policy, and Copyright Policy, effective September 15th, 2025. We're also updating our Community Guidelines, and we want your input! These new guidelines will take effect on October 15th, 2025. Learn more about these changes and how to share your thoughts with us by reading our blog post.`,
|
||||
)
|
||||
: _(msg`We're updating our policies`)
|
||||
|
||||
return (
|
||||
<Overlay label={label}>
|
||||
<View style={[a.align_start, a.gap_xl]}>
|
||||
<Badge />
|
||||
|
||||
{screenReaderEnabled ? (
|
||||
<View style={[a.gap_sm]}>
|
||||
<Text emoji style={[a.text_2xl, a.font_bold, a.leading_snug]}>
|
||||
<Trans>Hey there 👋</Trans>
|
||||
</Text>
|
||||
<Text style={[a.leading_snug, a.text_md]}>
|
||||
<Trans>
|
||||
We’re updating our Terms of Service, Privacy Policy, and
|
||||
Copyright Policy, effective September 15th, 2025.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text style={[a.leading_snug, a.text_md]}>
|
||||
<Trans>
|
||||
We're also updating our Community Guidelines, and we want your
|
||||
input! These new guidelines will take effect on October 15th,
|
||||
2025.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text style={[a.leading_snug, a.text_md]}>
|
||||
<Trans>
|
||||
Learn more about these changes and how to share your thoughts
|
||||
with us by reading our blog post.
|
||||
</Trans>
|
||||
</Text>
|
||||
|
||||
<Link {...links.terms} {...linkButtonStyles}>
|
||||
<ButtonText>
|
||||
<Trans>Terms of Service</Trans>
|
||||
</ButtonText>
|
||||
</Link>
|
||||
<Link {...links.privacy} {...linkButtonStyles}>
|
||||
<ButtonText>
|
||||
<Trans>Privacy Policy</Trans>
|
||||
</ButtonText>
|
||||
</Link>
|
||||
<Link {...links.copyright} {...linkButtonStyles}>
|
||||
<ButtonText>
|
||||
<Trans>Copyright Policy</Trans>
|
||||
</ButtonText>
|
||||
</Link>
|
||||
<Link {...links.blog} {...linkButtonStyles}>
|
||||
<ButtonText>
|
||||
<Trans>Read our blog post</Trans>
|
||||
</ButtonText>
|
||||
</Link>
|
||||
</View>
|
||||
) : (
|
||||
<View style={[a.gap_sm]}>
|
||||
<Text emoji style={[a.text_2xl, a.font_bold, a.leading_snug]}>
|
||||
<Trans>Hey there 👋</Trans>
|
||||
</Text>
|
||||
<Text style={[a.leading_snug, a.text_md]}>
|
||||
<Trans>
|
||||
We’re updating our{' '}
|
||||
<InlineLinkText {...links.terms} style={linkStyle}>
|
||||
Terms of Service
|
||||
</InlineLinkText>
|
||||
,{' '}
|
||||
<InlineLinkText {...links.privacy} style={linkStyle}>
|
||||
Privacy Policy
|
||||
</InlineLinkText>
|
||||
, and{' '}
|
||||
<InlineLinkText {...links.copyright} style={linkStyle}>
|
||||
Copyright Policy
|
||||
</InlineLinkText>
|
||||
, effective September 15th, 2025.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text style={[a.leading_snug, a.text_md]}>
|
||||
<Trans>
|
||||
We're also updating our{' '}
|
||||
<InlineLinkText {...links.guidelines} style={linkStyle}>
|
||||
Community Guidelines
|
||||
</InlineLinkText>
|
||||
, and we want your input! These new guidelines will take effect
|
||||
on October 15th, 2025.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text style={[a.leading_snug, a.text_md]}>
|
||||
<Trans>
|
||||
Learn more about these changes and how to share your thoughts
|
||||
with us by{' '}
|
||||
<InlineLinkText {...links.blog} style={linkStyle}>
|
||||
reading our blog post.
|
||||
</InlineLinkText>
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={[a.w_full, a.gap_md]}>
|
||||
<Button
|
||||
label={_(msg`Continue`)}
|
||||
accessibilityHint={_(
|
||||
msg`Tap to acknowledge that you understand and agree to these updates and continue using Bluesky`,
|
||||
)}
|
||||
color="primary"
|
||||
size="large"
|
||||
onPress={handleClose}>
|
||||
<ButtonText>
|
||||
<Trans>Continue</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
|
||||
<Text
|
||||
style={[
|
||||
a.leading_snug,
|
||||
a.text_sm,
|
||||
a.italic,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
<Trans>
|
||||
By clicking "Continue" you acknowledge that you understand and
|
||||
agree to these updates.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Overlay>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import {useMemo} from 'react'
|
||||
|
||||
import {useNux, useSaveNux} from '#/state/queries/nuxs'
|
||||
import {ACTIVE_UPDATE_ID} from '#/components/PolicyUpdateOverlay/config'
|
||||
import {logger} from '#/components/PolicyUpdateOverlay/logger'
|
||||
import {IS_DEV} from '#/env'
|
||||
import {device, useStorage} from '#/storage'
|
||||
|
||||
export type PolicyUpdateState = {
|
||||
completed: boolean
|
||||
complete: () => void
|
||||
}
|
||||
|
||||
export function usePolicyUpdateState({
|
||||
enabled,
|
||||
}: {
|
||||
/**
|
||||
* Used to skip the policy update overlay until we're actually ready to
|
||||
* show it.
|
||||
*/
|
||||
enabled: boolean
|
||||
}) {
|
||||
const nux = useNux(ACTIVE_UPDATE_ID)
|
||||
const {mutate: save, variables} = useSaveNux()
|
||||
const deviceStorage = useStorage(device, [ACTIVE_UPDATE_ID])
|
||||
const debugOverride =
|
||||
!!useStorage(device, ['policyUpdateDebugOverride'])[0] && IS_DEV
|
||||
|
||||
return useMemo(() => {
|
||||
/**
|
||||
* If not enabled, then just return a completed state so the app functions
|
||||
* as normal.
|
||||
*/
|
||||
if (!enabled) {
|
||||
return {
|
||||
completed: true,
|
||||
complete() {},
|
||||
}
|
||||
}
|
||||
|
||||
const nuxIsReady = nux.status === 'ready'
|
||||
const nuxIsCompleted = nux.nux?.completed === true
|
||||
const nuxIsOptimisticallyCompleted = !!variables?.completed
|
||||
const [completedForDevice, setCompletedForDevice] = deviceStorage
|
||||
|
||||
const completed = computeCompletedState({
|
||||
nuxIsReady,
|
||||
nuxIsCompleted,
|
||||
nuxIsOptimisticallyCompleted,
|
||||
completedForDevice,
|
||||
})
|
||||
|
||||
logger.debug(`state`, {
|
||||
completed,
|
||||
nux,
|
||||
completedForDevice,
|
||||
})
|
||||
|
||||
if (!debugOverride) {
|
||||
syncCompletedState({
|
||||
nuxIsReady,
|
||||
nuxIsCompleted,
|
||||
nuxIsOptimisticallyCompleted,
|
||||
completedForDevice,
|
||||
save,
|
||||
setCompletedForDevice,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
completed,
|
||||
complete() {
|
||||
logger.debug(`user completed`)
|
||||
save({
|
||||
id: ACTIVE_UPDATE_ID,
|
||||
completed: true,
|
||||
data: undefined,
|
||||
})
|
||||
setCompletedForDevice(true)
|
||||
},
|
||||
}
|
||||
}, [enabled, nux, save, variables, deviceStorage, debugOverride])
|
||||
}
|
||||
|
||||
export function computeCompletedState({
|
||||
nuxIsReady,
|
||||
nuxIsCompleted,
|
||||
nuxIsOptimisticallyCompleted,
|
||||
completedForDevice,
|
||||
}: {
|
||||
nuxIsReady: boolean
|
||||
nuxIsCompleted: boolean
|
||||
nuxIsOptimisticallyCompleted: boolean
|
||||
completedForDevice: boolean | undefined
|
||||
}): boolean {
|
||||
/**
|
||||
* Assume completed to prevent flash
|
||||
*/
|
||||
let completed = true
|
||||
|
||||
/**
|
||||
* Prefer server state, if available
|
||||
*/
|
||||
if (nuxIsReady) {
|
||||
completed = nuxIsCompleted
|
||||
}
|
||||
|
||||
/**
|
||||
* Override with optimistic state or device state
|
||||
*/
|
||||
if (nuxIsOptimisticallyCompleted || !!completedForDevice) {
|
||||
completed = true
|
||||
}
|
||||
|
||||
return completed
|
||||
}
|
||||
|
||||
export function syncCompletedState({
|
||||
nuxIsReady,
|
||||
nuxIsCompleted,
|
||||
nuxIsOptimisticallyCompleted,
|
||||
completedForDevice,
|
||||
save,
|
||||
setCompletedForDevice,
|
||||
}: {
|
||||
nuxIsReady: boolean
|
||||
nuxIsCompleted: boolean
|
||||
nuxIsOptimisticallyCompleted: boolean
|
||||
completedForDevice: boolean | undefined
|
||||
save: ReturnType<typeof useSaveNux>['mutate']
|
||||
setCompletedForDevice: (value: boolean) => void
|
||||
}) {
|
||||
/*
|
||||
* Sync device state to server state for this account
|
||||
*/
|
||||
if (
|
||||
nuxIsReady &&
|
||||
!nuxIsCompleted &&
|
||||
!nuxIsOptimisticallyCompleted &&
|
||||
!!completedForDevice
|
||||
) {
|
||||
logger.debug(`syncing device state to server state`)
|
||||
save({
|
||||
id: ACTIVE_UPDATE_ID,
|
||||
completed: true,
|
||||
data: undefined,
|
||||
})
|
||||
} else if (nuxIsReady && nuxIsCompleted && !completedForDevice) {
|
||||
logger.debug(`syncing server state to device state`)
|
||||
/*
|
||||
* Sync server state to device state
|
||||
*/
|
||||
setCompletedForDevice(true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import {useCallback} from 'react'
|
||||
|
||||
import {ACTIVE_UPDATE_ID} from '#/components/PolicyUpdateOverlay/config'
|
||||
import {logger} from '#/components/PolicyUpdateOverlay/logger'
|
||||
import {device, useStorage} from '#/storage'
|
||||
|
||||
/*
|
||||
* Marks the active policy update as completed in device storage.
|
||||
* `usePolicyUpdateState` will react to this and replicate this status in the
|
||||
* server NUX state for this account.
|
||||
*/
|
||||
export function usePreemptivelyCompleteActivePolicyUpdate() {
|
||||
const [_completedForDevice, setCompletedForDevice] = useStorage(device, [
|
||||
ACTIVE_UPDATE_ID,
|
||||
])
|
||||
|
||||
return useCallback(() => {
|
||||
logger.debug(`preemptively completing active policy update`)
|
||||
setCompletedForDevice(true)
|
||||
}, [setCompletedForDevice])
|
||||
}
|
||||
@@ -28,6 +28,7 @@ export function createPortalGroup() {
|
||||
append: () => {},
|
||||
remove: () => {},
|
||||
})
|
||||
Context.displayName = 'PortalContext'
|
||||
|
||||
function Provider(props: React.PropsWithChildren<{}>) {
|
||||
const map = useRef<ComponentMap>({})
|
||||
|
||||
@@ -15,6 +15,7 @@ const Context = React.createContext<{
|
||||
setActiveView: (viewId: string) => void
|
||||
sendViewPosition: (viewId: string, y: number) => void
|
||||
} | null>(null)
|
||||
Context.displayName = 'ActiveVideoWebContext'
|
||||
|
||||
export function Provider({children}: {children: React.ReactNode}) {
|
||||
if (!isWeb) {
|
||||
|
||||
@@ -8,6 +8,7 @@ const Context = React.createContext<{
|
||||
volume: number
|
||||
setVolume: React.Dispatch<React.SetStateAction<number>>
|
||||
} | null>(null)
|
||||
Context.displayName = 'VideoVolumeContext'
|
||||
|
||||
export function Provider({children}: {children: React.ReactNode}) {
|
||||
const [muted, setMuted] = React.useState(true)
|
||||
|
||||
@@ -125,6 +125,7 @@ export function VideoEmbed({
|
||||
}
|
||||
|
||||
const NearScreenContext = createContext(false)
|
||||
NearScreenContext.displayName = 'VideoNearScreenContext'
|
||||
|
||||
/**
|
||||
* Renders a 100vh tall div and watches it with an IntersectionObserver to
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import {View} from 'react-native'
|
||||
import {Trans} from '@lingui/macro'
|
||||
|
||||
import {useSession} from '#/state/session'
|
||||
import {UserInfoText} from '#/view/com/util/UserInfoText'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {ArrowCornerDownRight_Stroke2_Corner2_Rounded as ArrowCornerDownRightIcon} from '#/components/icons/ArrowCornerDownRight'
|
||||
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
|
||||
import {Text} from '#/components/Typography'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
|
||||
export function PostRepliedTo({
|
||||
parentAuthor,
|
||||
isParentBlocked,
|
||||
isParentNotFound,
|
||||
}: {
|
||||
parentAuthor: string | bsky.profile.AnyProfileView | undefined
|
||||
isParentBlocked?: boolean
|
||||
isParentNotFound?: boolean
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {currentAccount} = useSession()
|
||||
|
||||
const textStyle = [a.text_sm, t.atoms.text_contrast_medium, a.leading_snug]
|
||||
|
||||
let label
|
||||
if (isParentBlocked) {
|
||||
label = <Trans context="description">Replied to a blocked post</Trans>
|
||||
} else if (isParentNotFound) {
|
||||
label = <Trans context="description">Replied to a post</Trans>
|
||||
} else if (parentAuthor) {
|
||||
const did =
|
||||
typeof parentAuthor === 'string' ? parentAuthor : parentAuthor.did
|
||||
const isMe = currentAccount?.did === did
|
||||
if (isMe) {
|
||||
label = <Trans context="description">Replied to you</Trans>
|
||||
} else {
|
||||
label = (
|
||||
<Trans context="description">
|
||||
Replied to{' '}
|
||||
<ProfileHoverCard did={did}>
|
||||
<UserInfoText did={did} attr="displayName" style={textStyle} />
|
||||
</ProfileHoverCard>
|
||||
</Trans>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (!label) {
|
||||
// Should not happen.
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[a.flex_row, a.align_center, a.pb_xs, a.gap_xs]}>
|
||||
<ArrowCornerDownRightIcon
|
||||
size="xs"
|
||||
style={[t.atoms.text_contrast_medium, {top: -1}]}
|
||||
/>
|
||||
<Text style={textStyle}>{label}</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -32,8 +32,7 @@ export function DiscoverDebug({
|
||||
hitSlop={10}
|
||||
style={[
|
||||
a.absolute,
|
||||
a.bottom_0,
|
||||
{zIndex: 1000},
|
||||
{zIndex: 1000, maxWidth: 65, bottom: -4},
|
||||
gtMobile ? a.right_0 : a.left_0,
|
||||
]}
|
||||
onPress={e => {
|
||||
@@ -42,6 +41,7 @@ export function DiscoverDebug({
|
||||
Toast.show(t`Copied to clipboard`, 'clipboard-check')
|
||||
}}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={{
|
||||
color: theme.palette.contrast_400,
|
||||
fontSize: 7,
|
||||
|
||||
@@ -13,6 +13,7 @@ const PostControlContext = createContext<{
|
||||
active?: boolean
|
||||
color?: {color: string}
|
||||
}>({})
|
||||
PostControlContext.displayName = 'PostControlContext'
|
||||
|
||||
// Base button style, which the the other ones extend
|
||||
export function PostControlButton({
|
||||
|
||||
@@ -19,6 +19,7 @@ import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {DISCOVER_DEBUG_DIDS} from '#/lib/constants'
|
||||
import {useOpenLink} from '#/lib/hooks/useOpenLink'
|
||||
import {useTranslate} from '#/lib/hooks/useTranslate'
|
||||
import {getCurrentRoute} from '#/lib/routes/helpers'
|
||||
import {makeProfileLink} from '#/lib/routes/links'
|
||||
import {
|
||||
@@ -28,7 +29,6 @@ import {
|
||||
import {logEvent, useGate} from '#/lib/statsig/statsig'
|
||||
import {richTextToString} from '#/lib/strings/rich-text-helpers'
|
||||
import {toShareUrl} from '#/lib/strings/url-helpers'
|
||||
import {getTranslatorLink} from '#/locale/helpers'
|
||||
import {logger} from '#/logger'
|
||||
import {type Shadow} from '#/state/cache/post-shadow'
|
||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
@@ -118,6 +118,7 @@ let PostMenuItems = ({
|
||||
const {hidePost} = useHiddenPostsApi()
|
||||
const feedFeedback = useFeedFeedbackContext()
|
||||
const openLink = useOpenLink()
|
||||
const translate = useTranslate()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {mutedWordsDialogControl} = useGlobalDialogsControlContext()
|
||||
const blockPromptControl = useDialogControl()
|
||||
@@ -172,11 +173,6 @@ let PostMenuItems = ({
|
||||
return makeProfileLink(postAuthor, 'post', urip.rkey)
|
||||
}, [postUri, postAuthor])
|
||||
|
||||
const translatorUrl = getTranslatorLink(
|
||||
record.text,
|
||||
langPrefs.primaryLanguage,
|
||||
)
|
||||
|
||||
const onDeletePost = () => {
|
||||
deletePostMutate({uri: postUri}).then(
|
||||
() => {
|
||||
@@ -234,8 +230,8 @@ let PostMenuItems = ({
|
||||
Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
|
||||
}
|
||||
|
||||
const onPressTranslate = async () => {
|
||||
await openLink(translatorUrl, true)
|
||||
const onPressTranslate = () => {
|
||||
translate(record.text, langPrefs.primaryLanguage)
|
||||
|
||||
if (
|
||||
bsky.dangerousIsType<AppBskyFeedPost.Record>(
|
||||
|
||||
@@ -145,7 +145,7 @@ let RepostButtonDialogInner = ({
|
||||
<View style={a.gap_xl}>
|
||||
<View style={a.gap_xs}>
|
||||
<Button
|
||||
style={[a.justify_start, a.px_md]}
|
||||
style={[a.justify_start, a.px_md, a.gap_sm]}
|
||||
label={
|
||||
isReposted
|
||||
? _(msg`Remove repost`)
|
||||
@@ -167,7 +167,7 @@ let RepostButtonDialogInner = ({
|
||||
<Button
|
||||
disabled={embeddingDisabled}
|
||||
testID="quoteBtn"
|
||||
style={[a.justify_start, a.px_md]}
|
||||
style={[a.justify_start, a.px_md, a.gap_sm]}
|
||||
label={
|
||||
embeddingDisabled
|
||||
? _(msg`Quote posts disabled`)
|
||||
|
||||
@@ -4,10 +4,12 @@ import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {isBlockedOrBlocking, isMuted} from '#/lib/moderation/blocked-and-muted'
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {logger} from '#/logger'
|
||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useListConvosQuery} from '#/state/queries/messages/list-conversations'
|
||||
import {useSession} from '#/state/session'
|
||||
@@ -57,7 +59,11 @@ export function RecentChats({postUri}: {postUri: string}) {
|
||||
member => member.did !== currentAccount?.did,
|
||||
)
|
||||
|
||||
if (!otherMember || otherMember.handle === 'missing.invalid')
|
||||
if (
|
||||
!otherMember ||
|
||||
otherMember.handle === 'missing.invalid' ||
|
||||
convo.muted
|
||||
)
|
||||
return null
|
||||
|
||||
return (
|
||||
@@ -87,7 +93,7 @@ export function RecentChats({postUri}: {postUri: string}) {
|
||||
const WIDTH = 80
|
||||
|
||||
function RecentChatItem({
|
||||
profile,
|
||||
profile: profileUnshadowed,
|
||||
onPress,
|
||||
moderationOpts,
|
||||
}: {
|
||||
@@ -98,6 +104,8 @@ function RecentChatItem({
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
|
||||
const profile = useProfileShadow(profileUnshadowed)
|
||||
|
||||
const moderation = moderateProfile(profile, moderationOpts)
|
||||
const name = sanitizeDisplayName(
|
||||
profile.displayName || sanitizeHandle(profile.handle),
|
||||
@@ -105,6 +113,10 @@ function RecentChatItem({
|
||||
)
|
||||
const verification = useSimpleVerificationState({profile})
|
||||
|
||||
if (isBlockedOrBlocking(profile) || isMuted(profile)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
onPress={onPress}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {ProfileHoverCardProps} from './types'
|
||||
import {type ProfileHoverCardProps} from './types'
|
||||
|
||||
export function ProfileHoverCard({children}: ProfileHoverCardProps) {
|
||||
return children
|
||||
|
||||
@@ -74,7 +74,9 @@ export function ProfileHoverCard(props: ProfileHoverCardProps) {
|
||||
return props.children
|
||||
} else {
|
||||
return (
|
||||
<View onPointerMove={onPointerMove} style={[a.flex_shrink, props.style]}>
|
||||
<View
|
||||
onPointerMove={onPointerMove}
|
||||
style={[a.flex_shrink, props.inline && a.inline, props.style]}>
|
||||
<ProfileHoverCardInner {...props} />
|
||||
</View>
|
||||
)
|
||||
@@ -326,7 +328,7 @@ export function ProfileHoverCardInner(props: ProfileHoverCardProps) {
|
||||
onPointerLeave={onPointerLeaveTarget}
|
||||
// @ts-ignore web only prop
|
||||
onMouseUp={onPress}
|
||||
style={{flexShrink: 1}}>
|
||||
style={[a.flex_shrink, props.inline && a.inline]}>
|
||||
{props.children}
|
||||
{isVisible && (
|
||||
<Portal>
|
||||
|
||||
@@ -6,4 +6,5 @@ export type ProfileHoverCardProps = ViewStyleProp & {
|
||||
children: React.ReactNode
|
||||
did: string
|
||||
disable?: boolean
|
||||
inline?: boolean
|
||||
}
|
||||
|
||||
@@ -539,7 +539,7 @@ let Tab = ({
|
||||
]}>
|
||||
<Text
|
||||
style={[
|
||||
/* TODO: medium weight */
|
||||
a.font_medium,
|
||||
active || hovered || pressed || focused
|
||||
? t.atoms.text
|
||||
: t.atoms.text_contrast_medium,
|
||||
|
||||
@@ -27,6 +27,7 @@ const Context = React.createContext<{
|
||||
titleId: '',
|
||||
descriptionId: '',
|
||||
})
|
||||
Context.displayName = 'PromptContext'
|
||||
|
||||
export function Outer({
|
||||
children,
|
||||
|
||||
@@ -34,10 +34,12 @@ type ContextType = {
|
||||
} & Pick<RootProps, 'value' | 'onValueChange' | 'disabled'>
|
||||
|
||||
const Context = createContext<ContextType | null>(null)
|
||||
Context.displayName = 'SelectContext'
|
||||
|
||||
const ValueTextContext = createContext<
|
||||
[any, React.Dispatch<React.SetStateAction<any>>]
|
||||
>([undefined, () => {}])
|
||||
ValueTextContext.displayName = 'ValueTextContext'
|
||||
|
||||
function useSelectContext() {
|
||||
const ctx = useContext(Context)
|
||||
@@ -229,6 +231,7 @@ const ItemContext = createContext<{
|
||||
focused: false,
|
||||
pressed: false,
|
||||
})
|
||||
ItemContext.displayName = 'SelectItemContext'
|
||||
|
||||
export function useItemContext() {
|
||||
return useContext(ItemContext)
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from './types'
|
||||
|
||||
const SelectedValueContext = createContext<string | undefined | null>(null)
|
||||
SelectedValueContext.displayName = 'SelectSelectedValueContext'
|
||||
|
||||
export function Root(props: RootProps) {
|
||||
return (
|
||||
@@ -98,13 +99,14 @@ export function Trigger({children, label}: TriggerProps) {
|
||||
t.atoms.bg_contrast_25,
|
||||
a.rounded_sm,
|
||||
a.w_full,
|
||||
{maxWidth: 400},
|
||||
a.align_center,
|
||||
a.gap_sm,
|
||||
a.justify_between,
|
||||
a.py_sm,
|
||||
a.px_md,
|
||||
a.pointer,
|
||||
{
|
||||
maxWidth: 400,
|
||||
outline: 0,
|
||||
borderWidth: 2,
|
||||
borderStyle: 'solid',
|
||||
@@ -190,6 +192,7 @@ export function Content<T>({items, renderItem}: ContentProps<T>) {
|
||||
a.border,
|
||||
t.atoms.border_contrast_low,
|
||||
a.rounded_sm,
|
||||
a.overflow_hidden,
|
||||
]}>
|
||||
<RadixSelect.ScrollUpButton style={flatten(up)}>
|
||||
<ChevronUpIcon style={[t.atoms.text]} size="xs" />
|
||||
@@ -217,6 +220,7 @@ const ItemContext = createContext<{
|
||||
pressed: false,
|
||||
selected: false,
|
||||
})
|
||||
ItemContext.displayName = 'SelectItemContext'
|
||||
|
||||
export function useItemContext() {
|
||||
return useContext(ItemContext)
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import React, {useCallback} from 'react'
|
||||
import {ListRenderItemInfo, View} from 'react-native'
|
||||
import {AppBskyFeedDefs} from '@atproto/api'
|
||||
import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
|
||||
import {type ListRenderItemInfo, View} from 'react-native'
|
||||
import {type AppBskyFeedDefs} from '@atproto/api'
|
||||
|
||||
import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset'
|
||||
import {isNative, isWeb} from '#/platform/detection'
|
||||
import {List, ListRef} from '#/view/com/util/List'
|
||||
import {SectionRef} from '#/screens/Profile/Sections/types'
|
||||
import {List, type ListRef} from '#/view/com/util/List'
|
||||
import {type SectionRef} from '#/screens/Profile/Sections/types'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import * as FeedCard from '#/components/FeedCard'
|
||||
|
||||
@@ -37,7 +36,10 @@ export const FeedsList = React.forwardRef<SectionRef, ProfilesListProps>(
|
||||
scrollToTop: onScrollToTop,
|
||||
}))
|
||||
|
||||
const renderItem = ({item, index}: ListRenderItemInfo<GeneratorView>) => {
|
||||
const renderItem = ({
|
||||
item,
|
||||
index,
|
||||
}: ListRenderItemInfo<AppBskyFeedDefs.GeneratorView>) => {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import {useRef} from 'react'
|
||||
import type {ListRenderItemInfo} from 'react-native'
|
||||
import {type ListRenderItemInfo} from 'react-native'
|
||||
import {View} from 'react-native'
|
||||
import {AppBskyActorDefs, ModerationOpts} from '@atproto/api'
|
||||
import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
|
||||
import {
|
||||
type AppBskyActorDefs,
|
||||
type AppBskyFeedDefs,
|
||||
type ModerationOpts,
|
||||
} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useSession} from '#/state/session'
|
||||
import {ListMethods} from '#/view/com/util/List'
|
||||
import {WizardAction, WizardState} from '#/screens/StarterPack/Wizard/State'
|
||||
import {type ListMethods} from '#/view/com/util/List'
|
||||
import {
|
||||
type WizardAction,
|
||||
type WizardState,
|
||||
} from '#/screens/StarterPack/Wizard/State'
|
||||
import {atoms as a, native, useTheme, web} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
@@ -21,7 +27,7 @@ import {
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
function keyExtractor(
|
||||
item: AppBskyActorDefs.ProfileViewBasic | GeneratorView,
|
||||
item: AppBskyActorDefs.ProfileViewBasic | AppBskyFeedDefs.GeneratorView,
|
||||
index: number,
|
||||
) {
|
||||
return `${item.did}-${index}`
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import {Keyboard, View} from 'react-native'
|
||||
import {
|
||||
AppBskyActorDefs,
|
||||
AppBskyFeedDefs,
|
||||
type AppBskyActorDefs,
|
||||
type AppBskyFeedDefs,
|
||||
moderateFeedGenerator,
|
||||
moderateProfile,
|
||||
ModerationOpts,
|
||||
ModerationUI,
|
||||
type ModerationOpts,
|
||||
type ModerationUI,
|
||||
} from '@atproto/api'
|
||||
import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
@@ -16,13 +15,16 @@ import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {useSession} from '#/state/session'
|
||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {WizardAction, WizardState} from '#/screens/StarterPack/Wizard/State'
|
||||
import {
|
||||
type WizardAction,
|
||||
type WizardState,
|
||||
} from '#/screens/StarterPack/Wizard/State'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Toggle from '#/components/forms/Toggle'
|
||||
import {Checkbox} from '#/components/forms/Toggle'
|
||||
import {Text} from '#/components/Typography'
|
||||
import * as bsky from '#/types/bsky'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
|
||||
function WizardListCard({
|
||||
type,
|
||||
@@ -174,7 +176,7 @@ export function WizardFeedCard({
|
||||
moderationOpts,
|
||||
}: {
|
||||
btnType: 'checkbox' | 'remove'
|
||||
generator: GeneratorView
|
||||
generator: AppBskyFeedDefs.GeneratorView
|
||||
state: WizardState
|
||||
dispatch: (action: WizardAction) => void
|
||||
moderationOpts: ModerationOpts
|
||||
|
||||
+63
-101
@@ -1,21 +1,26 @@
|
||||
import {createContext, useContext, useMemo} from 'react'
|
||||
import {View} from 'react-native'
|
||||
|
||||
import {atoms as a, select, useTheme} from '#/alf'
|
||||
import {Check_Stroke2_Corner0_Rounded as SuccessIcon} from '#/components/icons/Check'
|
||||
import {atoms as a, select, useAlf, useTheme} from '#/alf'
|
||||
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
||||
import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo'
|
||||
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
|
||||
import {type ToastType} from '#/components/Toast/types'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {CircleCheck_Stroke2_Corner0_Rounded as CircleCheck} from '../icons/CircleCheck'
|
||||
|
||||
type ContextType = {
|
||||
type: ToastType
|
||||
}
|
||||
|
||||
export type ToastComponentProps = {
|
||||
type?: ToastType
|
||||
content: React.ReactNode
|
||||
}
|
||||
|
||||
export const ICONS = {
|
||||
default: SuccessIcon,
|
||||
success: SuccessIcon,
|
||||
default: CircleCheck,
|
||||
success: CircleCheck,
|
||||
error: ErrorIcon,
|
||||
warning: WarningIcon,
|
||||
info: CircleInfo,
|
||||
@@ -24,17 +29,22 @@ export const ICONS = {
|
||||
const Context = createContext<ContextType>({
|
||||
type: 'default',
|
||||
})
|
||||
Context.displayName = 'ToastContext'
|
||||
|
||||
export function Toast({
|
||||
type,
|
||||
content,
|
||||
}: {
|
||||
type: ToastType
|
||||
content: React.ReactNode
|
||||
}) {
|
||||
export function Toast({type = 'default', content}: ToastComponentProps) {
|
||||
const {fonts} = useAlf()
|
||||
const t = useTheme()
|
||||
const styles = useToastStyles({type})
|
||||
const Icon = ICONS[type]
|
||||
/**
|
||||
* Vibes-based number, adjusts `top` of `View` that wraps the text to
|
||||
* compensate for different type sizes and keep the first line of text
|
||||
* aligned with the icon. - esb
|
||||
*/
|
||||
const fontScaleCompensation = useMemo(
|
||||
() => parseInt(fonts.scale) * -1 * 0.65,
|
||||
[fonts.scale],
|
||||
)
|
||||
|
||||
return (
|
||||
<Context.Provider value={useMemo(() => ({type}), [type])}>
|
||||
@@ -56,7 +66,13 @@ export function Toast({
|
||||
]}>
|
||||
<Icon size="md" fill={styles.iconColor} />
|
||||
|
||||
<View style={[a.flex_1]}>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{
|
||||
top: fontScaleCompensation,
|
||||
},
|
||||
]}>
|
||||
{typeof content === 'string' ? (
|
||||
<ToastText>{content}</ToastText>
|
||||
) : (
|
||||
@@ -73,10 +89,12 @@ export function ToastText({children}: {children: React.ReactNode}) {
|
||||
const {textColor} = useToastStyles({type})
|
||||
return (
|
||||
<Text
|
||||
selectable={false}
|
||||
style={[
|
||||
a.text_md,
|
||||
a.font_bold,
|
||||
a.font_medium,
|
||||
a.leading_snug,
|
||||
a.pointer_events_none,
|
||||
{
|
||||
color: textColor,
|
||||
},
|
||||
@@ -91,114 +109,58 @@ function useToastStyles({type}: {type: ToastType}) {
|
||||
return useMemo(() => {
|
||||
return {
|
||||
default: {
|
||||
backgroundColor: select(t.name, {
|
||||
light: t.atoms.bg_contrast_25.backgroundColor,
|
||||
dim: t.atoms.bg_contrast_100.backgroundColor,
|
||||
dark: t.atoms.bg_contrast_100.backgroundColor,
|
||||
}),
|
||||
borderColor: select(t.name, {
|
||||
light: t.atoms.border_contrast_low.borderColor,
|
||||
dim: t.atoms.border_contrast_high.borderColor,
|
||||
dark: t.atoms.border_contrast_high.borderColor,
|
||||
}),
|
||||
iconColor: select(t.name, {
|
||||
light: t.atoms.text_contrast_medium.color,
|
||||
dim: t.atoms.text_contrast_medium.color,
|
||||
dark: t.atoms.text_contrast_medium.color,
|
||||
}),
|
||||
textColor: select(t.name, {
|
||||
light: t.atoms.text_contrast_medium.color,
|
||||
dim: t.atoms.text_contrast_medium.color,
|
||||
dark: t.atoms.text_contrast_medium.color,
|
||||
}),
|
||||
backgroundColor: t.atoms.bg_contrast_25.backgroundColor,
|
||||
borderColor: t.atoms.border_contrast_low.borderColor,
|
||||
iconColor: t.atoms.text.color,
|
||||
textColor: t.atoms.text.color,
|
||||
},
|
||||
success: {
|
||||
backgroundColor: select(t.name, {
|
||||
light: t.palette.primary_100,
|
||||
dim: t.palette.primary_100,
|
||||
dark: t.palette.primary_50,
|
||||
}),
|
||||
backgroundColor: t.palette.primary_25,
|
||||
borderColor: select(t.name, {
|
||||
light: t.palette.primary_500,
|
||||
dim: t.palette.primary_500,
|
||||
dark: t.palette.primary_500,
|
||||
light: t.palette.primary_300,
|
||||
dim: t.palette.primary_200,
|
||||
dark: t.palette.primary_100,
|
||||
}),
|
||||
iconColor: select(t.name, {
|
||||
light: t.palette.primary_500,
|
||||
dim: t.palette.primary_600,
|
||||
dark: t.palette.primary_600,
|
||||
light: t.palette.primary_600,
|
||||
dim: t.palette.primary_700,
|
||||
dark: t.palette.primary_700,
|
||||
}),
|
||||
textColor: select(t.name, {
|
||||
light: t.palette.primary_500,
|
||||
dim: t.palette.primary_600,
|
||||
dark: t.palette.primary_600,
|
||||
light: t.palette.primary_600,
|
||||
dim: t.palette.primary_700,
|
||||
dark: t.palette.primary_700,
|
||||
}),
|
||||
},
|
||||
error: {
|
||||
backgroundColor: select(t.name, {
|
||||
light: t.palette.negative_200,
|
||||
dim: t.palette.negative_25,
|
||||
dark: t.palette.negative_25,
|
||||
}),
|
||||
backgroundColor: t.palette.negative_25,
|
||||
borderColor: select(t.name, {
|
||||
light: t.palette.negative_300,
|
||||
dim: t.palette.negative_300,
|
||||
dark: t.palette.negative_300,
|
||||
light: t.palette.negative_200,
|
||||
dim: t.palette.negative_200,
|
||||
dark: t.palette.negative_100,
|
||||
}),
|
||||
iconColor: select(t.name, {
|
||||
light: t.palette.negative_600,
|
||||
dim: t.palette.negative_600,
|
||||
dark: t.palette.negative_600,
|
||||
light: t.palette.negative_700,
|
||||
dim: t.palette.negative_900,
|
||||
dark: t.palette.negative_900,
|
||||
}),
|
||||
textColor: select(t.name, {
|
||||
light: t.palette.negative_600,
|
||||
dim: t.palette.negative_600,
|
||||
dark: t.palette.negative_600,
|
||||
light: t.palette.negative_700,
|
||||
dim: t.palette.negative_900,
|
||||
dark: t.palette.negative_900,
|
||||
}),
|
||||
},
|
||||
warning: {
|
||||
backgroundColor: select(t.name, {
|
||||
light: t.atoms.bg_contrast_25.backgroundColor,
|
||||
dim: t.atoms.bg_contrast_100.backgroundColor,
|
||||
dark: t.atoms.bg_contrast_100.backgroundColor,
|
||||
}),
|
||||
borderColor: select(t.name, {
|
||||
light: t.atoms.border_contrast_low.borderColor,
|
||||
dim: t.atoms.border_contrast_high.borderColor,
|
||||
dark: t.atoms.border_contrast_high.borderColor,
|
||||
}),
|
||||
iconColor: select(t.name, {
|
||||
light: t.atoms.text_contrast_medium.color,
|
||||
dim: t.atoms.text_contrast_medium.color,
|
||||
dark: t.atoms.text_contrast_medium.color,
|
||||
}),
|
||||
textColor: select(t.name, {
|
||||
light: t.atoms.text_contrast_medium.color,
|
||||
dim: t.atoms.text_contrast_medium.color,
|
||||
dark: t.atoms.text_contrast_medium.color,
|
||||
}),
|
||||
backgroundColor: t.atoms.bg_contrast_25.backgroundColor,
|
||||
borderColor: t.atoms.border_contrast_low.borderColor,
|
||||
iconColor: t.atoms.text.color,
|
||||
textColor: t.atoms.text.color,
|
||||
},
|
||||
info: {
|
||||
backgroundColor: select(t.name, {
|
||||
light: t.atoms.bg_contrast_25.backgroundColor,
|
||||
dim: t.atoms.bg_contrast_100.backgroundColor,
|
||||
dark: t.atoms.bg_contrast_100.backgroundColor,
|
||||
}),
|
||||
borderColor: select(t.name, {
|
||||
light: t.atoms.border_contrast_low.borderColor,
|
||||
dim: t.atoms.border_contrast_high.borderColor,
|
||||
dark: t.atoms.border_contrast_high.borderColor,
|
||||
}),
|
||||
iconColor: select(t.name, {
|
||||
light: t.atoms.text_contrast_medium.color,
|
||||
dim: t.atoms.text_contrast_medium.color,
|
||||
dark: t.atoms.text_contrast_medium.color,
|
||||
}),
|
||||
textColor: select(t.name, {
|
||||
light: t.atoms.text_contrast_medium.color,
|
||||
dim: t.atoms.text_contrast_medium.color,
|
||||
dark: t.atoms.text_contrast_medium.color,
|
||||
}),
|
||||
backgroundColor: t.atoms.bg_contrast_25.backgroundColor,
|
||||
borderColor: t.atoms.border_contrast_low.borderColor,
|
||||
iconColor: t.atoms.text.color,
|
||||
textColor: t.atoms.text.color,
|
||||
},
|
||||
}[type]
|
||||
}, [t, type])
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const DEFAULT_TOAST_DURATION = 3000
|
||||
export const DURATION = 3e3
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
export function ToastContainer() {
|
||||
export function ToastOutlet() {
|
||||
return null
|
||||
}
|
||||
|
||||
export const api = () => {}
|
||||
api.success = () => {}
|
||||
api.wiggle = () => {}
|
||||
api.error = () => {}
|
||||
api.warning = () => {}
|
||||
api.info = () => {}
|
||||
api.promise = () => {}
|
||||
api.custom = () => {}
|
||||
api.loading = () => {}
|
||||
api.dismiss = () => {}
|
||||
|
||||
export function show() {}
|
||||
|
||||
+40
-188
@@ -1,197 +1,49 @@
|
||||
import {useEffect, useMemo, useRef, useState} from 'react'
|
||||
import {AccessibilityInfo} from 'react-native'
|
||||
import {
|
||||
Gesture,
|
||||
GestureDetector,
|
||||
GestureHandlerRootView,
|
||||
} from 'react-native-gesture-handler'
|
||||
import Animated, {
|
||||
Easing,
|
||||
runOnJS,
|
||||
SlideInUp,
|
||||
SlideOutUp,
|
||||
useAnimatedReaction,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withDecay,
|
||||
withSpring,
|
||||
} from 'react-native-reanimated'
|
||||
import RootSiblings from 'react-native-root-siblings'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {View} from 'react-native'
|
||||
import {toast as sonner, Toaster} from 'sonner-native'
|
||||
|
||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {DEFAULT_TOAST_DURATION} from '#/components/Toast/const'
|
||||
import {Toast} from '#/components/Toast/Toast'
|
||||
import {type ToastApi, type ToastType} from '#/components/Toast/types'
|
||||
import {DURATION} from '#/components/Toast/const'
|
||||
import {
|
||||
Toast as BaseToast,
|
||||
type ToastComponentProps,
|
||||
} from '#/components/Toast/Toast'
|
||||
import {type BaseToastOptions} from '#/components/Toast/types'
|
||||
|
||||
const TOAST_ANIMATION_DURATION = 300
|
||||
export {DURATION} from '#/components/Toast/const'
|
||||
|
||||
export function ToastContainer() {
|
||||
return null
|
||||
/**
|
||||
* Toasts are rendered in a global outlet, which is placed at the top of the
|
||||
* component tree.
|
||||
*/
|
||||
export function ToastOutlet() {
|
||||
return <Toaster pauseWhenPageIsHidden gap={a.gap_sm.gap} />
|
||||
}
|
||||
|
||||
export const toast: ToastApi = {
|
||||
show(props) {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
return
|
||||
}
|
||||
|
||||
AccessibilityInfo.announceForAccessibility(props.a11yLabel)
|
||||
|
||||
const item = new RootSiblings(
|
||||
(
|
||||
<AnimatedToast
|
||||
type={props.type}
|
||||
content={props.content}
|
||||
a11yLabel={props.a11yLabel}
|
||||
duration={props.duration ?? DEFAULT_TOAST_DURATION}
|
||||
destroy={() => item.destroy()}
|
||||
/>
|
||||
),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
function AnimatedToast({
|
||||
type,
|
||||
content,
|
||||
a11yLabel,
|
||||
duration,
|
||||
destroy,
|
||||
}: {
|
||||
type: ToastType
|
||||
content: React.ReactNode
|
||||
a11yLabel: string
|
||||
duration: number
|
||||
destroy: () => void
|
||||
}) {
|
||||
const {top} = useSafeAreaInsets()
|
||||
const isPanning = useSharedValue(false)
|
||||
const dismissSwipeTranslateY = useSharedValue(0)
|
||||
const [cardHeight, setCardHeight] = useState(0)
|
||||
|
||||
// for the exit animation to work on iOS the animated component
|
||||
// must not be the root component
|
||||
// so we need to wrap it in a view and unmount the toast ahead of time
|
||||
const [alive, setAlive] = useState(true)
|
||||
|
||||
const hideAndDestroyImmediately = () => {
|
||||
setAlive(false)
|
||||
setTimeout(() => {
|
||||
destroy()
|
||||
}, 1e3)
|
||||
}
|
||||
|
||||
const destroyTimeoutRef = useRef<ReturnType<typeof setTimeout>>()
|
||||
const hideAndDestroyAfterTimeout = useNonReactiveCallback(() => {
|
||||
clearTimeout(destroyTimeoutRef.current)
|
||||
destroyTimeoutRef.current = setTimeout(hideAndDestroyImmediately, duration)
|
||||
})
|
||||
const pauseDestroy = useNonReactiveCallback(() => {
|
||||
clearTimeout(destroyTimeoutRef.current)
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
hideAndDestroyAfterTimeout()
|
||||
}, [hideAndDestroyAfterTimeout])
|
||||
|
||||
const panGesture = useMemo(() => {
|
||||
return Gesture.Pan()
|
||||
.activeOffsetY([-10, 10])
|
||||
.failOffsetX([-10, 10])
|
||||
.maxPointers(1)
|
||||
.onStart(() => {
|
||||
'worklet'
|
||||
if (!alive) return
|
||||
isPanning.set(true)
|
||||
runOnJS(pauseDestroy)()
|
||||
})
|
||||
.onUpdate(e => {
|
||||
'worklet'
|
||||
if (!alive) return
|
||||
dismissSwipeTranslateY.value = e.translationY
|
||||
})
|
||||
.onEnd(e => {
|
||||
'worklet'
|
||||
if (!alive) return
|
||||
runOnJS(hideAndDestroyAfterTimeout)()
|
||||
isPanning.set(false)
|
||||
if (e.velocityY < -100) {
|
||||
if (dismissSwipeTranslateY.value === 0) {
|
||||
// HACK: If the initial value is 0, withDecay() animation doesn't start.
|
||||
// This is a bug in Reanimated, but for now we'll work around it like this.
|
||||
dismissSwipeTranslateY.value = 1
|
||||
}
|
||||
dismissSwipeTranslateY.value = withDecay({
|
||||
velocity: e.velocityY,
|
||||
velocityFactor: Math.max(3500 / Math.abs(e.velocityY), 1),
|
||||
deceleration: 1,
|
||||
})
|
||||
} else {
|
||||
dismissSwipeTranslateY.value = withSpring(0, {
|
||||
stiffness: 500,
|
||||
damping: 50,
|
||||
})
|
||||
}
|
||||
})
|
||||
}, [
|
||||
dismissSwipeTranslateY,
|
||||
isPanning,
|
||||
alive,
|
||||
hideAndDestroyAfterTimeout,
|
||||
pauseDestroy,
|
||||
])
|
||||
|
||||
const topOffset = top + 10
|
||||
|
||||
useAnimatedReaction(
|
||||
() =>
|
||||
!isPanning.get() &&
|
||||
dismissSwipeTranslateY.get() < -topOffset - cardHeight,
|
||||
(isSwipedAway, prevIsSwipedAway) => {
|
||||
'worklet'
|
||||
if (isSwipedAway && !prevIsSwipedAway) {
|
||||
runOnJS(destroy)()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => {
|
||||
const translation = dismissSwipeTranslateY.get()
|
||||
return {
|
||||
transform: [
|
||||
{
|
||||
translateY: translation > 0 ? translation ** 0.7 : translation,
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* The toast UI component
|
||||
*/
|
||||
export function Toast({type, content}: ToastComponentProps) {
|
||||
return (
|
||||
<GestureHandlerRootView
|
||||
style={[a.absolute, {top: topOffset, left: 16, right: 16}]}
|
||||
pointerEvents="box-none">
|
||||
{alive && (
|
||||
<Animated.View
|
||||
entering={SlideInUp.easing(Easing.out(Easing.exp)).duration(
|
||||
TOAST_ANIMATION_DURATION,
|
||||
)}
|
||||
exiting={SlideOutUp.easing(Easing.in(Easing.exp)).duration(
|
||||
TOAST_ANIMATION_DURATION * 0.7,
|
||||
)}
|
||||
onLayout={evt => setCardHeight(evt.nativeEvent.layout.height)}
|
||||
accessibilityRole="alert"
|
||||
accessible={true}
|
||||
accessibilityLabel={a11yLabel}
|
||||
accessibilityHint=""
|
||||
onAccessibilityEscape={hideAndDestroyImmediately}
|
||||
style={[a.flex_1, animatedStyle]}>
|
||||
<GestureDetector gesture={panGesture}>
|
||||
<Toast content={content} type={type} />
|
||||
</GestureDetector>
|
||||
</Animated.View>
|
||||
)}
|
||||
</GestureHandlerRootView>
|
||||
<View style={[a.px_xl, a.w_full]}>
|
||||
<BaseToast content={content} type={type} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Access the full Sonner API
|
||||
*/
|
||||
export const api = sonner
|
||||
|
||||
/**
|
||||
* Our base toast API, using the `Toast` export of this file.
|
||||
*/
|
||||
export function show(
|
||||
content: React.ReactNode,
|
||||
{type, ...options}: BaseToastOptions = {},
|
||||
) {
|
||||
sonner.custom(<Toast content={content} type={type} />, {
|
||||
...options,
|
||||
duration: options?.duration ?? DURATION,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,112 +1,40 @@
|
||||
/*
|
||||
* Note: relies on styles in #/styles.css
|
||||
*/
|
||||
import {toast as sonner, Toaster} from 'sonner'
|
||||
|
||||
import {useEffect, useState} from 'react'
|
||||
import {AccessibilityInfo, Pressable, View} from 'react-native'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {atoms as a, useBreakpoints} from '#/alf'
|
||||
import {DEFAULT_TOAST_DURATION} from '#/components/Toast/const'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {DURATION} from '#/components/Toast/const'
|
||||
import {Toast} from '#/components/Toast/Toast'
|
||||
import {type ToastApi, type ToastType} from '#/components/Toast/types'
|
||||
|
||||
const TOAST_ANIMATION_STYLES = {
|
||||
entering: {
|
||||
animation: 'toastFadeIn 0.3s ease-out forwards',
|
||||
},
|
||||
exiting: {
|
||||
animation: 'toastFadeOut 0.2s ease-in forwards',
|
||||
},
|
||||
}
|
||||
|
||||
interface ActiveToast {
|
||||
type: ToastType
|
||||
content: React.ReactNode
|
||||
a11yLabel: string
|
||||
}
|
||||
type GlobalSetActiveToast = (_activeToast: ActiveToast | undefined) => void
|
||||
let globalSetActiveToast: GlobalSetActiveToast | undefined
|
||||
let toastTimeout: NodeJS.Timeout | undefined
|
||||
type ToastContainerProps = {}
|
||||
|
||||
export const ToastContainer: React.FC<ToastContainerProps> = ({}) => {
|
||||
const {_} = useLingui()
|
||||
const {gtPhone} = useBreakpoints()
|
||||
const [activeToast, setActiveToast] = useState<ActiveToast | undefined>()
|
||||
const [isExiting, setIsExiting] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
globalSetActiveToast = (t: ActiveToast | undefined) => {
|
||||
if (!t && activeToast) {
|
||||
setIsExiting(true)
|
||||
setTimeout(() => {
|
||||
setActiveToast(t)
|
||||
setIsExiting(false)
|
||||
}, 200)
|
||||
} else {
|
||||
if (t) {
|
||||
AccessibilityInfo.announceForAccessibility(t.a11yLabel)
|
||||
}
|
||||
setActiveToast(t)
|
||||
setIsExiting(false)
|
||||
}
|
||||
}
|
||||
}, [activeToast])
|
||||
import {type BaseToastOptions} from '#/components/Toast/types'
|
||||
|
||||
/**
|
||||
* Toasts are rendered in a global outlet, which is placed at the top of the
|
||||
* component tree.
|
||||
*/
|
||||
export function ToastOutlet() {
|
||||
return (
|
||||
<>
|
||||
{activeToast && (
|
||||
<View
|
||||
style={[
|
||||
a.fixed,
|
||||
{
|
||||
left: a.px_xl.paddingLeft,
|
||||
right: a.px_xl.paddingLeft,
|
||||
bottom: a.px_xl.paddingLeft,
|
||||
...(isExiting
|
||||
? TOAST_ANIMATION_STYLES.exiting
|
||||
: TOAST_ANIMATION_STYLES.entering),
|
||||
},
|
||||
gtPhone && [
|
||||
{
|
||||
maxWidth: 380,
|
||||
},
|
||||
],
|
||||
]}>
|
||||
<Toast content={activeToast.content} type={activeToast.type} />
|
||||
<Pressable
|
||||
style={[a.absolute, a.inset_0]}
|
||||
accessibilityLabel={_(
|
||||
msg({
|
||||
message: `Dismiss message`,
|
||||
comment: `Accessibility label for dismissing a toast notification`,
|
||||
}),
|
||||
)}
|
||||
accessibilityHint=""
|
||||
onPress={() => setActiveToast(undefined)}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
<Toaster
|
||||
position="bottom-left"
|
||||
gap={a.gap_sm.gap}
|
||||
offset={a.p_xl.padding}
|
||||
mobileOffset={a.p_xl.padding}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const toast: ToastApi = {
|
||||
show(props) {
|
||||
if (toastTimeout) {
|
||||
clearTimeout(toastTimeout)
|
||||
}
|
||||
/**
|
||||
* Access the full Sonner API
|
||||
*/
|
||||
export const api = sonner
|
||||
|
||||
globalSetActiveToast?.({
|
||||
type: props.type,
|
||||
content: props.content,
|
||||
a11yLabel: props.a11yLabel,
|
||||
})
|
||||
|
||||
toastTimeout = setTimeout(() => {
|
||||
globalSetActiveToast?.(undefined)
|
||||
}, props.duration || DEFAULT_TOAST_DURATION)
|
||||
},
|
||||
/**
|
||||
* Our base toast API, using the `Toast` export of this file.
|
||||
*/
|
||||
export function show(
|
||||
content: React.ReactNode,
|
||||
{type, ...options}: BaseToastOptions = {},
|
||||
) {
|
||||
sonner(<Toast content={content} type={type} />, {
|
||||
unstyled: true, // required on web
|
||||
...options,
|
||||
duration: options?.duration ?? DURATION,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,24 +1,29 @@
|
||||
import {type toast as sonner} from 'sonner-native'
|
||||
|
||||
/**
|
||||
* This is not exported from `sonner-native` so just hacking it in here.
|
||||
*/
|
||||
export type ExternalToast = Exclude<
|
||||
Parameters<typeof sonner.custom>[1],
|
||||
undefined
|
||||
>
|
||||
|
||||
export type ToastType = 'default' | 'success' | 'error' | 'warning' | 'info'
|
||||
|
||||
export type ToastApi = {
|
||||
show: (props: {
|
||||
/**
|
||||
* The type of toast to show. This determines the styling and icon used.
|
||||
*/
|
||||
type: ToastType
|
||||
/**
|
||||
* A string, `Text`, or `Span` components to render inside the toast. This
|
||||
* allows additional formatting of the content, but should not be used for
|
||||
* interactive elements link links or buttons.
|
||||
*/
|
||||
content: React.ReactNode | string
|
||||
/**
|
||||
* Accessibility label for the toast, used for screen readers.
|
||||
*/
|
||||
a11yLabel: string
|
||||
/**
|
||||
* Defaults to `DEFAULT_TOAST_DURATION` from `#components/Toast/const`.
|
||||
*/
|
||||
duration?: number
|
||||
}) => void
|
||||
/**
|
||||
* Not all properties are available on all platforms, so we pick out only those
|
||||
* we support. Add more here as needed.
|
||||
*/
|
||||
export type BaseToastOptions = Pick<
|
||||
ExternalToast,
|
||||
'duration' | 'dismissible' | 'promiseOptions'
|
||||
> & {
|
||||
type?: ToastType
|
||||
|
||||
/**
|
||||
* These methods differ between web/native implementations
|
||||
*/
|
||||
onDismiss?: () => void
|
||||
onPress?: () => void
|
||||
onAutoClose?: () => void
|
||||
}
|
||||
|
||||
@@ -53,12 +53,14 @@ const TooltipContext = createContext<TooltipContextType>({
|
||||
visible: false,
|
||||
onVisibleChange: () => {},
|
||||
})
|
||||
TooltipContext.displayName = 'TooltipContext'
|
||||
|
||||
const TargetContext = createContext<TargetContextType>({
|
||||
targetMeasurements: undefined,
|
||||
setTargetMeasurements: () => {},
|
||||
shouldMeasure: false,
|
||||
})
|
||||
TargetContext.displayName = 'TargetContext'
|
||||
|
||||
export function Outer({
|
||||
children,
|
||||
|
||||
@@ -20,6 +20,7 @@ const TooltipContext = createContext<TooltipContextType>({
|
||||
position: 'bottom',
|
||||
onVisibleChange: () => {},
|
||||
})
|
||||
TooltipContext.displayName = 'TooltipContext'
|
||||
|
||||
export function Outer({
|
||||
children,
|
||||
|
||||
@@ -80,7 +80,7 @@ function Inner({
|
||||
]}>
|
||||
<Shield size="md" />
|
||||
</View>
|
||||
<View style={[a.flex_1, a.gap_xs, a.pr_2xl]}>
|
||||
<View style={[a.flex_1, a.gap_xs, a.pr_4xl]}>
|
||||
<Text style={[a.text_sm, a.leading_snug]}>{children}</Text>
|
||||
<Text style={[a.text_sm, a.leading_snug, a.font_bold]}>
|
||||
<Trans>
|
||||
|
||||
@@ -27,6 +27,7 @@ type ControlsContext = {
|
||||
}
|
||||
|
||||
const ControlsContext = createContext<ControlsContext | null>(null)
|
||||
ControlsContext.displayName = 'GlobalDialogControlsContext'
|
||||
|
||||
export function useGlobalDialogsControlContext() {
|
||||
const ctx = useContext(ControlsContext)
|
||||
|
||||
@@ -211,7 +211,9 @@ export function Verify({config, showScreen}: ScreenProps<ScreenID.Verify>) {
|
||||
<Trans>Verify your email</Trans>
|
||||
)
|
||||
) : (
|
||||
<Trans>Verify email code</Trans>
|
||||
<Trans comment="Dialog title when a user is verifying their email address by entering a code they have been sent">
|
||||
Verify email code
|
||||
</Trans>
|
||||
)}
|
||||
</Text>
|
||||
|
||||
@@ -345,7 +347,13 @@ export function Verify({config, showScreen}: ScreenProps<ScreenID.Verify>) {
|
||||
{state.error && <Admonition type="error">{state.error}</Admonition>}
|
||||
|
||||
<Button
|
||||
label={_(msg`Verify code`)}
|
||||
label={_(
|
||||
msg({
|
||||
message: `Verify code`,
|
||||
context: `action`,
|
||||
comment: `Button text and accessibility label for action to verify the user's email address using the code entered`,
|
||||
}),
|
||||
)}
|
||||
size="large"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
@@ -356,7 +364,11 @@ export function Verify({config, showScreen}: ScreenProps<ScreenID.Verify>) {
|
||||
state.mutationStatus === 'pending'
|
||||
}>
|
||||
<ButtonText>
|
||||
<Trans>Verify code</Trans>
|
||||
<Trans
|
||||
context="action"
|
||||
comment="Button text and accessibility label for action to verify the user's email address using the code entered">
|
||||
Verify code
|
||||
</Trans>
|
||||
</ButtonText>
|
||||
{state.mutationStatus === 'pending' && <ButtonIcon icon={Loader} />}
|
||||
</Button>
|
||||
|
||||
@@ -3,6 +3,7 @@ import {type AppBskyActorDefs} from '@atproto/api'
|
||||
|
||||
import {useGate} from '#/lib/statsig/statsig'
|
||||
import {logger} from '#/logger'
|
||||
import {STALE} from '#/state/queries'
|
||||
import {Nux, useNuxs, useResetNuxs, useSaveNux} from '#/state/queries/nuxs'
|
||||
import {
|
||||
usePreferencesQuery,
|
||||
@@ -47,6 +48,7 @@ const Context = React.createContext<Context>({
|
||||
activeNux: undefined,
|
||||
dismissActiveNux: () => {},
|
||||
})
|
||||
Context.displayName = 'NuxDialogContext'
|
||||
|
||||
export function useNuxDialogContext() {
|
||||
return React.useContext(Context)
|
||||
@@ -55,7 +57,10 @@ export function useNuxDialogContext() {
|
||||
export function NuxDialogs() {
|
||||
const {currentAccount} = useSession()
|
||||
const {data: preferences} = usePreferencesQuery()
|
||||
const {data: profile} = useProfileQuery({did: currentAccount?.did})
|
||||
const {data: profile} = useProfileQuery({
|
||||
did: currentAccount?.did,
|
||||
staleTime: STALE.INFINITY, // createdAt isn't gonna change
|
||||
})
|
||||
const onboardingActive = useOnboardingState().isActive
|
||||
|
||||
const isLoading =
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react'
|
||||
|
||||
const MessageContext = React.createContext(false)
|
||||
MessageContext.displayName = 'MessageContext'
|
||||
|
||||
export function MessageContextProvider({
|
||||
children,
|
||||
|
||||
@@ -5,9 +5,8 @@ import {type ChatBskyConvoDefs, RichText} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useOpenLink} from '#/lib/hooks/useOpenLink'
|
||||
import {useTranslate} from '#/lib/hooks/useTranslate'
|
||||
import {richTextToString} from '#/lib/strings/rich-text-helpers'
|
||||
import {getTranslatorLink} from '#/locale/helpers'
|
||||
import {logger} from '#/logger'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {useConvoActive} from '#/state/messages/convo'
|
||||
@@ -39,7 +38,7 @@ export let MessageContextMenu = ({
|
||||
const deleteControl = usePromptControl()
|
||||
const reportControl = usePromptControl()
|
||||
const langPrefs = useLanguagePrefs()
|
||||
const openLink = useOpenLink()
|
||||
const translate = useTranslate()
|
||||
|
||||
const isFromSelf = message.sender?.did === currentAccount?.did
|
||||
|
||||
@@ -57,11 +56,7 @@ export let MessageContextMenu = ({
|
||||
}, [_, message.text, message.facets])
|
||||
|
||||
const onPressTranslateMessage = useCallback(() => {
|
||||
const translatorUrl = getTranslatorLink(
|
||||
message.text,
|
||||
langPrefs.primaryLanguage,
|
||||
)
|
||||
openLink(translatorUrl, true)
|
||||
translate(message.text, langPrefs.primaryLanguage)
|
||||
|
||||
logger.metric(
|
||||
'translate',
|
||||
@@ -72,7 +67,7 @@ export let MessageContextMenu = ({
|
||||
},
|
||||
{statsig: false},
|
||||
)
|
||||
}, [langPrefs.primaryLanguage, message.text, openLink])
|
||||
}, [langPrefs.primaryLanguage, message.text, translate])
|
||||
|
||||
const onDelete = useCallback(() => {
|
||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
|
||||
|
||||
@@ -73,7 +73,8 @@ export function MessageProfileButton({
|
||||
a.align_center,
|
||||
t.atoms.bg_contrast_25,
|
||||
a.rounded_full,
|
||||
{width: 34, height: 34},
|
||||
// Matches size of button below to avoid layout shift
|
||||
{width: 33, height: 33},
|
||||
]}>
|
||||
<Message style={[t.atoms.text, {opacity: 0.3}]} size="md" />
|
||||
</View>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import {createContext, useContext, useMemo, useRef} from 'react'
|
||||
import {
|
||||
type AccessibilityProps,
|
||||
StyleSheet,
|
||||
@@ -16,7 +16,9 @@ import {
|
||||
applyFonts,
|
||||
atoms as a,
|
||||
ios,
|
||||
platform,
|
||||
type TextStyleProp,
|
||||
tokens,
|
||||
useAlf,
|
||||
useTheme,
|
||||
web,
|
||||
@@ -25,7 +27,7 @@ import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {type Props as SVGIconProps} from '#/components/icons/common'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
const Context = React.createContext<{
|
||||
const Context = createContext<{
|
||||
inputRef: React.RefObject<TextInput> | null
|
||||
isInvalid: boolean
|
||||
hovered: boolean
|
||||
@@ -44,11 +46,12 @@ const Context = React.createContext<{
|
||||
onFocus: () => {},
|
||||
onBlur: () => {},
|
||||
})
|
||||
Context.displayName = 'TextFieldContext'
|
||||
|
||||
export type RootProps = React.PropsWithChildren<{isInvalid?: boolean}>
|
||||
|
||||
export function Root({children, isInvalid = false}: RootProps) {
|
||||
const inputRef = React.useRef<TextInput>(null)
|
||||
const inputRef = useRef<TextInput>(null)
|
||||
const {
|
||||
state: hovered,
|
||||
onIn: onHoverIn,
|
||||
@@ -56,7 +59,7 @@ export function Root({children, isInvalid = false}: RootProps) {
|
||||
} = useInteractionState()
|
||||
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
|
||||
|
||||
const context = React.useMemo(
|
||||
const context = useMemo(
|
||||
() => ({
|
||||
inputRef,
|
||||
hovered,
|
||||
@@ -96,7 +99,7 @@ export function Root({children, isInvalid = false}: RootProps) {
|
||||
|
||||
export function useSharedInputStyles() {
|
||||
const t = useTheme()
|
||||
return React.useMemo(() => {
|
||||
return useMemo(() => {
|
||||
const hover: ViewStyle[] = [
|
||||
{
|
||||
borderColor: t.palette.contrast_100,
|
||||
@@ -158,7 +161,7 @@ export function createInput(Component: typeof TextInput) {
|
||||
}: InputProps) {
|
||||
const t = useTheme()
|
||||
const {fonts} = useAlf()
|
||||
const ctx = React.useContext(Context)
|
||||
const ctx = useContext(Context)
|
||||
const withinRoot = Boolean(ctx.inputRef)
|
||||
|
||||
const {chromeHover, chromeFocus, chromeError, chromeErrorHover} =
|
||||
@@ -283,8 +286,8 @@ export function LabelText({
|
||||
|
||||
export function Icon({icon: Comp}: {icon: React.ComponentType<SVGIconProps>}) {
|
||||
const t = useTheme()
|
||||
const ctx = React.useContext(Context)
|
||||
const {hover, focus, errorHover, errorFocus} = React.useMemo(() => {
|
||||
const ctx = useContext(Context)
|
||||
const {hover, focus, errorHover, errorFocus} = useMemo(() => {
|
||||
const hover: TextStyle[] = [
|
||||
{
|
||||
color: t.palette.contrast_800,
|
||||
@@ -342,7 +345,7 @@ export function SuffixText({
|
||||
}
|
||||
>) {
|
||||
const t = useTheme()
|
||||
const ctx = React.useContext(Context)
|
||||
const ctx = useContext(Context)
|
||||
return (
|
||||
<Text
|
||||
accessibilityLabel={label}
|
||||
@@ -362,3 +365,67 @@ export function SuffixText({
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
export function GhostText({
|
||||
children,
|
||||
value,
|
||||
}: {
|
||||
children: string
|
||||
value: string
|
||||
}) {
|
||||
const t = useTheme()
|
||||
// eslint-disable-next-line bsky-internal/avoid-unwrapped-text
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.pointer_events_none,
|
||||
a.absolute,
|
||||
a.z_10,
|
||||
{
|
||||
paddingLeft: platform({
|
||||
native:
|
||||
// input padding
|
||||
tokens.space.md +
|
||||
// icon
|
||||
tokens.space.xl +
|
||||
// icon padding
|
||||
tokens.space.xs +
|
||||
// text input padding
|
||||
tokens.space.xs,
|
||||
web:
|
||||
// icon
|
||||
tokens.space.xl +
|
||||
// icon padding
|
||||
tokens.space.xs +
|
||||
// text input padding
|
||||
tokens.space.xs,
|
||||
}),
|
||||
},
|
||||
web(a.pr_md),
|
||||
a.overflow_hidden,
|
||||
a.max_w_full,
|
||||
]}
|
||||
aria-hidden={true}
|
||||
accessibilityElementsHidden
|
||||
importantForAccessibility="no-hide-descendants">
|
||||
<Text
|
||||
style={[
|
||||
{color: 'transparent'},
|
||||
a.text_md,
|
||||
{lineHeight: a.text_md.fontSize * 1.1875},
|
||||
a.w_full,
|
||||
]}
|
||||
numberOfLines={1}>
|
||||
{children}
|
||||
<Text
|
||||
style={[
|
||||
t.atoms.text_contrast_low,
|
||||
a.text_md,
|
||||
{lineHeight: a.text_md.fontSize * 1.1875},
|
||||
]}>
|
||||
{value}
|
||||
</Text>
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import {Pressable, View, ViewStyle} from 'react-native'
|
||||
import {Pressable, View, type ViewStyle} from 'react-native'
|
||||
import Animated, {LinearTransition} from 'react-native-reanimated'
|
||||
|
||||
import {HITSLOP_10} from '#/lib/constants'
|
||||
@@ -8,9 +8,9 @@ import {
|
||||
atoms as a,
|
||||
flatten,
|
||||
native,
|
||||
TextStyleProp,
|
||||
type TextStyleProp,
|
||||
useTheme,
|
||||
ViewStyleProp,
|
||||
type ViewStyleProp,
|
||||
} from '#/alf'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {CheckThick_Stroke2_Corner0_Rounded as Checkmark} from '#/components/icons/Check'
|
||||
@@ -35,6 +35,7 @@ const ItemContext = React.createContext<ItemState>({
|
||||
pressed: false,
|
||||
focused: false,
|
||||
})
|
||||
ItemContext.displayName = 'ToggleItemContext'
|
||||
|
||||
const GroupContext = React.createContext<{
|
||||
values: string[]
|
||||
@@ -49,6 +50,7 @@ const GroupContext = React.createContext<{
|
||||
maxSelectionsReached: false,
|
||||
setFieldValue: () => {},
|
||||
})
|
||||
GroupContext.displayName = 'ToggleGroupContext'
|
||||
|
||||
export type GroupProps = React.PropsWithChildren<{
|
||||
type?: 'radio' | 'checkbox'
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import {createSinglePathSVG} from './TEMPLATE'
|
||||
|
||||
export const ArrowCornerDownRight_Stroke2_Corner2_Rounded = createSinglePathSVG(
|
||||
{
|
||||
path: 'M15.793 10.293a1 1 0 0 1 1.338-.068l.076.068 3.293 3.293a2 2 0 0 1 .138 2.677l-.138.151-3.293 3.293a1 1 0 1 1-1.414-1.414L18.086 16H8a5 5 0 0 1-5-5V5a1 1 0 0 1 2 0v6a3 3 0 0 0 3 3h10.086l-2.293-2.293-.068-.076a1 1 0 0 1 .068-1.338Z',
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,5 @@
|
||||
import {createSinglePathSVG} from './TEMPLATE'
|
||||
|
||||
export const Circle_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Z',
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {DialogControlProps} from '#/components/Dialog'
|
||||
import {type DialogControlProps} from '#/components/Dialog'
|
||||
import {VerifyEmailIntentDialog} from '#/components/intents/VerifyEmailIntentDialog'
|
||||
|
||||
interface Context {
|
||||
@@ -11,6 +11,7 @@ interface Context {
|
||||
}
|
||||
|
||||
const Context = React.createContext({} as Context)
|
||||
Context.displayName = 'IntentDialogsContext'
|
||||
export const useIntentDialogs = () => React.useContext(Context)
|
||||
|
||||
export function Provider({children}: {children: React.ReactNode}) {
|
||||
|
||||
@@ -240,7 +240,7 @@ function DialogInner({
|
||||
label={_(msg`Remove live status`)}
|
||||
onPress={() => removeLiveStatus()}
|
||||
size={platform({native: 'large', web: 'small'})}
|
||||
color="negative_secondary"
|
||||
color="negative_subtle"
|
||||
variant="solid"
|
||||
disabled={isRemovingLiveStatus || isGoingLive}>
|
||||
<ButtonText>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react'
|
||||
import {ModerationUI} from '@atproto/api'
|
||||
import {type ModerationUI} from '@atproto/api'
|
||||
|
||||
import {
|
||||
ModerationCauseDescription,
|
||||
type ModerationCauseDescription,
|
||||
useModerationCauseDescription,
|
||||
} from '#/lib/moderation/useModerationCauseDescription'
|
||||
import {
|
||||
@@ -22,6 +22,7 @@ type Context = {
|
||||
}
|
||||
|
||||
const Context = React.createContext<Context>({} as Context)
|
||||
Context.displayName = 'HiderContext'
|
||||
|
||||
export const useHider = () => React.useContext(Context)
|
||||
|
||||
|
||||
Vendored
+9
@@ -78,3 +78,12 @@ export const SENTRY_DSN: string | undefined = process.env.EXPO_PUBLIC_SENTRY_DSN
|
||||
*/
|
||||
export const BITDRIFT_API_KEY: string | undefined =
|
||||
process.env.EXPO_PUBLIC_BITDRIFT_API_KEY
|
||||
|
||||
/**
|
||||
* GCP project ID which is required for native device attestation. On web, this
|
||||
* should be unset and evaluate to 0.
|
||||
*/
|
||||
export const GCP_PROJECT_ID: number =
|
||||
process.env.EXPO_PUBLIC_GCP_PROJECT_ID === undefined
|
||||
? 0
|
||||
: Number(process.env.EXPO_PUBLIC_GCP_PROJECT_ID)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, {createContext, useContext, useMemo} from 'react'
|
||||
import {ScrollHandlers} from 'react-native-reanimated'
|
||||
import {createContext, useContext, useMemo} from 'react'
|
||||
import {type ScrollHandlers} from 'react-native-reanimated'
|
||||
|
||||
const ScrollContext = createContext<ScrollHandlers<any>>({
|
||||
onBeginDrag: undefined,
|
||||
@@ -7,6 +7,7 @@ const ScrollContext = createContext<ScrollHandlers<any>>({
|
||||
onScroll: undefined,
|
||||
onMomentumEnd: undefined,
|
||||
})
|
||||
ScrollContext.displayName = 'ScrollContext'
|
||||
|
||||
export function useScrollHandlers(): ScrollHandlers<any> {
|
||||
return useContext(ScrollContext)
|
||||
|
||||
@@ -89,6 +89,7 @@ export interface ThemeProviderProps {
|
||||
}
|
||||
|
||||
export const ThemeContext = createContext<Theme>(defaultTheme)
|
||||
ThemeContext.displayName = 'ThemeContext'
|
||||
|
||||
export const useTheme = () => useContext(ThemeContext)
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user