Compare commits

..

2 Commits

Author SHA1 Message Date
Eric Bailey 104bcf2055 Update var name 2025-08-08 09:47:30 -05:00
Eric Bailey e7506277f9 Add env to scripts 2025-08-08 09:38:16 -05:00
223 changed files with 11562 additions and 11842 deletions
+1 -1
View File
@@ -53,7 +53,7 @@ jobs:
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '16.4'
xcode-version: '16.2'
- name: ☕️ Setup Cocoapods
uses: maxim-lobanov/setup-cocoapods@v1
+1 -2
View File
@@ -50,7 +50,6 @@ 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: |
@@ -74,7 +73,6 @@ 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
@@ -145,3 +143,4 @@ jobs:
with:
header: fingerprint-diff
delete: true
+5 -6
View File
@@ -1,8 +1,7 @@
import React, {type ReactNode} from 'react'
import {FlatList, Modal, ScrollView, TextInput, View} from 'react-native'
import React, {ReactNode} from 'react'
import {View, ScrollView, Modal, FlatList, TextInput} from 'react-native'
const BottomSheetModalContext = React.createContext(null)
BottomSheetModalContext.displayName = 'BottomSheetModalContext'
const BottomSheetModalProvider = (props: any) => {
return <BottomSheetModalContext.Provider {...props} value={{}} />
@@ -48,13 +47,13 @@ export {useBottomSheetInternal}
export {useBottomSheetDynamicSnapPoints}
export {
BottomSheetModalProvider,
BottomSheetBackdrop,
BottomSheetFlatList,
BottomSheetFooter,
BottomSheetHandle,
BottomSheetModal,
BottomSheetModalProvider,
BottomSheetFooter,
BottomSheetScrollView,
BottomSheetFlatList,
BottomSheetTextInput,
}
+3 -6
View File
@@ -234,9 +234,8 @@ module.exports = function (_config) {
],
'./plugins/starterPackAppClipExtension/withStarterPackAppClip.js',
'./plugins/withGradleJVMHeapSizeIncrease.js',
'./plugins/withAndroidManifestLargeHeapPlugin.js',
'./plugins/withAndroidManifestPlugin.js',
'./plugins/withAndroidManifestFCMIconPlugin.js',
'./plugins/withAndroidManifestIntentQueriesPlugin.js',
'./plugins/withAndroidStylesAccentColorPlugin.js',
'./plugins/withAndroidDayNightThemePlugin.js',
'./plugins/withAndroidNoJitpackPlugin.js',
@@ -251,12 +250,10 @@ 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-Bold.otf',
'./assets/fonts/inter/Inter-BoldItalic.otf',
'./assets/fonts/inter/Inter-ExtraBold.otf',
'./assets/fonts/inter/Inter-ExtraBoldItalic.otf',
],
},
],
@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 334 B

