Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 104bcf2055 | |||
| e7506277f9 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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 |
@@ -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 -->
|
||||
|
||||
@@ -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,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
|
||||
|
||||
+1
-4
@@ -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)
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ 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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+5
-6
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+25
-27
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -53,7 +53,6 @@ export const borderRadius = {
|
||||
*/
|
||||
export const fontWeight = {
|
||||
normal: '400',
|
||||
medium: '500',
|
||||
bold: '600',
|
||||
heavy: '800',
|
||||
} as const
|
||||
|
||||
@@ -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'),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ type Context = {
|
||||
const Context = createContext<Context>({
|
||||
type: 'info',
|
||||
})
|
||||
Context.displayName = 'AdmonitionContext'
|
||||
|
||||
export function Icon() {
|
||||
const t = useTheme()
|
||||
|
||||
+16
-17
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -23,7 +23,6 @@ export const Context = createContext<DialogContextProps>({
|
||||
setDisableDrag: () => {},
|
||||
isWithinDialog: false,
|
||||
})
|
||||
Context.displayName = 'DialogContext'
|
||||
|
||||
export function useDialogContext() {
|
||||
return useContext(Context)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -77,7 +77,6 @@ export function Outer({
|
||||
}
|
||||
|
||||
const AlignmentContext = createContext<'platform' | 'left'>('platform')
|
||||
AlignmentContext.displayName = 'AlignmentContext'
|
||||
|
||||
export function Content({
|
||||
children,
|
||||
|
||||
@@ -3,4 +3,3 @@ import React from 'react'
|
||||
export const ScrollbarOffsetContext = React.createContext({
|
||||
isWithinOffsetView: false,
|
||||
})
|
||||
ScrollbarOffsetContext.displayName = 'ScrollbarOffsetContext'
|
||||
|
||||
@@ -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`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 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>
|
||||
We’re 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]}>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -27,7 +27,6 @@ const Context = React.createContext<{
|
||||
titleId: '',
|
||||
descriptionId: '',
|
||||
})
|
||||
Context.displayName = 'PromptContext'
|
||||
|
||||
export function Outer({
|
||||
children,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 @@
|
||||
export const DURATION = 3e3
|
||||
export const DEFAULT_TOAST_DURATION = 3000
|
||||
|
||||
@@ -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
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,7 +1,6 @@
|
||||
import React from 'react'
|
||||
|
||||
const MessageContext = React.createContext(false)
|
||||
MessageContext.displayName = 'MessageContext'
|
||||
|
||||
export function MessageContextProvider({
|
||||
children,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -46,7 +46,6 @@ const Context = createContext<{
|
||||
onFocus: () => {},
|
||||
onBlur: () => {},
|
||||
})
|
||||
Context.displayName = 'TextFieldContext'
|
||||
|
||||
export type RootProps = React.PropsWithChildren<{isInvalid?: boolean}>
|
||||
|
||||
|
||||
@@ -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,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}) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -89,7 +89,6 @@ export interface ThemeProviderProps {
|
||||
}
|
||||
|
||||
export const ThemeContext = createContext<Theme>(defaultTheme)
|
||||
ThemeContext.displayName = 'ThemeContext'
|
||||
|
||||
export const useTheme = () => useContext(ThemeContext)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -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],
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+195
-199
File diff suppressed because it is too large
Load Diff
+299
-305
File diff suppressed because it is too large
Load Diff
+195
-199
File diff suppressed because it is too large
Load Diff
+299
-305
File diff suppressed because it is too large
Load Diff
+299
-305
File diff suppressed because it is too large
Load Diff
+208
-212
File diff suppressed because it is too large
Load Diff
+196
-200
File diff suppressed because it is too large
Load Diff
+196
-200
File diff suppressed because it is too large
Load Diff
+512
-516
File diff suppressed because it is too large
Load Diff
+195
-199
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+348
-337
File diff suppressed because it is too large
Load Diff
+198
-202
File diff suppressed because it is too large
Load Diff
+196
-200
File diff suppressed because it is too large
Load Diff
+208
-212
File diff suppressed because it is too large
Load Diff
+195
-199
File diff suppressed because it is too large
Load Diff
+219
-223
File diff suppressed because it is too large
Load Diff
+208
-212
File diff suppressed because it is too large
Load Diff
+195
-199
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
Reference in New Issue
Block a user