-1
View File
@@ -8,7 +8,6 @@
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 -->
+7 -7
View File
@@ -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
@@ -19,6 +19,7 @@ import expo.modules.kotlin.AppContext
import expo.modules.kotlin.viewevent.EventDispatcher
import expo.modules.kotlin.views.ExpoView
class BottomSheetView(
context: Context,
appContext: AppContext,
@@ -30,14 +31,12 @@ 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()
@@ -46,7 +45,7 @@ class BottomSheetView(
// Props
var disableDrag = false
set(value) {
set (value) {
field = value
this.setDraggable(!value)
}
@@ -158,8 +157,8 @@ class BottomSheetView(
// Presentation
private fun getHalfExpandedRatio(contentHeight: Float): Float =
when {
private fun getHalfExpandedRatio(contentHeight: Float): Float {
return when {
// Full height sheets
contentHeight >= safeScreenHeight -> 0.99f
// Medium height sheets (>50% but <100%)
@@ -169,6 +168,7 @@ class BottomSheetView(
else ->
this.clampRatio(this.getTargetHeight() / rawScreenHeight)
}
}
private fun present() {
if (this.isOpen || this.isOpening || this.isClosing) return
@@ -139,10 +139,7 @@ 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,7 +5,6 @@ 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)
-1
View File
@@ -18,7 +18,6 @@ export function createPortalGroup_INTERNAL() {
append: () => {},
remove: () => {},
})
Context.displayName = 'BottomSheetPortalContext'
function Provider(props: React.PropsWithChildren<{}>) {
const map = React.useRef<ComponentMap>({})
@@ -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,
),
)
}
@@ -5,12 +5,11 @@ 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")
}
}
}
@@ -8,35 +8,33 @@ 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()
}
private fun setupView() {
addView(
emojiView,
LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT,
),
)
emojiView.setOnEmojiPickedListener { emoji ->
onEmojiSelected(mapOf("emoji" to emoji.emoji))
init {
setupView()
}
}
override fun onConfigurationChanged(newConfig: Configuration?) {
super.onConfigurationChanged(newConfig)
removeView(emojiView)
setupView()
}
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()
}
}
+2 -6
View File
@@ -1,6 +1,6 @@
{
"name": "bsky.app",
"version": "1.107.0",
"version": "1.106.0",
"private": true,
"engines": {
"node": ">=20"
@@ -20,7 +20,7 @@
},
"scripts": {
"prepare": "is-ci || husky install",
"postinstall": "patch-package && yarn intl:compile-if-needed",
"postinstall": "patch-package && yarn intl:compile",
"prebuild": "expo prebuild --clean",
"android": "expo run:android",
"android:prod": "expo run:android --variant release",
@@ -58,7 +58,6 @@
"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",
@@ -147,7 +146,6 @@
"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",
@@ -214,8 +212,6 @@
"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",
@@ -1,30 +0,0 @@
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
+12 -17
View File
@@ -29,7 +29,6 @@ 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,
@@ -74,7 +73,6 @@ import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialo
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'
@@ -157,21 +155,18 @@ function InnerApp() {
<MutedThreadsProvider>
<ProgressGuideProvider>
<ServiceAccountManager>
<EmailVerificationProvider>
<HideBottomBarBorderProvider>
<GestureHandlerRootView
style={s.h100pct}>
<GlobalGestureEventsProvider>
<IntentDialogProvider>
<TestCtrls />
<Shell />
<NuxDialogs />
<ToastOutlet />
</IntentDialogProvider>
</GlobalGestureEventsProvider>
</GestureHandlerRootView>
</HideBottomBarBorderProvider>
</EmailVerificationProvider>
<HideBottomBarBorderProvider>
<GestureHandlerRootView
style={s.h100pct}>
<GlobalGestureEventsProvider>
<IntentDialogProvider>
<TestCtrls />
<Shell />
<NuxDialogs />
</IntentDialogProvider>
</GlobalGestureEventsProvider>
</GestureHandlerRootView>
</HideBottomBarBorderProvider>
</ServiceAccountManager>
</ProgressGuideProvider>
</MutedThreadsProvider>
+8 -11
View File
@@ -18,7 +18,6 @@ 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,
@@ -62,7 +61,7 @@ import {Provider as PolicyUpdateOverlayProvider} from '#/components/PolicyUpdate
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 {ToastOutlet} from '#/components/Toast'
import {ToastContainer} from '#/components/Toast'
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottomBarBorder'
@@ -137,15 +136,12 @@ function InnerApp() {
<SafeAreaProvider>
<ProgressGuideProvider>
<ServiceConfigProvider>
<EmailVerificationProvider>
<HideBottomBarBorderProvider>
<IntentDialogProvider>
<Shell />
<NuxDialogs />
<ToastOutlet />
</IntentDialogProvider>
</HideBottomBarBorderProvider>
</EmailVerificationProvider>
<HideBottomBarBorderProvider>
<IntentDialogProvider>
<Shell />
<NuxDialogs />
</IntentDialogProvider>
</HideBottomBarBorderProvider>
</ServiceConfigProvider>
</ProgressGuideProvider>
</SafeAreaProvider>
@@ -164,6 +160,7 @@ function InnerApp() {
</StatsigProvider>
</PolicyUpdateOverlayProvider>
</QueryProvider>
<ToastContainer />
</React.Fragment>
</ActiveVideoProvider>
</VideoVolumeProvider>
-3
View File
@@ -332,9 +332,6 @@ export const atoms = {
font_normal: {
fontWeight: tokens.fontWeight.normal,
},
font_medium: {
fontWeight: tokens.fontWeight.medium,
},
font_bold: {
fontWeight: tokens.fontWeight.bold,
},
+6 -6
View File
@@ -1,7 +1,7 @@
import {type TextStyle} from 'react-native'
import {TextStyle} from 'react-native'
import {isAndroid, isWeb} from '#/platform/detection'
import {type Device, device} from '#/storage'
import {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-Medium',
500: 'Inter-Regular',
600: 'Inter-SemiBold',
700: 'Inter-Bold',
800: 'Inter-Bold',
900: 'Inter-Bold',
700: 'Inter-SemiBold',
800: 'Inter-ExtraBold',
900: 'Inter-ExtraBold',
}[String(style.fontWeight || '400')] || 'Inter-Regular'
if (style.fontStyle === 'italic') {
+2 -3
View File
@@ -8,9 +8,9 @@ import {
setFontScale as persistFontScale,
} from '#/alf/fonts'
import {createThemes, defaultTheme} from '#/alf/themes'
import {type Theme, type ThemeName} from '#/alf/types'
import {Theme, ThemeName} from '#/alf/types'
import {BLUE_HUE, GREEN_HUE, RED_HUE} from '#/alf/util/colorGeneration'
import {type Device} from '#/storage'
import {Device} from '#/storage'
export {atoms} from '#/alf/atoms'
export * from '#/alf/breakpoints'
@@ -61,7 +61,6 @@ export const Context = React.createContext<Alf>({
},
flags: {},
})
Context.displayName = 'AlfContext'
export function ThemeProvider({
children,
-1
View File
@@ -53,7 +53,6 @@ export const borderRadius = {
*/
export const fontWeight = {
normal: '400',
medium: '500',
bold: '600',
heavy: '800',
} as const
+4 -6
View File
@@ -11,11 +11,9 @@ 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-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'),
'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'),
})
}
-1
View File
@@ -23,7 +23,6 @@ type Context = {
const Context = createContext<Context>({
type: 'info',
})
Context.displayName = 'AdmonitionContext'
export function Icon() {
const t = useTheme()
+16 -17
View File
@@ -109,7 +109,6 @@ const Context = React.createContext<VariantProps & ButtonState>({
pressed: false,
disabled: false,
})
Context.displayName = 'ButtonContext'
export function useButtonContext() {
return React.useContext(Context)
@@ -461,22 +460,22 @@ export const Button = React.forwardRef<View, ButtonProps>(
if (shape === 'default') {
if (size === 'large') {
baseStyles.push({
paddingVertical: 12,
paddingHorizontal: 25,
paddingVertical: 14,
paddingHorizontal: 24,
borderRadius: 10,
gap: 3,
gap: 4,
})
} else if (size === 'small') {
baseStyles.push({
paddingVertical: 8,
paddingHorizontal: 13,
paddingHorizontal: 12,
borderRadius: 8,
gap: 3,
})
} else if (size === 'tiny') {
baseStyles.push({
paddingVertical: 5,
paddingHorizontal: 9,
paddingVertical: 6,
paddingHorizontal: 8,
borderRadius: 6,
gap: 2,
})
@@ -488,9 +487,9 @@ export const Button = React.forwardRef<View, ButtonProps>(
*/
if (size === 'large') {
if (shape === 'round') {
baseStyles.push({height: 44, width: 44})
baseStyles.push({height: 45, width: 45})
} else {
baseStyles.push({height: 44, width: 44})
baseStyles.push({height: 45, width: 45})
}
} else if (size === 'small') {
if (shape === 'round') {
@@ -759,11 +758,11 @@ export function useSharedButtonTextStyles() {
}
if (size === 'large') {
baseStyles.push(a.text_md, a.leading_snug, a.font_medium)
baseStyles.push(a.text_md, a.leading_tight)
} else if (size === 'small') {
baseStyles.push(a.text_sm, a.leading_snug, a.font_medium)
baseStyles.push(a.text_md, a.leading_tight)
} else if (size === 'tiny') {
baseStyles.push(a.text_xs, a.leading_snug, a.font_medium)
baseStyles.push(a.text_xs, a.leading_tight)
}
return StyleSheet.flatten(baseStyles)
@@ -774,7 +773,7 @@ export function ButtonText({children, style, ...rest}: ButtonTextProps) {
const textStyles = useSharedButtonTextStyles()
return (
<Text {...rest} style={[a.text_center, textStyles, style]}>
<Text {...rest} style={[a.font_bold, a.text_center, textStyles, style]}>
{children}
</Text>
)
@@ -800,7 +799,7 @@ export function ButtonIcon({
const iconSizeShorthand =
size ??
(({
large: 'md',
large: 'sm',
small: 'sm',
tiny: 'xs',
}[buttonSize || 'small'] || 'sm') as Exclude<
@@ -815,7 +814,7 @@ export function ButtonIcon({
const iconSize = {
xs: 12,
sm: 16,
md: 18,
md: 20,
lg: 24,
xl: 28,
'2xl': 32,
@@ -826,9 +825,9 @@ export function ButtonIcon({
* don't increase button size
*/
const iconContainerSize = {
large: 20,
large: 17,
small: 17,
tiny: 15,
tiny: 13,
}[buttonSize || 'small']
return {
-3
View File
@@ -7,13 +7,10 @@ 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)
-1
View File
@@ -23,7 +23,6 @@ export const Context = createContext<DialogContextProps>({
setDisableDrag: () => {},
isWithinDialog: false,
})
Context.displayName = 'DialogContext'
export function useDialogContext() {
return useContext(Context)
+1 -2
View File
@@ -1,12 +1,11 @@
import {createContext, useContext, useMemo} from 'react'
import {View} from 'react-native'
import {atoms as a, type ViewStyleProp} from '#/alf'
import {atoms as a, ViewStyleProp} from '#/alf'
const Context = createContext({
gap: 0,
})
Context.displayName = 'GridContext'
export function Row({
children,
-1
View File
@@ -77,7 +77,6 @@ export function Outer({
}
const AlignmentContext = createContext<'platform' | 'left'>('platform')
AlignmentContext.displayName = 'AlignmentContext'
export function Content({
children,
-1
View File
@@ -3,4 +3,3 @@ import React from 'react'
export const ScrollbarOffsetContext = React.createContext({
isWithinOffsetView: false,
})
ScrollbarOffsetContext.displayName = 'ScrollbarOffsetContext'
-2
View File
@@ -3,10 +3,8 @@ 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)
@@ -28,7 +28,6 @@ const Context = createContext<{
*/
setIsReadyToShowOverlay: () => {},
})
Context.displayName = 'PolicyUpdateOverlayContext'
export function usePolicyUpdateContext() {
const context = useContext(Context)
@@ -46,7 +46,7 @@ export function Content({state}: {state: PolicyUpdateState}) {
},
blog: {
overridePresentation: false,
to: `https://bsky.social/about/blog/08-14-2025-updated-terms-and-policies`,
to: `https://bsky.social/about/blog/08-11-2025-updated-terms-and-policies`,
label: _(msg`Our blog post`),
},
}
@@ -58,7 +58,7 @@ export function Content({state}: {state: PolicyUpdateState}) {
const label = isAndroid
? _(
msg`Were 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`Were updating our Terms of Service, Privacy Policy, and Copyright Policy, effective September 12th, 2025. We're also updating our Community Guidelines, and we want your input! These new guidelines will take effect on October 13th, 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`)
@@ -75,13 +75,13 @@ export function Content({state}: {state: PolicyUpdateState}) {
<Text style={[a.leading_snug, a.text_md]}>
<Trans>
Were updating our Terms of Service, Privacy Policy, and
Copyright Policy, effective September 15th, 2025.
Copyright Policy, effective September 12th, 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,
input! These new guidelines will take effect on October 13th,
2025.
</Trans>
</Text>
@@ -132,7 +132,7 @@ export function Content({state}: {state: PolicyUpdateState}) {
<InlineLinkText {...links.copyright} style={linkStyle}>
Copyright Policy
</InlineLinkText>
, effective September 15th, 2025.
, effective September 12th, 2025.
</Trans>
</Text>
<Text style={[a.leading_snug, a.text_md]}>
@@ -142,7 +142,7 @@ export function Content({state}: {state: PolicyUpdateState}) {
Community Guidelines
</InlineLinkText>
, and we want your input! These new guidelines will take effect
on October 15th, 2025.
on October 13th, 2025.
</Trans>
</Text>
<Text style={[a.leading_snug, a.text_md]}>
-1
View File
@@ -28,7 +28,6 @@ export function createPortalGroup() {
append: () => {},
remove: () => {},
})
Context.displayName = 'PortalContext'
function Provider(props: React.PropsWithChildren<{}>) {
const map = useRef<ComponentMap>({})
@@ -15,7 +15,6 @@ 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,7 +8,6 @@ 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,7 +125,6 @@ export function VideoEmbed({
}
const NearScreenContext = createContext(false)
NearScreenContext.displayName = 'VideoNearScreenContext'
/**
* Renders a 100vh tall div and watches it with an IntersectionObserver to
-63
View File
@@ -1,63 +0,0 @@
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,7 +32,8 @@ export function DiscoverDebug({
hitSlop={10}
style={[
a.absolute,
{zIndex: 1000, maxWidth: 65, bottom: -4},
a.bottom_0,
{zIndex: 1000},
gtMobile ? a.right_0 : a.left_0,
]}
onPress={e => {
@@ -41,7 +42,6 @@ export function DiscoverDebug({
Toast.show(t`Copied to clipboard`, 'clipboard-check')
}}>
<Text
numberOfLines={1}
style={{
color: theme.palette.contrast_400,
fontSize: 7,
@@ -13,7 +13,6 @@ const PostControlContext = createContext<{
active?: boolean
color?: {color: string}
}>({})
PostControlContext.displayName = 'PostControlContext'
// Base button style, which the the other ones extend
export function PostControlButton({
@@ -19,7 +19,6 @@ 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 {
@@ -29,6 +28,7 @@ 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,7 +118,6 @@ let PostMenuItems = ({
const {hidePost} = useHiddenPostsApi()
const feedFeedback = useFeedFeedbackContext()
const openLink = useOpenLink()
const translate = useTranslate()
const navigation = useNavigation<NavigationProp>()
const {mutedWordsDialogControl} = useGlobalDialogsControlContext()
const blockPromptControl = useDialogControl()
@@ -173,6 +172,11 @@ let PostMenuItems = ({
return makeProfileLink(postAuthor, 'post', urip.rkey)
}, [postUri, postAuthor])
const translatorUrl = getTranslatorLink(
record.text,
langPrefs.primaryLanguage,
)
const onDeletePost = () => {
deletePostMutate({uri: postUri}).then(
() => {
@@ -230,8 +234,8 @@ let PostMenuItems = ({
Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
}
const onPressTranslate = () => {
translate(record.text, langPrefs.primaryLanguage)
const onPressTranslate = async () => {
await openLink(translatorUrl, true)
if (
bsky.dangerousIsType<AppBskyFeedPost.Record>(
+2 -2
View File
@@ -145,7 +145,7 @@ let RepostButtonDialogInner = ({
<View style={a.gap_xl}>
<View style={a.gap_xs}>
<Button
style={[a.justify_start, a.px_md, a.gap_sm]}
style={[a.justify_start, a.px_md]}
label={
isReposted
? _(msg`Remove repost`)
@@ -167,7 +167,7 @@ let RepostButtonDialogInner = ({
<Button
disabled={embeddingDisabled}
testID="quoteBtn"
style={[a.justify_start, a.px_md, a.gap_sm]}
style={[a.justify_start, a.px_md]}
label={
embeddingDisabled
? _(msg`Quote posts disabled`)
@@ -4,12 +4,10 @@ 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'
@@ -59,11 +57,7 @@ export function RecentChats({postUri}: {postUri: string}) {
member => member.did !== currentAccount?.did,
)
if (
!otherMember ||
otherMember.handle === 'missing.invalid' ||
convo.muted
)
if (!otherMember || otherMember.handle === 'missing.invalid')
return null
return (
@@ -93,7 +87,7 @@ export function RecentChats({postUri}: {postUri: string}) {
const WIDTH = 80
function RecentChatItem({
profile: profileUnshadowed,
profile,
onPress,
moderationOpts,
}: {
@@ -104,8 +98,6 @@ function RecentChatItem({
const {_} = useLingui()
const t = useTheme()
const profile = useProfileShadow(profileUnshadowed)
const moderation = moderateProfile(profile, moderationOpts)
const name = sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle),
@@ -113,10 +105,6 @@ function RecentChatItem({
)
const verification = useSimpleVerificationState({profile})
if (isBlockedOrBlocking(profile) || isMuted(profile)) {
return null
}
return (
<Button
onPress={onPress}
@@ -539,7 +539,7 @@ let Tab = ({
]}>
<Text
style={[
a.font_medium,
/* TODO: medium weight */
active || hovered || pressed || focused
? t.atoms.text
: t.atoms.text_contrast_medium,
-1
View File
@@ -27,7 +27,6 @@ const Context = React.createContext<{
titleId: '',
descriptionId: '',
})
Context.displayName = 'PromptContext'
export function Outer({
children,
-3
View File
@@ -34,12 +34,10 @@ 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)
@@ -231,7 +229,6 @@ const ItemContext = createContext<{
focused: false,
pressed: false,
})
ItemContext.displayName = 'SelectItemContext'
export function useItemContext() {
return useContext(ItemContext)
-2
View File
@@ -23,7 +23,6 @@ import {
} from './types'
const SelectedValueContext = createContext<string | undefined | null>(null)
SelectedValueContext.displayName = 'SelectSelectedValueContext'
export function Root(props: RootProps) {
return (
@@ -220,7 +219,6 @@ const ItemContext = createContext<{
pressed: false,
selected: false,
})
ItemContext.displayName = 'SelectItemContext'
export function useItemContext() {
return useContext(ItemContext)
@@ -1,11 +1,12 @@
import React, {useCallback} from 'react'
import {type ListRenderItemInfo, View} from 'react-native'
import {type AppBskyFeedDefs} from '@atproto/api'
import {ListRenderItemInfo, View} from 'react-native'
import {AppBskyFeedDefs} from '@atproto/api'
import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset'
import {isNative, isWeb} from '#/platform/detection'
import {List, type ListRef} from '#/view/com/util/List'
import {type SectionRef} from '#/screens/Profile/Sections/types'
import {List, ListRef} from '#/view/com/util/List'
import {SectionRef} from '#/screens/Profile/Sections/types'
import {atoms as a, useTheme} from '#/alf'
import * as FeedCard from '#/components/FeedCard'
@@ -36,10 +37,7 @@ export const FeedsList = React.forwardRef<SectionRef, ProfilesListProps>(
scrollToTop: onScrollToTop,
}))
const renderItem = ({
item,
index,
}: ListRenderItemInfo<AppBskyFeedDefs.GeneratorView>) => {
const renderItem = ({item, index}: ListRenderItemInfo<GeneratorView>) => {
return (
<View
style={[
@@ -1,22 +1,16 @@
import {useRef} from 'react'
import {type ListRenderItemInfo} from 'react-native'
import type {ListRenderItemInfo} from 'react-native'
import {View} from 'react-native'
import {
type AppBskyActorDefs,
type AppBskyFeedDefs,
type ModerationOpts,
} from '@atproto/api'
import {AppBskyActorDefs, ModerationOpts} 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'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {isWeb} from '#/platform/detection'
import {useSession} from '#/state/session'
import {type ListMethods} from '#/view/com/util/List'
import {
type WizardAction,
type WizardState,
} from '#/screens/StarterPack/Wizard/State'
import {ListMethods} from '#/view/com/util/List'
import {WizardAction, 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'
@@ -27,7 +21,7 @@ import {
import {Text} from '#/components/Typography'
function keyExtractor(
item: AppBskyActorDefs.ProfileViewBasic | AppBskyFeedDefs.GeneratorView,
item: AppBskyActorDefs.ProfileViewBasic | GeneratorView,
index: number,
) {
return `${item.did}-${index}`
@@ -1,12 +1,13 @@
import {Keyboard, View} from 'react-native'
import {
type AppBskyActorDefs,
type AppBskyFeedDefs,
AppBskyActorDefs,
AppBskyFeedDefs,
moderateFeedGenerator,
moderateProfile,
type ModerationOpts,
type ModerationUI,
ModerationOpts,
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'
@@ -15,16 +16,13 @@ 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 {
type WizardAction,
type WizardState,
} from '#/screens/StarterPack/Wizard/State'
import {WizardAction, 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 type * as bsky from '#/types/bsky'
import * as bsky from '#/types/bsky'
function WizardListCard({
type,
@@ -176,7 +174,7 @@ export function WizardFeedCard({
moderationOpts,
}: {
btnType: 'checkbox' | 'remove'
generator: AppBskyFeedDefs.GeneratorView
generator: GeneratorView
state: WizardState
dispatch: (action: WizardAction) => void
moderationOpts: ModerationOpts
+8 -10
View File
@@ -13,11 +13,6 @@ type ContextType = {
type: ToastType
}
export type ToastComponentProps = {
type?: ToastType
content: React.ReactNode
}
export const ICONS = {
default: CircleCheck,
success: CircleCheck,
@@ -29,9 +24,14 @@ export const ICONS = {
const Context = createContext<ContextType>({
type: 'default',
})
Context.displayName = 'ToastContext'
export function Toast({type = 'default', content}: ToastComponentProps) {
export function Toast({
type,
content,
}: {
type: ToastType
content: React.ReactNode
}) {
const {fonts} = useAlf()
const t = useTheme()
const styles = useToastStyles({type})
@@ -89,12 +89,10 @@ export function ToastText({children}: {children: React.ReactNode}) {
const {textColor} = useToastStyles({type})
return (
<Text
selectable={false}
style={[
a.text_md,
a.font_medium,
a.font_bold,
a.leading_snug,
a.pointer_events_none,
{
color: textColor,
},
+1 -1
View File
@@ -1 +1 @@
export const DURATION = 3e3
export const DEFAULT_TOAST_DURATION = 3000
+6 -13
View File
@@ -1,16 +1,9 @@
export function ToastOutlet() {
import {type ToastApi} from '#/components/Toast/types'
export function ToastContainer() {
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() {}
export const toast: ToastApi = {
show() {},
}
+189 -41
View File
@@ -1,49 +1,197 @@
import {View} from 'react-native'
import {toast as sonner, Toaster} from 'sonner-native'
import {atoms as a} from '#/alf'
import {DURATION} from '#/components/Toast/const'
import {useEffect, useMemo, useRef, useState} from 'react'
import {AccessibilityInfo} from 'react-native'
import {
Toast as BaseToast,
type ToastComponentProps,
} from '#/components/Toast/Toast'
import {type BaseToastOptions} from '#/components/Toast/types'
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'
export {DURATION} from '#/components/Toast/const'
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'
/**
* 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} />
const TOAST_ANIMATION_DURATION = 300
export function ToastContainer() {
return null
}
/**
* The toast UI component
*/
export function Toast({type, content}: ToastComponentProps) {
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,
},
],
}
})
return (
<View style={[a.px_xl, a.w_full]}>
<BaseToast content={content} type={type} />
</View>
<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>
)
}
/**
* 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,
})
}
+105 -33
View File
@@ -1,40 +1,112 @@
import {toast as sonner, Toaster} from 'sonner'
import {atoms as a} from '#/alf'
import {DURATION} from '#/components/Toast/const'
import {Toast} from '#/components/Toast/Toast'
import {type BaseToastOptions} from '#/components/Toast/types'
/**
* Toasts are rendered in a global outlet, which is placed at the top of the
* component tree.
/*
* Note: relies on styles in #/styles.css
*/
export function ToastOutlet() {
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 {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])
return (
<Toaster
position="bottom-left"
gap={a.gap_sm.gap}
offset={a.p_xl.padding}
mobileOffset={a.p_xl.padding}
/>
<>
{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>
)}
</>
)
}
/**
* Access the full Sonner API
*/
export const api = sonner
export const toast: ToastApi = {
show(props) {
if (toastTimeout) {
clearTimeout(toastTimeout)
}
/**
* 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,
})
globalSetActiveToast?.({
type: props.type,
content: props.content,
a11yLabel: props.a11yLabel,
})
toastTimeout = setTimeout(() => {
globalSetActiveToast?.(undefined)
}, props.duration || DEFAULT_TOAST_DURATION)
},
}
+21 -26
View File
@@ -1,29 +1,24 @@
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'
/**
* 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
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
}
-2
View File
@@ -53,14 +53,12 @@ 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,
-1
View File
@@ -20,7 +20,6 @@ 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_4xl]}>
<View style={[a.flex_1, a.gap_xs, a.pr_2xl]}>
<Text style={[a.text_sm, a.leading_snug]}>{children}</Text>
<Text style={[a.text_sm, a.leading_snug, a.font_bold]}>
<Trans>
-1
View File
@@ -27,7 +27,6 @@ type ControlsContext = {
}
const ControlsContext = createContext<ControlsContext | null>(null)
ControlsContext.displayName = 'GlobalDialogControlsContext'
export function useGlobalDialogsControlContext() {
const ctx = useContext(ControlsContext)
@@ -211,9 +211,7 @@ export function Verify({config, showScreen}: ScreenProps<ScreenID.Verify>) {
<Trans>Verify your email</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>
<Trans>Verify email code</Trans>
)}
</Text>
@@ -347,13 +345,7 @@ export function Verify({config, showScreen}: ScreenProps<ScreenID.Verify>) {
{state.error && <Admonition type="error">{state.error}</Admonition>}
<Button
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`,
}),
)}
label={_(msg`Verify code`)}
size="large"
variant="solid"
color="primary"
@@ -364,11 +356,7 @@ export function Verify({config, showScreen}: ScreenProps<ScreenID.Verify>) {
state.mutationStatus === 'pending'
}>
<ButtonText>
<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>
<Trans>Verify code</Trans>
</ButtonText>
{state.mutationStatus === 'pending' && <ButtonIcon icon={Loader} />}
</Button>
+1 -6
View File
@@ -3,7 +3,6 @@ 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,
@@ -48,7 +47,6 @@ const Context = React.createContext<Context>({
activeNux: undefined,
dismissActiveNux: () => {},
})
Context.displayName = 'NuxDialogContext'
export function useNuxDialogContext() {
return React.useContext(Context)
@@ -57,10 +55,7 @@ export function useNuxDialogContext() {
export function NuxDialogs() {
const {currentAccount} = useSession()
const {data: preferences} = usePreferencesQuery()
const {data: profile} = useProfileQuery({
did: currentAccount?.did,
staleTime: STALE.INFINITY, // createdAt isn't gonna change
})
const {data: profile} = useProfileQuery({did: currentAccount?.did})
const onboardingActive = useOnboardingState().isActive
const isLoading =
-1
View File
@@ -1,7 +1,6 @@
import React from 'react'
const MessageContext = React.createContext(false)
MessageContext.displayName = 'MessageContext'
export function MessageContextProvider({
children,
+9 -4
View File
@@ -5,8 +5,9 @@ import {type ChatBskyConvoDefs, RichText} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useTranslate} from '#/lib/hooks/useTranslate'
import {useOpenLink} from '#/lib/hooks/useOpenLink'
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'
@@ -38,7 +39,7 @@ export let MessageContextMenu = ({
const deleteControl = usePromptControl()
const reportControl = usePromptControl()
const langPrefs = useLanguagePrefs()
const translate = useTranslate()
const openLink = useOpenLink()
const isFromSelf = message.sender?.did === currentAccount?.did
@@ -56,7 +57,11 @@ export let MessageContextMenu = ({
}, [_, message.text, message.facets])
const onPressTranslateMessage = useCallback(() => {
translate(message.text, langPrefs.primaryLanguage)
const translatorUrl = getTranslatorLink(
message.text,
langPrefs.primaryLanguage,
)
openLink(translatorUrl, true)
logger.metric(
'translate',
@@ -67,7 +72,7 @@ export let MessageContextMenu = ({
},
{statsig: false},
)
}, [langPrefs.primaryLanguage, message.text, translate])
}, [langPrefs.primaryLanguage, message.text, openLink])
const onDelete = useCallback(() => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
+1 -2
View File
@@ -73,8 +73,7 @@ export function MessageProfileButton({
a.align_center,
t.atoms.bg_contrast_25,
a.rounded_full,
// Matches size of button below to avoid layout shift
{width: 33, height: 33},
{width: 34, height: 34},
]}>
<Message style={[t.atoms.text, {opacity: 0.3}]} size="md" />
</View>
-1
View File
@@ -46,7 +46,6 @@ const Context = createContext<{
onFocus: () => {},
onBlur: () => {},
})
Context.displayName = 'TextFieldContext'
export type RootProps = React.PropsWithChildren<{isInvalid?: boolean}>
+3 -5
View File
@@ -1,5 +1,5 @@
import React from 'react'
import {Pressable, View, type ViewStyle} from 'react-native'
import {Pressable, View, 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,
type TextStyleProp,
TextStyleProp,
useTheme,
type ViewStyleProp,
ViewStyleProp,
} from '#/alf'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {CheckThick_Stroke2_Corner0_Rounded as Checkmark} from '#/components/icons/Check'
@@ -35,7 +35,6 @@ const ItemContext = React.createContext<ItemState>({
pressed: false,
focused: false,
})
ItemContext.displayName = 'ToggleItemContext'
const GroupContext = React.createContext<{
values: string[]
@@ -50,7 +49,6 @@ const GroupContext = React.createContext<{
maxSelectionsReached: false,
setFieldValue: () => {},
})
GroupContext.displayName = 'ToggleGroupContext'
export type GroupProps = React.PropsWithChildren<{
type?: 'radio' | 'checkbox'
@@ -1,7 +0,0 @@
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',
},
)
+1 -2
View File
@@ -1,7 +1,7 @@
import React from 'react'
import * as Dialog from '#/components/Dialog'
import {type DialogControlProps} from '#/components/Dialog'
import {DialogControlProps} from '#/components/Dialog'
import {VerifyEmailIntentDialog} from '#/components/intents/VerifyEmailIntentDialog'
interface Context {
@@ -11,7 +11,6 @@ interface Context {
}
const Context = React.createContext({} as Context)
Context.displayName = 'IntentDialogsContext'
export const useIntentDialogs = () => React.useContext(Context)
export function Provider({children}: {children: React.ReactNode}) {
+2 -3
View File
@@ -1,8 +1,8 @@
import React from 'react'
import {type ModerationUI} from '@atproto/api'
import {ModerationUI} from '@atproto/api'
import {
type ModerationCauseDescription,
ModerationCauseDescription,
useModerationCauseDescription,
} from '#/lib/moderation/useModerationCauseDescription'
import {
@@ -22,7 +22,6 @@ type Context = {
}
const Context = React.createContext<Context>({} as Context)
Context.displayName = 'HiderContext'
export const useHider = () => React.useContext(Context)
+2 -3
View File
@@ -1,5 +1,5 @@
import {createContext, useContext, useMemo} from 'react'
import {type ScrollHandlers} from 'react-native-reanimated'
import React, {createContext, useContext, useMemo} from 'react'
import {ScrollHandlers} from 'react-native-reanimated'
const ScrollContext = createContext<ScrollHandlers<any>>({
onBeginDrag: undefined,
@@ -7,7 +7,6 @@ const ScrollContext = createContext<ScrollHandlers<any>>({
onScroll: undefined,
onMomentumEnd: undefined,
})
ScrollContext.displayName = 'ScrollContext'
export function useScrollHandlers(): ScrollHandlers<any> {
return useContext(ScrollContext)
-1
View File
@@ -89,7 +89,6 @@ export interface ThemeProviderProps {
}
export const ThemeContext = createContext<Theme>(defaultTheme)
ThemeContext.displayName = 'ThemeContext'
export const useTheme = () => useContext(ThemeContext)
+36
View File
@@ -0,0 +1,36 @@
import {STALE} from '#/state/queries'
import {useServiceConfigQuery} from '#/state/queries/email-verification-required'
import {useProfileQuery} from '#/state/queries/profile'
import {useSession} from '#/state/session'
import {BSKY_SERVICE} from '../constants'
import {getHostnameFromUrl} from '../strings/url-helpers'
export function useEmail() {
const {currentAccount} = useSession()
const {data: serviceConfig} = useServiceConfigQuery()
const {data: profile} = useProfileQuery({
did: currentAccount?.did,
staleTime: STALE.INFINITY,
})
const checkEmailConfirmed = !!serviceConfig?.checkEmailConfirmed
// Date set for 11 AM PST on the 18th of November
const isNewEnough =
!!profile?.createdAt &&
Date.parse(profile.createdAt) >= Date.parse('2024-11-18T19:00:00.000Z')
const isSelfHost =
currentAccount &&
getHostnameFromUrl(currentAccount.service) !==
getHostnameFromUrl(BSKY_SERVICE)
const needsEmailVerification =
!isSelfHost &&
checkEmailConfirmed &&
!currentAccount?.emailConfirmed &&
isNewEnough
return {needsEmailVerification}
}
@@ -19,8 +19,6 @@ const KeyboardControllerRefCountContext = createContext<{
incrementRefCount: () => {},
decrementRefCount: () => {},
})
KeyboardControllerRefCountContext.displayName =
'KeyboardControllerRefCountContext'
export function KeyboardControllerProvider({
children,
-3
View File
@@ -4,11 +4,8 @@ import {useFocusEffect} from '@react-navigation/native'
type HideBottomBarBorderSetter = () => () => void
const HideBottomBarBorderContext = createContext<boolean>(false)
HideBottomBarBorderContext.displayName = 'HideBottomBarBorderContext'
const HideBottomBarBorderSetterContext =
createContext<HideBottomBarBorderSetter | null>(null)
HideBottomBarBorderSetterContext.displayName =
'HideBottomBarBorderSetterContext'
export function useHideBottomBarBorderSetter() {
const hideBottomBarBorder = useContext(HideBottomBarBorderSetterContext)
+2 -2
View File
@@ -2,10 +2,10 @@ import {useMemo} from 'react'
import {Trans} from '@lingui/macro'
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
import {useOpenComposer as useRootOpenComposer} from '#/state/shell/composer'
import {useOpenComposer as rootUseOpenComposer} from '#/state/shell/composer'
export function useOpenComposer() {
const {openComposer} = useRootOpenComposer()
const {openComposer} = rootUseOpenComposer()
const requireEmailVerification = useRequireEmailVerification()
return useMemo(() => {
return {
@@ -1,7 +1,7 @@
import {useCallback} from 'react'
import {Keyboard} from 'react-native'
import {useEmail} from '#/state/email-verification'
import {useEmail} from '#/lib/hooks/useEmail'
import {useRequireAuth, useSession} from '#/state/session'
import {useCloseAllActiveElements} from '#/state/util'
import {
-54
View File
@@ -1,54 +0,0 @@
import {useCallback} from 'react'
import * as IntentLauncher from 'expo-intent-launcher'
import {getTranslatorLink} from '#/locale/helpers'
import {isAndroid} from '#/platform/detection'
import {useOpenLink} from './useOpenLink'
export function useTranslate() {
const openLink = useOpenLink()
return useCallback(
async (text: string, language: string) => {
const translateUrl = getTranslatorLink(text, language)
if (isAndroid) {
try {
// use getApplicationIconAsync to determine if the translate app is installed
if (
!(await IntentLauncher.getApplicationIconAsync(
'com.google.android.apps.translate',
))
) {
throw new Error('Translate app not installed')
}
// TODO: this should only be called one at a time, use something like
// RQ's `scope` - otherwise can trigger the browser to open unexpectedly when the call throws -sfn
await IntentLauncher.startActivityAsync(
'android.intent.action.PROCESS_TEXT',
{
type: 'text/plain',
extra: {
'android.intent.extra.PROCESS_TEXT': text,
'android.intent.extra.PROCESS_TEXT_READONLY': true,
},
// note: to skip the intermediate app select, we need to specify a
// `className`. however, this isn't safe to hardcode, we'd need to query the
// package manager for the correct activity. this requires native code, so
// skip for now -sfn
// packageName: 'com.google.android.apps.translate',
// className: 'com.google.android.apps.translate.TranslateActivity',
},
)
} catch (err) {
if (__DEV__) console.error(err)
// most likely means they don't have the translate app
await openLink(translateUrl)
}
} else {
await openLink(translateUrl)
}
},
[openLink],
)
}
-1
View File
@@ -147,7 +147,6 @@ function toStringRecord<E extends keyof MetricEvents>(
// and it's been difficult to get it to behave in a predictable way.
// Our own cache ensures consistent evaluation within a single session.
const GateCache = React.createContext<Map<string, boolean> | null>(null)
GateCache.displayName = 'StatsigGateCacheContext'
type GateOptions = {
dangerouslyDisableExposureLogging?: boolean
+2 -9
View File
@@ -25,17 +25,10 @@ export function isInvalidHandle(handle: string): boolean {
return handle === 'handle.invalid'
}
export function sanitizeHandle(
handle: string,
prefix = '',
forceLeftToRight = true,
): string {
const lowercasedWithPrefix = `${prefix}${handle.toLocaleLowerCase()}`
export function sanitizeHandle(handle: string, prefix = ''): string {
return isInvalidHandle(handle)
? '⚠Invalid Handle'
: forceLeftToRight
? forceLTR(lowercasedWithPrefix)
: lowercasedWithPrefix
: forceLTR(`${prefix}${handle.toLocaleLowerCase()}`)
}
export interface IsValidHandle {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More