[Sheets] [Pt. 1] Root PR (#5557)
Co-authored-by: Samuel Newman <mozzius@protonmail.com> Co-authored-by: Eric Bailey <git@esb.lol> Co-authored-by: dan <dan.abramov@gmail.com> Co-authored-by: Hailey <me@haileyok.com>
This commit is contained in:
@@ -96,6 +96,11 @@ jest.mock('expo-modules-core', () => ({
|
|||||||
getIsReducedMotionEnabled: () => false,
|
getIsReducedMotionEnabled: () => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (moduleName === 'BottomSheet') {
|
||||||
|
return {
|
||||||
|
dismissAll: () => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
requireNativeViewManager: jest.fn().mockImplementation(moduleName => {
|
requireNativeViewManager: jest.fn().mockImplementation(moduleName => {
|
||||||
return () => null
|
return () => null
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
apply plugin: 'com.android.library'
|
||||||
|
|
||||||
|
group = 'expo.modules.bottomsheet'
|
||||||
|
version = '0.1.0'
|
||||||
|
|
||||||
|
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
||||||
|
apply from: expoModulesCorePlugin
|
||||||
|
applyKotlinExpoModulesCorePlugin()
|
||||||
|
useCoreDependencies()
|
||||||
|
useExpoPublishing()
|
||||||
|
|
||||||
|
// If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
|
||||||
|
// The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
|
||||||
|
// Most of the time, you may like to manage the Android SDK versions yourself.
|
||||||
|
def useManagedAndroidSdkVersions = false
|
||||||
|
if (useManagedAndroidSdkVersions) {
|
||||||
|
useDefaultAndroidSdkVersions()
|
||||||
|
} else {
|
||||||
|
buildscript {
|
||||||
|
// Simple helper that allows the root project to override versions declared by this library.
|
||||||
|
ext.safeExtGet = { prop, fallback ->
|
||||||
|
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
project.android {
|
||||||
|
compileSdkVersion safeExtGet("compileSdkVersion", 34)
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion safeExtGet("minSdkVersion", 21)
|
||||||
|
targetSdkVersion safeExtGet("targetSdkVersion", 34)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace "expo.modules.bottomsheet"
|
||||||
|
defaultConfig {
|
||||||
|
versionCode 1
|
||||||
|
versionName "0.1.0"
|
||||||
|
}
|
||||||
|
lintOptions {
|
||||||
|
abortOnError false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation project(':expo-modules-core')
|
||||||
|
implementation 'com.google.android.material:material:1.12.0'
|
||||||
|
implementation "com.facebook.react:react-native:+"
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<manifest>
|
||||||
|
</manifest>
|
||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
package expo.modules.bottomsheet
|
||||||
|
|
||||||
|
import expo.modules.kotlin.modules.Module
|
||||||
|
import expo.modules.kotlin.modules.ModuleDefinition
|
||||||
|
|
||||||
|
class BottomSheetModule : Module() {
|
||||||
|
override fun definition() =
|
||||||
|
ModuleDefinition {
|
||||||
|
Name("BottomSheet")
|
||||||
|
|
||||||
|
AsyncFunction("dismissAll") {
|
||||||
|
SheetManager.dismissAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
View(BottomSheetView::class) {
|
||||||
|
Events(
|
||||||
|
arrayOf(
|
||||||
|
"onAttemptDismiss",
|
||||||
|
"onSnapPointChange",
|
||||||
|
"onStateChange",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
AsyncFunction("dismiss") { view: BottomSheetView ->
|
||||||
|
view.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncFunction("updateLayout") { view: BottomSheetView ->
|
||||||
|
view.updateLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
Prop("disableDrag") { view: BottomSheetView, prop: Boolean ->
|
||||||
|
view.disableDrag = prop
|
||||||
|
}
|
||||||
|
|
||||||
|
Prop("minHeight") { view: BottomSheetView, prop: Float ->
|
||||||
|
view.minHeight = prop
|
||||||
|
}
|
||||||
|
|
||||||
|
Prop("maxHeight") { view: BottomSheetView, prop: Float ->
|
||||||
|
view.maxHeight = prop
|
||||||
|
}
|
||||||
|
|
||||||
|
Prop("preventDismiss") { view: BottomSheetView, prop: Boolean ->
|
||||||
|
view.preventDismiss = prop
|
||||||
|
}
|
||||||
|
|
||||||
|
Prop("preventExpansion") { view: BottomSheetView, prop: Boolean ->
|
||||||
|
view.preventExpansion = prop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+339
@@ -0,0 +1,339 @@
|
|||||||
|
package expo.modules.bottomsheet
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.ViewStructure
|
||||||
|
import android.view.accessibility.AccessibilityEvent
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import androidx.core.view.allViews
|
||||||
|
import com.facebook.react.bridge.LifecycleEventListener
|
||||||
|
import com.facebook.react.bridge.ReactContext
|
||||||
|
import com.facebook.react.bridge.UiThreadUtil
|
||||||
|
import com.facebook.react.uimanager.UIManagerHelper
|
||||||
|
import com.facebook.react.uimanager.events.EventDispatcher
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||||
|
import expo.modules.kotlin.AppContext
|
||||||
|
import expo.modules.kotlin.viewevent.EventDispatcher
|
||||||
|
import expo.modules.kotlin.views.ExpoView
|
||||||
|
import java.util.ArrayList
|
||||||
|
|
||||||
|
class BottomSheetView(
|
||||||
|
context: Context,
|
||||||
|
appContext: AppContext,
|
||||||
|
) : ExpoView(context, appContext),
|
||||||
|
LifecycleEventListener {
|
||||||
|
private var innerView: View? = null
|
||||||
|
private var dialog: BottomSheetDialog? = null
|
||||||
|
|
||||||
|
private lateinit var dialogRootViewGroup: DialogRootViewGroup
|
||||||
|
private var eventDispatcher: EventDispatcher? = null
|
||||||
|
|
||||||
|
private val screenHeight =
|
||||||
|
context.resources.displayMetrics.heightPixels
|
||||||
|
.toFloat()
|
||||||
|
|
||||||
|
private val onAttemptDismiss by EventDispatcher()
|
||||||
|
private val onSnapPointChange by EventDispatcher()
|
||||||
|
private val onStateChange by EventDispatcher()
|
||||||
|
|
||||||
|
// Props
|
||||||
|
var disableDrag = false
|
||||||
|
set (value) {
|
||||||
|
field = value
|
||||||
|
this.setDraggable(!value)
|
||||||
|
}
|
||||||
|
|
||||||
|
var preventDismiss = false
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
this.dialog?.setCancelable(!value)
|
||||||
|
}
|
||||||
|
var preventExpansion = false
|
||||||
|
|
||||||
|
var minHeight = 0f
|
||||||
|
set(value) {
|
||||||
|
field =
|
||||||
|
if (value < 0) {
|
||||||
|
0f
|
||||||
|
} else {
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var maxHeight = this.screenHeight
|
||||||
|
set(value) {
|
||||||
|
field =
|
||||||
|
if (value > this.screenHeight) {
|
||||||
|
this.screenHeight.toFloat()
|
||||||
|
} else {
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isOpen: Boolean = false
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
onStateChange(
|
||||||
|
mapOf(
|
||||||
|
"state" to if (value) "open" else "closed",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isOpening: Boolean = false
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
if (value) {
|
||||||
|
onStateChange(
|
||||||
|
mapOf(
|
||||||
|
"state" to "opening",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isClosing: Boolean = false
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
if (value) {
|
||||||
|
onStateChange(
|
||||||
|
mapOf(
|
||||||
|
"state" to "closing",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var selectedSnapPoint = 0
|
||||||
|
set(value) {
|
||||||
|
if (field == value) return
|
||||||
|
|
||||||
|
field = value
|
||||||
|
onSnapPointChange(
|
||||||
|
mapOf(
|
||||||
|
"snapPoint" to value,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
init {
|
||||||
|
(appContext.reactContext as? ReactContext)?.let {
|
||||||
|
it.addLifecycleEventListener(this)
|
||||||
|
this.eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(it, this.id)
|
||||||
|
|
||||||
|
this.dialogRootViewGroup = DialogRootViewGroup(context)
|
||||||
|
this.dialogRootViewGroup.eventDispatcher = this.eventDispatcher
|
||||||
|
}
|
||||||
|
SheetManager.add(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLayout(
|
||||||
|
changed: Boolean,
|
||||||
|
l: Int,
|
||||||
|
t: Int,
|
||||||
|
r: Int,
|
||||||
|
b: Int,
|
||||||
|
) {
|
||||||
|
this.present()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun destroy() {
|
||||||
|
this.isClosing = false
|
||||||
|
this.isOpen = false
|
||||||
|
this.dialog = null
|
||||||
|
this.innerView = null
|
||||||
|
SheetManager.remove(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Presentation
|
||||||
|
|
||||||
|
private fun present() {
|
||||||
|
if (this.isOpen || this.isOpening || this.isClosing) return
|
||||||
|
|
||||||
|
val contentHeight = this.getContentHeight()
|
||||||
|
|
||||||
|
val dialog = BottomSheetDialog(context)
|
||||||
|
dialog.setContentView(dialogRootViewGroup)
|
||||||
|
dialog.setCancelable(!preventDismiss)
|
||||||
|
dialog.setOnShowListener {
|
||||||
|
val bottomSheet = dialog.findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet)
|
||||||
|
bottomSheet?.let {
|
||||||
|
// Let the outside view handle the background color on its own, the default for this is
|
||||||
|
// white and we don't want that.
|
||||||
|
it.setBackgroundColor(0)
|
||||||
|
|
||||||
|
val behavior = BottomSheetBehavior.from(it)
|
||||||
|
|
||||||
|
behavior.isFitToContents = true
|
||||||
|
behavior.halfExpandedRatio = this.clampRatio(this.getTargetHeight() / this.screenHeight)
|
||||||
|
if (contentHeight > this.screenHeight) {
|
||||||
|
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||||
|
this.selectedSnapPoint = 2
|
||||||
|
} else {
|
||||||
|
behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
|
||||||
|
this.selectedSnapPoint = 1
|
||||||
|
}
|
||||||
|
behavior.skipCollapsed = true
|
||||||
|
behavior.isDraggable = true
|
||||||
|
behavior.isHideable = true
|
||||||
|
|
||||||
|
behavior.addBottomSheetCallback(
|
||||||
|
object : BottomSheetBehavior.BottomSheetCallback() {
|
||||||
|
override fun onStateChanged(
|
||||||
|
bottomSheet: View,
|
||||||
|
newState: Int,
|
||||||
|
) {
|
||||||
|
when (newState) {
|
||||||
|
BottomSheetBehavior.STATE_EXPANDED -> {
|
||||||
|
selectedSnapPoint = 2
|
||||||
|
}
|
||||||
|
BottomSheetBehavior.STATE_COLLAPSED -> {
|
||||||
|
selectedSnapPoint = 1
|
||||||
|
}
|
||||||
|
BottomSheetBehavior.STATE_HALF_EXPANDED -> {
|
||||||
|
selectedSnapPoint = 1
|
||||||
|
}
|
||||||
|
BottomSheetBehavior.STATE_HIDDEN -> {
|
||||||
|
selectedSnapPoint = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSlide(
|
||||||
|
bottomSheet: View,
|
||||||
|
slideOffset: Float,
|
||||||
|
) { }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dialog.setOnDismissListener {
|
||||||
|
this.isClosing = true
|
||||||
|
this.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isOpening = true
|
||||||
|
dialog.show()
|
||||||
|
this.dialog = dialog
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateLayout() {
|
||||||
|
val dialog = this.dialog ?: return
|
||||||
|
val contentHeight = this.getContentHeight()
|
||||||
|
|
||||||
|
val bottomSheet = dialog.findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet)
|
||||||
|
bottomSheet?.let {
|
||||||
|
val behavior = BottomSheetBehavior.from(it)
|
||||||
|
|
||||||
|
behavior.halfExpandedRatio = this.clampRatio(this.getTargetHeight() / this.screenHeight)
|
||||||
|
|
||||||
|
if (contentHeight > this.screenHeight && behavior.state != BottomSheetBehavior.STATE_EXPANDED) {
|
||||||
|
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||||
|
} else if (contentHeight < this.screenHeight && behavior.state != BottomSheetBehavior.STATE_HALF_EXPANDED) {
|
||||||
|
behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismiss() {
|
||||||
|
this.dialog?.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Util
|
||||||
|
|
||||||
|
private fun getContentHeight(): Float {
|
||||||
|
val innerView = this.innerView ?: return 0f
|
||||||
|
var index = 0
|
||||||
|
innerView.allViews.forEach {
|
||||||
|
if (index == 1) {
|
||||||
|
return it.height.toFloat()
|
||||||
|
}
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
return 0f
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getTargetHeight(): Float {
|
||||||
|
val contentHeight = this.getContentHeight()
|
||||||
|
val height =
|
||||||
|
if (contentHeight > maxHeight) {
|
||||||
|
maxHeight
|
||||||
|
} else if (contentHeight < minHeight) {
|
||||||
|
minHeight
|
||||||
|
} else {
|
||||||
|
contentHeight
|
||||||
|
}
|
||||||
|
return height
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clampRatio(ratio: Float): Float {
|
||||||
|
if (ratio < 0.01) {
|
||||||
|
return 0.01f
|
||||||
|
} else if (ratio > 0.99) {
|
||||||
|
return 0.99f
|
||||||
|
}
|
||||||
|
return ratio
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setDraggable(draggable: Boolean) {
|
||||||
|
val dialog = this.dialog ?: return
|
||||||
|
val bottomSheet = dialog.findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet)
|
||||||
|
bottomSheet?.let {
|
||||||
|
val behavior = BottomSheetBehavior.from(it)
|
||||||
|
behavior.isDraggable = draggable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onHostResume() { }
|
||||||
|
|
||||||
|
override fun onHostPause() { }
|
||||||
|
|
||||||
|
override fun onHostDestroy() {
|
||||||
|
(appContext.reactContext as? ReactContext)?.let {
|
||||||
|
it.removeLifecycleEventListener(this)
|
||||||
|
this.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// View overrides to pass to DialogRootViewGroup instead
|
||||||
|
|
||||||
|
override fun dispatchProvideStructure(structure: ViewStructure?) {
|
||||||
|
dialogRootViewGroup.dispatchProvideStructure(structure)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setId(id: Int) {
|
||||||
|
super.setId(id)
|
||||||
|
dialogRootViewGroup.id = id
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addView(
|
||||||
|
child: View?,
|
||||||
|
index: Int,
|
||||||
|
) {
|
||||||
|
this.innerView = child
|
||||||
|
(child as ViewGroup).let {
|
||||||
|
dialogRootViewGroup.addView(child, index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeView(view: View?) {
|
||||||
|
UiThreadUtil.assertOnUiThread()
|
||||||
|
if (view != null) {
|
||||||
|
dialogRootViewGroup.removeView(view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeViewAt(index: Int) {
|
||||||
|
UiThreadUtil.assertOnUiThread()
|
||||||
|
val child = getChildAt(index)
|
||||||
|
dialogRootViewGroup.removeView(child)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addChildrenForAccessibility(outChildren: ArrayList<View>?) { }
|
||||||
|
|
||||||
|
override fun dispatchPopulateAccessibilityEvent(event: AccessibilityEvent?): Boolean = false
|
||||||
|
}
|
||||||
+171
@@ -0,0 +1,171 @@
|
|||||||
|
package expo.modules.bottomsheet
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import com.facebook.react.bridge.GuardedRunnable
|
||||||
|
import com.facebook.react.config.ReactFeatureFlags
|
||||||
|
import com.facebook.react.uimanager.JSPointerDispatcher
|
||||||
|
import com.facebook.react.uimanager.JSTouchDispatcher
|
||||||
|
import com.facebook.react.uimanager.RootView
|
||||||
|
import com.facebook.react.uimanager.ThemedReactContext
|
||||||
|
import com.facebook.react.uimanager.UIManagerModule
|
||||||
|
import com.facebook.react.uimanager.events.EventDispatcher
|
||||||
|
import com.facebook.react.views.view.ReactViewGroup
|
||||||
|
|
||||||
|
// SEE https://github.com/facebook/react-native/blob/309cdea337101cfe2212cfb6abebf1e783e43282/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.kt#L378
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DialogRootViewGroup is the ViewGroup which contains all the children of a Modal. It gets all
|
||||||
|
* child information forwarded from [ReactModalHostView] and uses that to create children. It is
|
||||||
|
* also responsible for acting as a RootView and handling touch events. It does this the same way
|
||||||
|
* as ReactRootView.
|
||||||
|
*
|
||||||
|
* To get layout to work properly, we need to layout all the elements within the Modal as if they
|
||||||
|
* can fill the entire window. To do that, we need to explicitly set the styleWidth and
|
||||||
|
* styleHeight on the LayoutShadowNode to be the window size. This is done through the
|
||||||
|
* UIManagerModule, and will then cause the children to layout as if they can fill the window.
|
||||||
|
*/
|
||||||
|
class DialogRootViewGroup(
|
||||||
|
private val context: Context?,
|
||||||
|
) : ReactViewGroup(context),
|
||||||
|
RootView {
|
||||||
|
private var hasAdjustedSize = false
|
||||||
|
private var viewWidth = 0
|
||||||
|
private var viewHeight = 0
|
||||||
|
|
||||||
|
private val jSTouchDispatcher = JSTouchDispatcher(this)
|
||||||
|
private var jSPointerDispatcher: JSPointerDispatcher? = null
|
||||||
|
private var sizeChangeListener: OnSizeChangeListener? = null
|
||||||
|
|
||||||
|
var eventDispatcher: EventDispatcher? = null
|
||||||
|
|
||||||
|
interface OnSizeChangeListener {
|
||||||
|
fun onSizeChange(
|
||||||
|
width: Int,
|
||||||
|
height: Int,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (ReactFeatureFlags.dispatchPointerEvents) {
|
||||||
|
jSPointerDispatcher = JSPointerDispatcher(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSizeChanged(
|
||||||
|
w: Int,
|
||||||
|
h: Int,
|
||||||
|
oldw: Int,
|
||||||
|
oldh: Int,
|
||||||
|
) {
|
||||||
|
super.onSizeChanged(w, h, oldw, oldh)
|
||||||
|
|
||||||
|
viewWidth = w
|
||||||
|
viewHeight = h
|
||||||
|
updateFirstChildView()
|
||||||
|
|
||||||
|
sizeChangeListener?.onSizeChange(w, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setOnSizeChangeListener(listener: OnSizeChangeListener) {
|
||||||
|
sizeChangeListener = listener
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateFirstChildView() {
|
||||||
|
if (childCount > 0) {
|
||||||
|
hasAdjustedSize = false
|
||||||
|
val viewTag = getChildAt(0).id
|
||||||
|
reactContext.runOnNativeModulesQueueThread(
|
||||||
|
object : GuardedRunnable(reactContext) {
|
||||||
|
override fun runGuarded() {
|
||||||
|
val uiManager: UIManagerModule =
|
||||||
|
reactContext
|
||||||
|
.reactApplicationContext
|
||||||
|
.getNativeModule(UIManagerModule::class.java) ?: return
|
||||||
|
|
||||||
|
uiManager.updateNodeSize(viewTag, viewWidth, viewHeight)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
hasAdjustedSize = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addView(
|
||||||
|
child: View,
|
||||||
|
index: Int,
|
||||||
|
params: LayoutParams,
|
||||||
|
) {
|
||||||
|
super.addView(child, index, params)
|
||||||
|
if (hasAdjustedSize) {
|
||||||
|
updateFirstChildView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleException(t: Throwable) {
|
||||||
|
reactContext.reactApplicationContext.handleException(RuntimeException(t))
|
||||||
|
}
|
||||||
|
|
||||||
|
private val reactContext: ThemedReactContext
|
||||||
|
get() = context as ThemedReactContext
|
||||||
|
|
||||||
|
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
|
||||||
|
eventDispatcher?.let { jSTouchDispatcher.handleTouchEvent(event, it) }
|
||||||
|
jSPointerDispatcher?.handleMotionEvent(event, eventDispatcher, true)
|
||||||
|
return super.onInterceptTouchEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||||
|
eventDispatcher?.let { jSTouchDispatcher.handleTouchEvent(event, it) }
|
||||||
|
jSPointerDispatcher?.handleMotionEvent(event, eventDispatcher, false)
|
||||||
|
super.onTouchEvent(event)
|
||||||
|
|
||||||
|
// In case when there is no children interested in handling touch event, we return true from
|
||||||
|
// the root view in order to receive subsequent events related to that gesture
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onInterceptHoverEvent(event: MotionEvent): Boolean {
|
||||||
|
jSPointerDispatcher?.handleMotionEvent(event, eventDispatcher, true)
|
||||||
|
return super.onHoverEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onHoverEvent(event: MotionEvent): Boolean {
|
||||||
|
jSPointerDispatcher?.handleMotionEvent(event, eventDispatcher, false)
|
||||||
|
return super.onHoverEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
|
override fun onChildStartedNativeGesture(ev: MotionEvent?) {
|
||||||
|
eventDispatcher?.let {
|
||||||
|
if (ev != null) {
|
||||||
|
jSTouchDispatcher.onChildStartedNativeGesture(ev, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onChildStartedNativeGesture(
|
||||||
|
childView: View,
|
||||||
|
ev: MotionEvent,
|
||||||
|
) {
|
||||||
|
eventDispatcher?.let { jSTouchDispatcher.onChildStartedNativeGesture(ev, it) }
|
||||||
|
jSPointerDispatcher?.onChildStartedNativeGesture(childView, ev, eventDispatcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onChildEndedNativeGesture(
|
||||||
|
childView: View,
|
||||||
|
ev: MotionEvent,
|
||||||
|
) {
|
||||||
|
eventDispatcher?.let { jSTouchDispatcher.onChildEndedNativeGesture(ev, it) }
|
||||||
|
jSPointerDispatcher?.onChildEndedNativeGesture()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
|
||||||
|
// No-op - override in order to still receive events to onInterceptTouchEvent
|
||||||
|
// even when some other view disallow that
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package expo.modules.bottomsheet
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
|
class SheetManager {
|
||||||
|
companion object {
|
||||||
|
private val sheets = mutableSetOf<WeakReference<BottomSheetView>>()
|
||||||
|
|
||||||
|
fun add(view: BottomSheetView) {
|
||||||
|
sheets.add(WeakReference(view))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun remove(view: BottomSheetView) {
|
||||||
|
sheets.forEach {
|
||||||
|
if (it.get() == view) {
|
||||||
|
sheets.remove(it)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismissAll() {
|
||||||
|
sheets.forEach {
|
||||||
|
it.get()?.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"platforms": ["ios", "android"],
|
||||||
|
"ios": {
|
||||||
|
"modules": ["BottomSheetModule"]
|
||||||
|
},
|
||||||
|
"android": {
|
||||||
|
"modules": ["expo.modules.bottomsheet.BottomSheetModule"]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import {BottomSheet} from './src/BottomSheet'
|
||||||
|
import {
|
||||||
|
BottomSheetSnapPoint,
|
||||||
|
BottomSheetState,
|
||||||
|
BottomSheetViewProps,
|
||||||
|
} from './src/BottomSheet.types'
|
||||||
|
|
||||||
|
export {
|
||||||
|
BottomSheet,
|
||||||
|
BottomSheetSnapPoint,
|
||||||
|
type BottomSheetState,
|
||||||
|
type BottomSheetViewProps,
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
Pod::Spec.new do |s|
|
||||||
|
s.name = 'BottomSheet'
|
||||||
|
s.version = '1.0.0'
|
||||||
|
s.summary = 'A bottom sheet for use in Bluesky'
|
||||||
|
s.description = 'A bottom sheet for use in Bluesky'
|
||||||
|
s.author = ''
|
||||||
|
s.homepage = 'https://github.com/bluesky-social/social-app'
|
||||||
|
s.platforms = { :ios => '15.0', :tvos => '15.0' }
|
||||||
|
s.source = { git: '' }
|
||||||
|
s.static_framework = true
|
||||||
|
|
||||||
|
s.dependency 'ExpoModulesCore'
|
||||||
|
|
||||||
|
# Swift/Objective-C compatibility
|
||||||
|
s.pod_target_xcconfig = {
|
||||||
|
'DEFINES_MODULE' => 'YES',
|
||||||
|
'SWIFT_COMPILATION_MODE' => 'wholemodule'
|
||||||
|
}
|
||||||
|
|
||||||
|
s.source_files = "**/*.{h,m,swift}"
|
||||||
|
end
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import ExpoModulesCore
|
||||||
|
|
||||||
|
public class BottomSheetModule: Module {
|
||||||
|
public func definition() -> ModuleDefinition {
|
||||||
|
Name("BottomSheet")
|
||||||
|
|
||||||
|
AsyncFunction("dismissAll") {
|
||||||
|
SheetManager.shared.dismissAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
View(SheetView.self) {
|
||||||
|
Events([
|
||||||
|
"onAttemptDismiss",
|
||||||
|
"onSnapPointChange",
|
||||||
|
"onStateChange"
|
||||||
|
])
|
||||||
|
|
||||||
|
AsyncFunction("dismiss") { (view: SheetView) in
|
||||||
|
view.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncFunction("updateLayout") { (view: SheetView) in
|
||||||
|
view.updateLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
Prop("cornerRadius") { (view: SheetView, prop: Float) in
|
||||||
|
view.cornerRadius = CGFloat(prop)
|
||||||
|
}
|
||||||
|
|
||||||
|
Prop("minHeight") { (view: SheetView, prop: Double) in
|
||||||
|
view.minHeight = prop
|
||||||
|
}
|
||||||
|
|
||||||
|
Prop("maxHeight") { (view: SheetView, prop: Double) in
|
||||||
|
view.maxHeight = prop
|
||||||
|
}
|
||||||
|
|
||||||
|
Prop("preventDismiss") { (view: SheetView, prop: Bool) in
|
||||||
|
view.preventDismiss = prop
|
||||||
|
}
|
||||||
|
|
||||||
|
Prop("preventExpansion") { (view: SheetView, prop: Bool) in
|
||||||
|
view.preventExpansion = prop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
//
|
||||||
|
// SheetManager.swift
|
||||||
|
// Pods
|
||||||
|
//
|
||||||
|
// Created by Hailey on 10/1/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import ExpoModulesCore
|
||||||
|
|
||||||
|
class SheetManager {
|
||||||
|
static let shared = SheetManager()
|
||||||
|
|
||||||
|
private var sheetViews = NSHashTable<SheetView>(options: .weakMemory)
|
||||||
|
|
||||||
|
func add(_ view: SheetView) {
|
||||||
|
sheetViews.add(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
func remove(_ view: SheetView) {
|
||||||
|
sheetViews.remove(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
func dismissAll() {
|
||||||
|
sheetViews.allObjects.forEach { sheetView in
|
||||||
|
sheetView.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
import ExpoModulesCore
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SheetView: ExpoView, UISheetPresentationControllerDelegate {
|
||||||
|
// Views
|
||||||
|
private var sheetVc: SheetViewController?
|
||||||
|
private var innerView: UIView?
|
||||||
|
private var touchHandler: RCTTouchHandler?
|
||||||
|
|
||||||
|
// Events
|
||||||
|
private let onAttemptDismiss = EventDispatcher()
|
||||||
|
private let onSnapPointChange = EventDispatcher()
|
||||||
|
private let onStateChange = EventDispatcher()
|
||||||
|
|
||||||
|
// Open event firing
|
||||||
|
private var isOpen: Bool = false {
|
||||||
|
didSet {
|
||||||
|
onStateChange([
|
||||||
|
"state": isOpen ? "open" : "closed"
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// React view props
|
||||||
|
var preventDismiss = false
|
||||||
|
var preventExpansion = false
|
||||||
|
var cornerRadius: CGFloat?
|
||||||
|
var minHeight = 0.0
|
||||||
|
var maxHeight: CGFloat! {
|
||||||
|
didSet {
|
||||||
|
let screenHeight = Util.getScreenHeight() ?? 0
|
||||||
|
if maxHeight > screenHeight {
|
||||||
|
maxHeight = screenHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isOpening = false {
|
||||||
|
didSet {
|
||||||
|
if isOpening {
|
||||||
|
onStateChange([
|
||||||
|
"state": "opening"
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private var isClosing = false {
|
||||||
|
didSet {
|
||||||
|
if isClosing {
|
||||||
|
onStateChange([
|
||||||
|
"state": "closing"
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private var selectedDetentIdentifier: UISheetPresentationController.Detent.Identifier? {
|
||||||
|
didSet {
|
||||||
|
if selectedDetentIdentifier == .large {
|
||||||
|
onSnapPointChange([
|
||||||
|
"snapPoint": 2
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
onSnapPointChange([
|
||||||
|
"snapPoint": 1
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Lifecycle
|
||||||
|
|
||||||
|
required init (appContext: AppContext? = nil) {
|
||||||
|
super.init(appContext: appContext)
|
||||||
|
self.maxHeight = Util.getScreenHeight()
|
||||||
|
self.touchHandler = RCTTouchHandler(bridge: appContext?.reactBridge)
|
||||||
|
SheetManager.shared.add(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't want this view to actually get added to the tree, so we'll simply store it for adding
|
||||||
|
// to the SheetViewController
|
||||||
|
override func insertReactSubview(_ subview: UIView!, at atIndex: Int) {
|
||||||
|
self.touchHandler?.attach(to: subview)
|
||||||
|
self.innerView = subview
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll grab the content height from here so we know the initial detent to set
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
guard let innerView = self.innerView else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if innerView.subviews.count != 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.present()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func destroy() {
|
||||||
|
self.isClosing = false
|
||||||
|
self.isOpen = false
|
||||||
|
self.sheetVc = nil
|
||||||
|
self.touchHandler?.detach(from: self.innerView)
|
||||||
|
self.touchHandler = nil
|
||||||
|
self.innerView = nil
|
||||||
|
SheetManager.shared.remove(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Presentation
|
||||||
|
|
||||||
|
func present() {
|
||||||
|
guard !self.isOpen,
|
||||||
|
!self.isOpening,
|
||||||
|
!self.isClosing,
|
||||||
|
let innerView = self.innerView,
|
||||||
|
let contentHeight = innerView.subviews.first?.frame.height,
|
||||||
|
let rvc = self.reactViewController() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let sheetVc = SheetViewController()
|
||||||
|
sheetVc.setDetents(contentHeight: self.clampHeight(contentHeight), preventExpansion: self.preventExpansion)
|
||||||
|
if let sheet = sheetVc.sheetPresentationController {
|
||||||
|
sheet.delegate = self
|
||||||
|
sheet.preferredCornerRadius = self.cornerRadius
|
||||||
|
self.selectedDetentIdentifier = sheet.selectedDetentIdentifier
|
||||||
|
}
|
||||||
|
sheetVc.view.addSubview(innerView)
|
||||||
|
|
||||||
|
self.sheetVc = sheetVc
|
||||||
|
self.isOpening = true
|
||||||
|
|
||||||
|
rvc.present(sheetVc, animated: true) { [weak self] in
|
||||||
|
self?.isOpening = false
|
||||||
|
self?.isOpen = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateLayout() {
|
||||||
|
if let contentHeight = self.innerView?.subviews.first?.frame.size.height {
|
||||||
|
self.sheetVc?.updateDetents(contentHeight: self.clampHeight(contentHeight),
|
||||||
|
preventExpansion: self.preventExpansion)
|
||||||
|
self.selectedDetentIdentifier = self.sheetVc?.getCurrentDetentIdentifier()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func dismiss() {
|
||||||
|
self.isClosing = true
|
||||||
|
self.sheetVc?.dismiss(animated: true) { [weak self] in
|
||||||
|
self?.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Utils
|
||||||
|
|
||||||
|
private func clampHeight(_ height: CGFloat) -> CGFloat {
|
||||||
|
if height < self.minHeight {
|
||||||
|
return self.minHeight
|
||||||
|
} else if height > self.maxHeight {
|
||||||
|
return self.maxHeight
|
||||||
|
}
|
||||||
|
return height
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UISheetPresentationControllerDelegate
|
||||||
|
|
||||||
|
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
|
||||||
|
self.onAttemptDismiss()
|
||||||
|
return !self.preventDismiss
|
||||||
|
}
|
||||||
|
|
||||||
|
func presentationControllerWillDismiss(_ presentationController: UIPresentationController) {
|
||||||
|
self.isClosing = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
|
||||||
|
self.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
func sheetPresentationControllerDidChangeSelectedDetentIdentifier(_ sheetPresentationController: UISheetPresentationController) {
|
||||||
|
self.selectedDetentIdentifier = sheetPresentationController.selectedDetentIdentifier
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
//
|
||||||
|
// SheetViewController.swift
|
||||||
|
// Pods
|
||||||
|
//
|
||||||
|
// Created by Hailey on 9/30/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SheetViewController: UIViewController {
|
||||||
|
init() {
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
|
||||||
|
self.modalPresentationStyle = .formSheet
|
||||||
|
self.isModalInPresentation = false
|
||||||
|
|
||||||
|
if let sheet = self.sheetPresentationController {
|
||||||
|
sheet.prefersGrabberVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setDetents(contentHeight: CGFloat, preventExpansion: Bool) {
|
||||||
|
guard let sheet = self.sheetPresentationController,
|
||||||
|
let screenHeight = Util.getScreenHeight()
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if contentHeight > screenHeight - 100 {
|
||||||
|
sheet.detents = [
|
||||||
|
.large()
|
||||||
|
]
|
||||||
|
sheet.selectedDetentIdentifier = .large
|
||||||
|
} else {
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
|
sheet.detents = [
|
||||||
|
.custom { _ in
|
||||||
|
return contentHeight
|
||||||
|
}
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
sheet.detents = [
|
||||||
|
.medium()
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
if !preventExpansion {
|
||||||
|
sheet.detents.append(.large())
|
||||||
|
}
|
||||||
|
sheet.selectedDetentIdentifier = .medium
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateDetents(contentHeight: CGFloat, preventExpansion: Bool) {
|
||||||
|
if let sheet = self.sheetPresentationController {
|
||||||
|
sheet.animateChanges {
|
||||||
|
self.setDetents(contentHeight: contentHeight, preventExpansion: preventExpansion)
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
|
sheet.invalidateDetents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCurrentDetentIdentifier() -> UISheetPresentationController.Detent.Identifier? {
|
||||||
|
guard let sheet = self.sheetPresentationController else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return sheet.selectedDetentIdentifier
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
//
|
||||||
|
// Util.swift
|
||||||
|
// Pods
|
||||||
|
//
|
||||||
|
// Created by Hailey on 10/2/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
class Util {
|
||||||
|
static func getScreenHeight() -> CGFloat? {
|
||||||
|
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||||
|
let window = windowScene.windows.first {
|
||||||
|
let safeAreaInsets = window.safeAreaInsets
|
||||||
|
let fullScreenHeight = UIScreen.main.bounds.height
|
||||||
|
return fullScreenHeight - (safeAreaInsets.top + safeAreaInsets.bottom)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
import {
|
||||||
|
Dimensions,
|
||||||
|
NativeSyntheticEvent,
|
||||||
|
Platform,
|
||||||
|
StyleProp,
|
||||||
|
View,
|
||||||
|
ViewStyle,
|
||||||
|
} from 'react-native'
|
||||||
|
import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core'
|
||||||
|
|
||||||
|
import {BottomSheetState, BottomSheetViewProps} from './BottomSheet.types'
|
||||||
|
|
||||||
|
const screenHeight = Dimensions.get('screen').height
|
||||||
|
|
||||||
|
const NativeView: React.ComponentType<
|
||||||
|
BottomSheetViewProps & {
|
||||||
|
ref: React.RefObject<any>
|
||||||
|
style: StyleProp<ViewStyle>
|
||||||
|
}
|
||||||
|
> = requireNativeViewManager('BottomSheet')
|
||||||
|
|
||||||
|
const NativeModule = requireNativeModule('BottomSheet')
|
||||||
|
|
||||||
|
export class BottomSheet extends React.Component<
|
||||||
|
BottomSheetViewProps,
|
||||||
|
{
|
||||||
|
open: boolean
|
||||||
|
}
|
||||||
|
> {
|
||||||
|
ref = React.createRef<any>()
|
||||||
|
|
||||||
|
constructor(props: BottomSheetViewProps) {
|
||||||
|
super(props)
|
||||||
|
this.state = {
|
||||||
|
open: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
present() {
|
||||||
|
this.setState({open: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
dismiss() {
|
||||||
|
this.ref.current?.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
private onStateChange = (
|
||||||
|
event: NativeSyntheticEvent<{state: BottomSheetState}>,
|
||||||
|
) => {
|
||||||
|
const {state} = event.nativeEvent
|
||||||
|
const isOpen = state !== 'closed'
|
||||||
|
this.setState({open: isOpen})
|
||||||
|
this.props.onStateChange?.(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateLayout = () => {
|
||||||
|
this.ref.current?.updateLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
static dismissAll = async () => {
|
||||||
|
await NativeModule.dismissAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {children, backgroundColor, ...rest} = this.props
|
||||||
|
const cornerRadius = rest.cornerRadius ?? 0
|
||||||
|
|
||||||
|
if (!this.state.open) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NativeView
|
||||||
|
{...rest}
|
||||||
|
onStateChange={this.onStateChange}
|
||||||
|
ref={this.ref}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
height: screenHeight,
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
containerBackgroundColor={backgroundColor}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor,
|
||||||
|
},
|
||||||
|
Platform.OS === 'android' && {
|
||||||
|
borderTopLeftRadius: cornerRadius,
|
||||||
|
borderTopRightRadius: cornerRadius,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<View onLayout={this.updateLayout}>{children}</View>
|
||||||
|
</View>
|
||||||
|
</NativeView>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {ColorValue, NativeSyntheticEvent} from 'react-native'
|
||||||
|
|
||||||
|
export type BottomSheetState = 'closed' | 'closing' | 'open' | 'opening'
|
||||||
|
|
||||||
|
export enum BottomSheetSnapPoint {
|
||||||
|
Hidden,
|
||||||
|
Partial,
|
||||||
|
Full,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BottomSheetAttemptDismissEvent = NativeSyntheticEvent<object>
|
||||||
|
export type BottomSheetSnapPointChangeEvent = NativeSyntheticEvent<{
|
||||||
|
snapPoint: BottomSheetSnapPoint
|
||||||
|
}>
|
||||||
|
export type BottomSheetStateChangeEvent = NativeSyntheticEvent<{
|
||||||
|
state: BottomSheetState
|
||||||
|
}>
|
||||||
|
|
||||||
|
export interface BottomSheetViewProps {
|
||||||
|
children: React.ReactNode
|
||||||
|
cornerRadius?: number
|
||||||
|
preventDismiss?: boolean
|
||||||
|
preventExpansion?: boolean
|
||||||
|
backgroundColor?: ColorValue
|
||||||
|
containerBackgroundColor?: ColorValue
|
||||||
|
disableDrag?: boolean
|
||||||
|
|
||||||
|
minHeight?: number
|
||||||
|
maxHeight?: number
|
||||||
|
|
||||||
|
onAttemptDismiss?: (event: BottomSheetAttemptDismissEvent) => void
|
||||||
|
onSnapPointChange?: (event: BottomSheetSnapPointChangeEvent) => void
|
||||||
|
onStateChange?: (event: BottomSheetStateChangeEvent) => void
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import {BottomSheetViewProps} from './BottomSheet.types'
|
||||||
|
|
||||||
|
export function BottomSheet(_: BottomSheetViewProps) {
|
||||||
|
throw new Error('BottomSheetView is not available on web')
|
||||||
|
}
|
||||||
+2
-3
@@ -137,7 +137,6 @@
|
|||||||
"expo-sharing": "^12.0.1",
|
"expo-sharing": "^12.0.1",
|
||||||
"expo-splash-screen": "~0.27.4",
|
"expo-splash-screen": "~0.27.4",
|
||||||
"expo-status-bar": "~1.12.1",
|
"expo-status-bar": "~1.12.1",
|
||||||
"expo-system-ui": "~3.0.4",
|
|
||||||
"expo-task-manager": "~11.8.1",
|
"expo-task-manager": "~11.8.1",
|
||||||
"expo-updates": "~0.25.14",
|
"expo-updates": "~0.25.14",
|
||||||
"expo-web-browser": "~13.0.3",
|
"expo-web-browser": "~13.0.3",
|
||||||
@@ -171,11 +170,11 @@
|
|||||||
"react-native-compressor": "^1.8.24",
|
"react-native-compressor": "^1.8.24",
|
||||||
"react-native-date-picker": "^4.4.2",
|
"react-native-date-picker": "^4.4.2",
|
||||||
"react-native-drawer-layout": "^4.0.0-alpha.3",
|
"react-native-drawer-layout": "^4.0.0-alpha.3",
|
||||||
"react-native-gesture-handler": "~2.16.2",
|
"react-native-gesture-handler": "2.20.0",
|
||||||
"react-native-get-random-values": "~1.11.0",
|
"react-native-get-random-values": "~1.11.0",
|
||||||
"react-native-image-crop-picker": "0.41.2",
|
"react-native-image-crop-picker": "0.41.2",
|
||||||
"react-native-ios-context-menu": "^1.15.3",
|
"react-native-ios-context-menu": "^1.15.3",
|
||||||
"react-native-keyboard-controller": "^1.12.1",
|
"react-native-keyboard-controller": "^1.14.0",
|
||||||
"react-native-mmkv": "^2.12.2",
|
"react-native-mmkv": "^2.12.2",
|
||||||
"react-native-pager-view": "6.2.3",
|
"react-native-pager-view": "6.2.3",
|
||||||
"react-native-picker-select": "^9.1.3",
|
"react-native-picker-select": "^9.1.3",
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
import 'react-native-url-polyfill/auto'
|
import 'react-native-url-polyfill/auto'
|
||||||
import 'lib/sentry' // must be near top
|
import '#/lib/sentry' // must be near top
|
||||||
import 'view/icons'
|
import '#/view/icons'
|
||||||
|
|
||||||
import React, {useEffect, useState} from 'react'
|
import React, {useEffect, useState} from 'react'
|
||||||
import {GestureHandlerRootView} from 'react-native-gesture-handler'
|
import {GestureHandlerRootView} from 'react-native-gesture-handler'
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {ColorSchemeName, useColorScheme} from 'react-native'
|
import {ColorSchemeName, useColorScheme} from 'react-native'
|
||||||
import * as SystemUI from 'expo-system-ui'
|
|
||||||
|
|
||||||
import {isWeb} from 'platform/detection'
|
import {isWeb} from '#/platform/detection'
|
||||||
import {useThemePrefs} from 'state/shell'
|
import {useThemePrefs} from '#/state/shell'
|
||||||
import {dark, dim, light} from '#/alf/themes'
|
import {dark, dim, light} from '#/alf/themes'
|
||||||
import {ThemeName} from '#/alf/types'
|
import {ThemeName} from '#/alf/types'
|
||||||
|
|
||||||
@@ -12,7 +11,6 @@ export function useColorModeTheme(): ThemeName {
|
|||||||
|
|
||||||
React.useLayoutEffect(() => {
|
React.useLayoutEffect(() => {
|
||||||
updateDocument(theme)
|
updateDocument(theme)
|
||||||
SystemUI.setBackgroundColorAsync(getBackgroundColor(theme))
|
|
||||||
}, [theme])
|
}, [theme])
|
||||||
|
|
||||||
return theme
|
return theme
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ export type ButtonProps = Pick<
|
|||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
hoverStyle?: StyleProp<ViewStyle>
|
hoverStyle?: StyleProp<ViewStyle>
|
||||||
children: NonTextElements | ((context: ButtonContext) => NonTextElements)
|
children: NonTextElements | ((context: ButtonContext) => NonTextElements)
|
||||||
|
PressableComponent?: React.ComponentType<PressableProps>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean}
|
export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean}
|
||||||
@@ -114,6 +115,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
style,
|
style,
|
||||||
hoverStyle: hoverStyleProp,
|
hoverStyle: hoverStyleProp,
|
||||||
|
PressableComponent = Pressable,
|
||||||
...rest
|
...rest
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
@@ -449,10 +451,11 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
|||||||
const flattenedBaseStyles = flatten([baseStyles, style])
|
const flattenedBaseStyles = flatten([baseStyles, style])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<PressableComponent
|
||||||
role="button"
|
role="button"
|
||||||
accessibilityHint={undefined} // optional
|
accessibilityHint={undefined} // optional
|
||||||
{...rest}
|
{...rest}
|
||||||
|
// @ts-ignore - this will always be a pressable
|
||||||
ref={ref}
|
ref={ref}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
aria-pressed={state.pressed}
|
aria-pressed={state.pressed}
|
||||||
@@ -500,7 +503,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
|||||||
<Context.Provider value={context}>
|
<Context.Provider value={context}>
|
||||||
{typeof children === 'function' ? children(context) : children}
|
{typeof children === 'function' ? children(context) : children}
|
||||||
</Context.Provider>
|
</Context.Provider>
|
||||||
</Pressable>
|
</PressableComponent>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,9 +6,14 @@ import {
|
|||||||
DialogControlRefProps,
|
DialogControlRefProps,
|
||||||
DialogOuterProps,
|
DialogOuterProps,
|
||||||
} from '#/components/Dialog/types'
|
} from '#/components/Dialog/types'
|
||||||
|
import {BottomSheetSnapPoint} from '../../../modules/bottom-sheet/src/BottomSheet.types'
|
||||||
|
|
||||||
export const Context = React.createContext<DialogContextProps>({
|
export const Context = React.createContext<DialogContextProps>({
|
||||||
close: () => {},
|
close: () => {},
|
||||||
|
isNativeDialog: false,
|
||||||
|
nativeSnapPoint: BottomSheetSnapPoint.Hidden,
|
||||||
|
disableDrag: false,
|
||||||
|
setDisableDrag: () => {},
|
||||||
})
|
})
|
||||||
|
|
||||||
export function useDialogContext() {
|
export function useDialogContext() {
|
||||||
|
|||||||
+173
-207
@@ -1,86 +1,48 @@
|
|||||||
import React, {useImperativeHandle} from 'react'
|
import React, {useImperativeHandle} from 'react'
|
||||||
import {
|
import {
|
||||||
Dimensions,
|
NativeScrollEvent,
|
||||||
Keyboard,
|
NativeSyntheticEvent,
|
||||||
Pressable,
|
Pressable,
|
||||||
|
ScrollView,
|
||||||
StyleProp,
|
StyleProp,
|
||||||
|
TextInput,
|
||||||
View,
|
View,
|
||||||
ViewStyle,
|
ViewStyle,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import Animated, {useAnimatedStyle} from 'react-native-reanimated'
|
import {
|
||||||
|
KeyboardAwareScrollView,
|
||||||
|
useKeyboardHandler,
|
||||||
|
} from 'react-native-keyboard-controller'
|
||||||
|
import {runOnJS} from 'react-native-reanimated'
|
||||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
import BottomSheet, {
|
import {msg} from '@lingui/macro'
|
||||||
BottomSheetBackdropProps,
|
import {useLingui} from '@lingui/react'
|
||||||
BottomSheetFlatList,
|
|
||||||
BottomSheetFlatListMethods,
|
|
||||||
BottomSheetScrollView,
|
|
||||||
BottomSheetScrollViewMethods,
|
|
||||||
BottomSheetTextInput,
|
|
||||||
BottomSheetView,
|
|
||||||
useBottomSheet,
|
|
||||||
WINDOW_HEIGHT,
|
|
||||||
} from '@discord/bottom-sheet/src'
|
|
||||||
import {BottomSheetFlatListProps} from '@discord/bottom-sheet/src/components/bottomSheetScrollable/types'
|
|
||||||
|
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
|
import {isAndroid, isIOS} from '#/platform/detection'
|
||||||
|
import {useA11y} from '#/state/a11y'
|
||||||
import {useDialogStateControlContext} from '#/state/dialogs'
|
import {useDialogStateControlContext} from '#/state/dialogs'
|
||||||
import {atoms as a, flatten, useTheme} from '#/alf'
|
import {List, ListMethods, ListProps} from '#/view/com/util/List'
|
||||||
import {Context} from '#/components/Dialog/context'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import {Context, useDialogContext} from '#/components/Dialog/context'
|
||||||
import {
|
import {
|
||||||
DialogControlProps,
|
DialogControlProps,
|
||||||
DialogInnerProps,
|
DialogInnerProps,
|
||||||
DialogOuterProps,
|
DialogOuterProps,
|
||||||
} from '#/components/Dialog/types'
|
} from '#/components/Dialog/types'
|
||||||
import {createInput} from '#/components/forms/TextField'
|
import {createInput} from '#/components/forms/TextField'
|
||||||
import {FullWindowOverlay} from '#/components/FullWindowOverlay'
|
import {Portal as DefaultPortal} from '#/components/Portal'
|
||||||
import {Portal} from '#/components/Portal'
|
import {BottomSheet, BottomSheetSnapPoint} from '../../../modules/bottom-sheet'
|
||||||
|
import {
|
||||||
|
BottomSheetSnapPointChangeEvent,
|
||||||
|
BottomSheetStateChangeEvent,
|
||||||
|
} from '../../../modules/bottom-sheet/src/BottomSheet.types'
|
||||||
|
|
||||||
export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
|
export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
|
||||||
export * from '#/components/Dialog/types'
|
export * from '#/components/Dialog/types'
|
||||||
export * from '#/components/Dialog/utils'
|
export * from '#/components/Dialog/utils'
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export const Input = createInput(BottomSheetTextInput)
|
export const Input = createInput(TextInput)
|
||||||
|
|
||||||
function Backdrop(props: BottomSheetBackdropProps) {
|
|
||||||
const t = useTheme()
|
|
||||||
const bottomSheet = useBottomSheet()
|
|
||||||
|
|
||||||
const animatedStyle = useAnimatedStyle(() => {
|
|
||||||
const opacity =
|
|
||||||
(Math.abs(WINDOW_HEIGHT - props.animatedPosition.value) - 50) / 1000
|
|
||||||
|
|
||||||
return {
|
|
||||||
opacity: Math.min(Math.max(opacity, 0), 0.55),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const onPress = React.useCallback(() => {
|
|
||||||
bottomSheet.close()
|
|
||||||
}, [bottomSheet])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Animated.View
|
|
||||||
style={[
|
|
||||||
t.atoms.bg_contrast_300,
|
|
||||||
{
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
position: 'absolute',
|
|
||||||
},
|
|
||||||
animatedStyle,
|
|
||||||
]}>
|
|
||||||
<Pressable
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel="Dialog backdrop"
|
|
||||||
accessibilityHint="Press the backdrop to close the dialog"
|
|
||||||
style={{flex: 1}}
|
|
||||||
onPress={onPress}
|
|
||||||
/>
|
|
||||||
</Animated.View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Outer({
|
export function Outer({
|
||||||
children,
|
children,
|
||||||
@@ -88,24 +50,22 @@ export function Outer({
|
|||||||
onClose,
|
onClose,
|
||||||
nativeOptions,
|
nativeOptions,
|
||||||
testID,
|
testID,
|
||||||
|
Portal = DefaultPortal,
|
||||||
}: React.PropsWithChildren<DialogOuterProps>) {
|
}: React.PropsWithChildren<DialogOuterProps>) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const sheet = React.useRef<BottomSheet>(null)
|
const ref = React.useRef<BottomSheet>(null)
|
||||||
const sheetOptions = nativeOptions?.sheet || {}
|
|
||||||
const hasSnapPoints = !!sheetOptions.snapPoints
|
|
||||||
const insets = useSafeAreaInsets()
|
|
||||||
const closeCallbacks = React.useRef<(() => void)[]>([])
|
const closeCallbacks = React.useRef<(() => void)[]>([])
|
||||||
const {setDialogIsOpen} = useDialogStateControlContext()
|
const {setDialogIsOpen, setFullyExpandedCount} =
|
||||||
|
useDialogStateControlContext()
|
||||||
|
|
||||||
/*
|
const prevSnapPoint = React.useRef<BottomSheetSnapPoint>(
|
||||||
* Used to manage open/closed, but index is otherwise handled internally by `BottomSheet`
|
BottomSheetSnapPoint.Hidden,
|
||||||
*/
|
)
|
||||||
const [openIndex, setOpenIndex] = React.useState(-1)
|
|
||||||
|
|
||||||
/*
|
const [disableDrag, setDisableDrag] = React.useState(false)
|
||||||
* `openIndex` is the index of the snap point to open the bottom sheet to. If >0, the bottom sheet is open.
|
const [snapPoint, setSnapPoint] = React.useState<BottomSheetSnapPoint>(
|
||||||
*/
|
BottomSheetSnapPoint.Partial,
|
||||||
const isOpen = openIndex > -1
|
)
|
||||||
|
|
||||||
const callQueuedCallbacks = React.useCallback(() => {
|
const callQueuedCallbacks = React.useCallback(() => {
|
||||||
for (const cb of closeCallbacks.current) {
|
for (const cb of closeCallbacks.current) {
|
||||||
@@ -119,25 +79,19 @@ export function Outer({
|
|||||||
closeCallbacks.current = []
|
closeCallbacks.current = []
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const open = React.useCallback<DialogControlProps['open']>(
|
const open = React.useCallback<DialogControlProps['open']>(() => {
|
||||||
({index} = {}) => {
|
// Run any leftover callbacks that might have been queued up before calling `.open()`
|
||||||
// Run any leftover callbacks that might have been queued up before calling `.open()`
|
callQueuedCallbacks()
|
||||||
callQueuedCallbacks()
|
setDialogIsOpen(control.id, true)
|
||||||
|
ref.current?.present()
|
||||||
setDialogIsOpen(control.id, true)
|
}, [setDialogIsOpen, control.id, callQueuedCallbacks])
|
||||||
// can be set to any index of `snapPoints`, but `0` is the first i.e. "open"
|
|
||||||
setOpenIndex(index || 0)
|
|
||||||
sheet.current?.snapToIndex(index || 0)
|
|
||||||
},
|
|
||||||
[setDialogIsOpen, control.id, callQueuedCallbacks],
|
|
||||||
)
|
|
||||||
|
|
||||||
// This is the function that we call when we want to dismiss the dialog.
|
// This is the function that we call when we want to dismiss the dialog.
|
||||||
const close = React.useCallback<DialogControlProps['close']>(cb => {
|
const close = React.useCallback<DialogControlProps['close']>(cb => {
|
||||||
if (typeof cb === 'function') {
|
if (typeof cb === 'function') {
|
||||||
closeCallbacks.current.push(cb)
|
closeCallbacks.current.push(cb)
|
||||||
}
|
}
|
||||||
sheet.current?.close()
|
ref.current?.dismiss()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// This is the actual thing we are doing once we "confirm" the dialog. We want the dialog's close animation to
|
// This is the actual thing we are doing once we "confirm" the dialog. We want the dialog's close animation to
|
||||||
@@ -146,12 +100,39 @@ export function Outer({
|
|||||||
// This removes the dialog from our list of stored dialogs. Not super necessary on iOS, but on Android this
|
// This removes the dialog from our list of stored dialogs. Not super necessary on iOS, but on Android this
|
||||||
// tells us that we need to toggle the accessibility overlay setting
|
// tells us that we need to toggle the accessibility overlay setting
|
||||||
setDialogIsOpen(control.id, false)
|
setDialogIsOpen(control.id, false)
|
||||||
setOpenIndex(-1)
|
|
||||||
|
|
||||||
callQueuedCallbacks()
|
callQueuedCallbacks()
|
||||||
onClose?.()
|
onClose?.()
|
||||||
}, [callQueuedCallbacks, control.id, onClose, setDialogIsOpen])
|
}, [callQueuedCallbacks, control.id, onClose, setDialogIsOpen])
|
||||||
|
|
||||||
|
const onSnapPointChange = (e: BottomSheetSnapPointChangeEvent) => {
|
||||||
|
const {snapPoint} = e.nativeEvent
|
||||||
|
setSnapPoint(snapPoint)
|
||||||
|
|
||||||
|
if (
|
||||||
|
snapPoint === BottomSheetSnapPoint.Full &&
|
||||||
|
prevSnapPoint.current !== BottomSheetSnapPoint.Full
|
||||||
|
) {
|
||||||
|
setFullyExpandedCount(c => c + 1)
|
||||||
|
} else if (
|
||||||
|
snapPoint !== BottomSheetSnapPoint.Full &&
|
||||||
|
prevSnapPoint.current === BottomSheetSnapPoint.Full
|
||||||
|
) {
|
||||||
|
setFullyExpandedCount(c => c - 1)
|
||||||
|
}
|
||||||
|
prevSnapPoint.current = snapPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
const onStateChange = (e: BottomSheetStateChangeEvent) => {
|
||||||
|
if (e.nativeEvent.state === 'closed') {
|
||||||
|
onCloseAnimationComplete()
|
||||||
|
|
||||||
|
if (prevSnapPoint.current === BottomSheetSnapPoint.Full) {
|
||||||
|
setFullyExpandedCount(c => c - 1)
|
||||||
|
}
|
||||||
|
prevSnapPoint.current = BottomSheetSnapPoint.Hidden
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useImperativeHandle(
|
useImperativeHandle(
|
||||||
control.ref,
|
control.ref,
|
||||||
() => ({
|
() => ({
|
||||||
@@ -161,159 +142,144 @@ export function Outer({
|
|||||||
[open, close],
|
[open, close],
|
||||||
)
|
)
|
||||||
|
|
||||||
React.useEffect(() => {
|
const context = React.useMemo(
|
||||||
return () => {
|
() => ({
|
||||||
setDialogIsOpen(control.id, false)
|
close,
|
||||||
}
|
isNativeDialog: true,
|
||||||
}, [control.id, setDialogIsOpen])
|
nativeSnapPoint: snapPoint,
|
||||||
|
disableDrag,
|
||||||
const context = React.useMemo(() => ({close}), [close])
|
setDisableDrag,
|
||||||
|
}),
|
||||||
|
[close, snapPoint, disableDrag, setDisableDrag],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
isOpen && (
|
<Portal>
|
||||||
<Portal>
|
<Context.Provider value={context}>
|
||||||
<FullWindowOverlay>
|
<BottomSheet
|
||||||
<View
|
ref={ref}
|
||||||
// iOS
|
cornerRadius={20}
|
||||||
accessibilityViewIsModal
|
backgroundColor={t.atoms.bg.backgroundColor}
|
||||||
// Android
|
{...nativeOptions}
|
||||||
importantForAccessibility="yes"
|
onSnapPointChange={onSnapPointChange}
|
||||||
style={[a.absolute, a.inset_0]}
|
onStateChange={onStateChange}
|
||||||
testID={testID}
|
disableDrag={disableDrag}>
|
||||||
onTouchMove={() => Keyboard.dismiss()}>
|
<View testID={testID}>{children}</View>
|
||||||
<BottomSheet
|
</BottomSheet>
|
||||||
enableDynamicSizing={!hasSnapPoints}
|
</Context.Provider>
|
||||||
enablePanDownToClose
|
</Portal>
|
||||||
keyboardBehavior="interactive"
|
|
||||||
android_keyboardInputMode="adjustResize"
|
|
||||||
keyboardBlurBehavior="restore"
|
|
||||||
topInset={insets.top}
|
|
||||||
{...sheetOptions}
|
|
||||||
snapPoints={sheetOptions.snapPoints || ['100%']}
|
|
||||||
ref={sheet}
|
|
||||||
index={openIndex}
|
|
||||||
backgroundStyle={{backgroundColor: 'transparent'}}
|
|
||||||
backdropComponent={Backdrop}
|
|
||||||
handleIndicatorStyle={{backgroundColor: t.palette.primary_500}}
|
|
||||||
handleStyle={{display: 'none'}}
|
|
||||||
onClose={onCloseAnimationComplete}>
|
|
||||||
<Context.Provider value={context}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.absolute,
|
|
||||||
a.inset_0,
|
|
||||||
t.atoms.bg,
|
|
||||||
{
|
|
||||||
borderTopLeftRadius: 40,
|
|
||||||
borderTopRightRadius: 40,
|
|
||||||
height: Dimensions.get('window').height * 2,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
{children}
|
|
||||||
</Context.Provider>
|
|
||||||
</BottomSheet>
|
|
||||||
</View>
|
|
||||||
</FullWindowOverlay>
|
|
||||||
</Portal>
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Inner({children, style}: DialogInnerProps) {
|
export function Inner({children, style}: DialogInnerProps) {
|
||||||
const insets = useSafeAreaInsets()
|
const insets = useSafeAreaInsets()
|
||||||
return (
|
return (
|
||||||
<BottomSheetView
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.py_xl,
|
a.pt_2xl,
|
||||||
a.px_xl,
|
a.px_xl,
|
||||||
{
|
{
|
||||||
paddingTop: 40,
|
paddingBottom: insets.bottom + insets.top,
|
||||||
borderTopLeftRadius: 40,
|
|
||||||
borderTopRightRadius: 40,
|
|
||||||
paddingBottom: insets.bottom + a.pb_5xl.paddingBottom,
|
|
||||||
},
|
},
|
||||||
flatten(style),
|
style,
|
||||||
]}>
|
]}>
|
||||||
{children}
|
{children}
|
||||||
</BottomSheetView>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ScrollableInner = React.forwardRef<
|
export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>(
|
||||||
BottomSheetScrollViewMethods,
|
function ScrollableInner({children, style, ...props}, ref) {
|
||||||
DialogInnerProps
|
const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext()
|
||||||
>(function ScrollableInner({children, style}, ref) {
|
const insets = useSafeAreaInsets()
|
||||||
const insets = useSafeAreaInsets()
|
const [keyboardHeight, setKeyboardHeight] = React.useState(0)
|
||||||
return (
|
useKeyboardHandler({
|
||||||
<BottomSheetScrollView
|
onEnd: e => {
|
||||||
keyboardShouldPersistTaps="handled"
|
'worklet'
|
||||||
style={[
|
runOnJS(setKeyboardHeight)(e.height)
|
||||||
a.flex_1, // main diff is this
|
},
|
||||||
a.p_xl,
|
})
|
||||||
a.h_full,
|
|
||||||
{
|
const basePading =
|
||||||
paddingTop: 40,
|
(isIOS ? 30 : 50) + (isIOS ? keyboardHeight / 4 : keyboardHeight)
|
||||||
borderTopLeftRadius: 40,
|
const fullPaddingBase = insets.bottom + insets.top + basePading
|
||||||
borderTopRightRadius: 40,
|
const fullPadding = isIOS ? fullPaddingBase : fullPaddingBase + 50
|
||||||
},
|
|
||||||
style,
|
const paddingBottom =
|
||||||
]}
|
nativeSnapPoint === BottomSheetSnapPoint.Full ? fullPadding : basePading
|
||||||
contentContainerStyle={a.pb_4xl}
|
|
||||||
ref={ref}>
|
const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||||
{children}
|
const {contentOffset} = e.nativeEvent
|
||||||
<View style={{height: insets.bottom + a.pt_5xl.paddingTop}} />
|
if (contentOffset.y > 0 && !disableDrag) {
|
||||||
</BottomSheetScrollView>
|
setDisableDrag(true)
|
||||||
)
|
} else if (contentOffset.y <= 1 && disableDrag) {
|
||||||
})
|
setDisableDrag(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<KeyboardAwareScrollView
|
||||||
|
style={[style]}
|
||||||
|
contentContainerStyle={[a.pt_2xl, a.px_xl, {paddingBottom}]}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
bounces={nativeSnapPoint === BottomSheetSnapPoint.Full}
|
||||||
|
bottomOffset={30}
|
||||||
|
scrollEventThrottle={50}
|
||||||
|
onScroll={isAndroid ? onScroll : undefined}>
|
||||||
|
{children}
|
||||||
|
</KeyboardAwareScrollView>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
export const InnerFlatList = React.forwardRef<
|
export const InnerFlatList = React.forwardRef<
|
||||||
BottomSheetFlatListMethods,
|
ListMethods,
|
||||||
BottomSheetFlatListProps<any> & {webInnerStyle?: StyleProp<ViewStyle>}
|
ListProps<any> & {webInnerStyle?: StyleProp<ViewStyle>}
|
||||||
>(function InnerFlatList({style, contentContainerStyle, ...props}, ref) {
|
>(function InnerFlatList({style, ...props}, ref) {
|
||||||
const insets = useSafeAreaInsets()
|
const insets = useSafeAreaInsets()
|
||||||
|
const {nativeSnapPoint} = useDialogContext()
|
||||||
return (
|
return (
|
||||||
<BottomSheetFlatList
|
<List
|
||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
contentContainerStyle={[a.pb_4xl, flatten(contentContainerStyle)]}
|
bounces={nativeSnapPoint === BottomSheetSnapPoint.Full}
|
||||||
ListFooterComponent={
|
ListFooterComponent={
|
||||||
<View style={{height: insets.bottom + a.pt_5xl.paddingTop}} />
|
<View style={{height: insets.bottom + a.pt_5xl.paddingTop}} />
|
||||||
}
|
}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
style={[
|
style={[style]}
|
||||||
a.flex_1,
|
|
||||||
a.p_xl,
|
|
||||||
a.pt_0,
|
|
||||||
a.h_full,
|
|
||||||
{
|
|
||||||
marginTop: 40,
|
|
||||||
},
|
|
||||||
flatten(style),
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export function Handle() {
|
export function Handle() {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
|
const {screenReaderEnabled} = useA11y()
|
||||||
|
const {close} = useDialogContext()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.absolute, a.w_full, a.align_center, a.z_10, {height: 40}]}>
|
<View style={[a.absolute, a.w_full, a.align_center, a.z_10, {height: 20}]}>
|
||||||
<View
|
<Pressable
|
||||||
style={[
|
accessible={screenReaderEnabled}
|
||||||
a.rounded_sm,
|
onPress={() => close()}
|
||||||
{
|
accessibilityLabel={_(msg`Dismiss`)}
|
||||||
top: a.pt_lg.paddingTop,
|
accessibilityHint={_(msg`Double tap to close the dialog`)}>
|
||||||
width: 35,
|
<View
|
||||||
height: 4,
|
style={[
|
||||||
alignSelf: 'center',
|
a.rounded_sm,
|
||||||
backgroundColor: t.palette.contrast_900,
|
{
|
||||||
opacity: 0.5,
|
top: 10,
|
||||||
},
|
width: 35,
|
||||||
]}
|
height: 5,
|
||||||
/>
|
alignSelf: 'center',
|
||||||
|
backgroundColor: t.palette.contrast_975,
|
||||||
|
opacity: 0.5,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,6 +103,10 @@ export function Outer({
|
|||||||
const context = React.useMemo(
|
const context = React.useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
close,
|
close,
|
||||||
|
isNativeDialog: false,
|
||||||
|
nativeSnapPoint: 0,
|
||||||
|
disableDrag: false,
|
||||||
|
setDisableDrag: () => {},
|
||||||
}),
|
}),
|
||||||
[close],
|
[close],
|
||||||
)
|
)
|
||||||
@@ -229,10 +233,6 @@ export const InnerFlatList = React.forwardRef<
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export function Handle() {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Close() {
|
export function Close() {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {close} = React.useContext(Context)
|
const {close} = React.useContext(Context)
|
||||||
@@ -258,3 +258,7 @@ export function Close() {
|
|||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function Handle() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import {useCallback} from 'react'
|
||||||
|
|
||||||
|
import {useDialogStateControlContext} from '#/state/dialogs'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If we're calling a system API like the image picker that opens a sheet
|
||||||
|
* wrap it in this function to make sure the status bar is the correct color.
|
||||||
|
*/
|
||||||
|
export function useSheetWrapper() {
|
||||||
|
const {setFullyExpandedCount} = useDialogStateControlContext()
|
||||||
|
return useCallback(
|
||||||
|
async <T>(promise: Promise<T>): Promise<T> => {
|
||||||
|
setFullyExpandedCount(c => c + 1)
|
||||||
|
const res = await promise
|
||||||
|
setFullyExpandedCount(c => c - 1)
|
||||||
|
return res
|
||||||
|
},
|
||||||
|
[setFullyExpandedCount],
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -4,9 +4,11 @@ import type {
|
|||||||
GestureResponderEvent,
|
GestureResponderEvent,
|
||||||
ScrollViewProps,
|
ScrollViewProps,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {BottomSheetProps} from '@discord/bottom-sheet/src'
|
|
||||||
|
|
||||||
import {ViewStyleProp} from '#/alf'
|
import {ViewStyleProp} from '#/alf'
|
||||||
|
import {PortalComponent} from '#/components/Portal'
|
||||||
|
import {BottomSheetViewProps} from '../../../modules/bottom-sheet'
|
||||||
|
import {BottomSheetSnapPoint} from '../../../modules/bottom-sheet/src/BottomSheet.types'
|
||||||
|
|
||||||
type A11yProps = Required<AccessibilityProps>
|
type A11yProps = Required<AccessibilityProps>
|
||||||
|
|
||||||
@@ -37,6 +39,10 @@ export type DialogControlProps = DialogControlRefProps & {
|
|||||||
|
|
||||||
export type DialogContextProps = {
|
export type DialogContextProps = {
|
||||||
close: DialogControlProps['close']
|
close: DialogControlProps['close']
|
||||||
|
isNativeDialog: boolean
|
||||||
|
nativeSnapPoint: BottomSheetSnapPoint
|
||||||
|
disableDrag: boolean
|
||||||
|
setDisableDrag: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DialogControlOpenOptions = {
|
export type DialogControlOpenOptions = {
|
||||||
@@ -52,11 +58,10 @@ export type DialogControlOpenOptions = {
|
|||||||
export type DialogOuterProps = {
|
export type DialogOuterProps = {
|
||||||
control: DialogControlProps
|
control: DialogControlProps
|
||||||
onClose?: () => void
|
onClose?: () => void
|
||||||
nativeOptions?: {
|
nativeOptions?: Omit<BottomSheetViewProps, 'children'>
|
||||||
sheet?: Omit<BottomSheetProps, 'children'>
|
|
||||||
}
|
|
||||||
webOptions?: {}
|
webOptions?: {}
|
||||||
testID?: string
|
testID?: string
|
||||||
|
Portal?: PortalComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
type DialogInnerPropsBase<T> = React.PropsWithChildren<ViewStyleProp> & T
|
type DialogInnerPropsBase<T> = React.PropsWithChildren<ViewStyleProp> & T
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import {useKeyboardHandler} from 'react-native-keyboard-controller'
|
|
||||||
import Animated, {
|
|
||||||
useAnimatedStyle,
|
|
||||||
useSharedValue,
|
|
||||||
} from 'react-native-reanimated'
|
|
||||||
|
|
||||||
export function KeyboardControllerPadding({maxHeight}: {maxHeight?: number}) {
|
|
||||||
const keyboardHeight = useSharedValue(0)
|
|
||||||
|
|
||||||
useKeyboardHandler(
|
|
||||||
{
|
|
||||||
onMove: e => {
|
|
||||||
'worklet'
|
|
||||||
|
|
||||||
if (maxHeight && e.height > maxHeight) {
|
|
||||||
keyboardHeight.value = maxHeight
|
|
||||||
} else {
|
|
||||||
keyboardHeight.value = e.height
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
[maxHeight],
|
|
||||||
)
|
|
||||||
|
|
||||||
const animatedStyle = useAnimatedStyle(() => ({
|
|
||||||
height: keyboardHeight.value,
|
|
||||||
}))
|
|
||||||
|
|
||||||
return <Animated.View style={animatedStyle} />
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export function KeyboardControllerPadding({
|
|
||||||
maxHeight: _,
|
|
||||||
}: {
|
|
||||||
maxHeight?: number
|
|
||||||
}) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
@@ -1,20 +1,19 @@
|
|||||||
import React, {useMemo, useCallback} from 'react'
|
import React, {useCallback, useMemo} from 'react'
|
||||||
import {ActivityIndicator, FlatList, View} from 'react-native'
|
import {ActivityIndicator, FlatList, View} from 'react-native'
|
||||||
|
import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
|
|
||||||
|
|
||||||
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
|
|
||||||
import {useLikedByQuery} from '#/state/queries/post-liked-by'
|
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
|
import {useLikedByQuery} from '#/state/queries/post-liked-by'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
|
||||||
import {Text} from '#/components/Typography'
|
|
||||||
import * as Dialog from '#/components/Dialog'
|
|
||||||
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
|
|
||||||
import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
|
import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
|
||||||
|
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
|
||||||
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import * as Dialog from '#/components/Dialog'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
interface LikesDialogProps {
|
interface LikesDialogProps {
|
||||||
control: Dialog.DialogOuterProps['control']
|
control: Dialog.DialogOuterProps['control']
|
||||||
@@ -25,7 +24,6 @@ export function LikesDialog(props: LikesDialogProps) {
|
|||||||
return (
|
return (
|
||||||
<Dialog.Outer control={props.control}>
|
<Dialog.Outer control={props.control}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
|
|
||||||
<LikesDialogInner {...props} />
|
<LikesDialogInner {...props} />
|
||||||
</Dialog.Outer>
|
</Dialog.Outer>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -103,17 +103,17 @@ export function useLink({
|
|||||||
linkRequiresWarning(href, displayText),
|
linkRequiresWarning(href, displayText),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (requiresWarning) {
|
if (isWeb) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requiresWarning) {
|
||||||
openModal({
|
openModal({
|
||||||
name: 'link-warning',
|
name: 'link-warning',
|
||||||
text: displayText,
|
text: displayText,
|
||||||
href: href,
|
href: href,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
e.preventDefault()
|
|
||||||
|
|
||||||
if (isExternal) {
|
if (isExternal) {
|
||||||
openLink(href)
|
openLink(href)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {msg, Trans} from '@lingui/macro'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import flattenReactChildren from 'react-keyed-flatten-children'
|
import flattenReactChildren from 'react-keyed-flatten-children'
|
||||||
|
|
||||||
import {isNative} from 'platform/detection'
|
import {isNative} from '#/platform/detection'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Button, ButtonText} from '#/components/Button'
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
@@ -82,19 +82,21 @@ export function Outer({
|
|||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
}>) {
|
}>) {
|
||||||
const context = React.useContext(Context)
|
const context = React.useContext(Context)
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.Outer control={context.control}>
|
<Dialog.Outer
|
||||||
|
control={context.control}
|
||||||
|
nativeOptions={{preventExpansion: true}}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
|
|
||||||
{/* Re-wrap with context since Dialogs are portal-ed to root */}
|
{/* Re-wrap with context since Dialogs are portal-ed to root */}
|
||||||
<Context.Provider value={context}>
|
<Context.Provider value={context}>
|
||||||
<Dialog.ScrollableInner label="Menu TODO">
|
<Dialog.ScrollableInner label={_(msg`Menu`)} style={[a.pt_sm]}>
|
||||||
<View style={[a.gap_lg]}>
|
<View style={[a.gap_lg]}>
|
||||||
{children}
|
{children}
|
||||||
{isNative && showCancel && <Cancel />}
|
{isNative && showCancel && <Cancel />}
|
||||||
|
<View style={[{height: a.pb_lg.paddingBottom}]} />
|
||||||
</View>
|
</View>
|
||||||
<View style={{height: a.gap_lg.gap}} />
|
|
||||||
</Dialog.ScrollableInner>
|
</Dialog.ScrollableInner>
|
||||||
</Context.Provider>
|
</Context.Provider>
|
||||||
</Dialog.Outer>
|
</Dialog.Outer>
|
||||||
@@ -116,15 +118,14 @@ export function Item({children, label, style, onPress, ...rest}: ItemProps) {
|
|||||||
{...rest}
|
{...rest}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
accessibilityLabel={label}
|
accessibilityLabel={label}
|
||||||
onPress={e => {
|
onFocus={onFocus}
|
||||||
onPress(e)
|
onBlur={onBlur}
|
||||||
|
onPress={async e => {
|
||||||
|
await onPress(e)
|
||||||
if (!e.defaultPrevented) {
|
if (!e.defaultPrevented) {
|
||||||
control?.close()
|
control?.close()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onFocus={onFocus}
|
|
||||||
onBlur={onBlur}
|
|
||||||
onPressIn={e => {
|
onPressIn={e => {
|
||||||
onPressIn()
|
onPressIn()
|
||||||
rest.onPressIn?.(e)
|
rest.onPressIn?.(e)
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import {msg, Trans} from '@lingui/macro'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {differenceInSeconds} from 'date-fns'
|
import {differenceInSeconds} from 'date-fns'
|
||||||
|
|
||||||
|
import {HITSLOP_10} from '#/lib/constants'
|
||||||
import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
|
import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
|
||||||
|
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||||
import {isNative} from '#/platform/detection'
|
import {isNative} from '#/platform/detection'
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
import {HITSLOP_10} from 'lib/constants'
|
import {useSession} from '#/state/session'
|
||||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
|
||||||
import {useSession} from 'state/session'
|
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Button, ButtonText} from '#/components/Button'
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ type ComponentMap = {
|
|||||||
[id: string]: Component
|
[id: string]: Component
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PortalComponent = ({children}: {children?: React.ReactNode}) => null
|
||||||
|
|
||||||
export function createPortalGroup() {
|
export function createPortalGroup() {
|
||||||
const Context = React.createContext<ContextType>({
|
const Context = React.createContext<ContextType>({
|
||||||
outlet: null,
|
outlet: null,
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ import {msg} from '@lingui/macro'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||||
import {Button, ButtonColor, ButtonProps, ButtonText} from '#/components/Button'
|
import {Button, ButtonColor, ButtonText} from '#/components/Button'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
|
import {PortalComponent} from '#/components/Portal'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -25,9 +26,11 @@ export function Outer({
|
|||||||
children,
|
children,
|
||||||
control,
|
control,
|
||||||
testID,
|
testID,
|
||||||
|
Portal,
|
||||||
}: React.PropsWithChildren<{
|
}: React.PropsWithChildren<{
|
||||||
control: Dialog.DialogControlProps
|
control: Dialog.DialogControlProps
|
||||||
testID?: string
|
testID?: string
|
||||||
|
Portal?: PortalComponent
|
||||||
}>) {
|
}>) {
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
const titleId = React.useId()
|
const titleId = React.useId()
|
||||||
@@ -39,10 +42,9 @@ export function Outer({
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.Outer control={control} testID={testID}>
|
<Dialog.Outer control={control} testID={testID} Portal={Portal}>
|
||||||
|
<Dialog.Handle />
|
||||||
<Context.Provider value={context}>
|
<Context.Provider value={context}>
|
||||||
<Dialog.Handle />
|
|
||||||
|
|
||||||
<Dialog.ScrollableInner
|
<Dialog.ScrollableInner
|
||||||
accessibilityLabelledBy={titleId}
|
accessibilityLabelledBy={titleId}
|
||||||
accessibilityDescribedBy={descriptionId}
|
accessibilityDescribedBy={descriptionId}
|
||||||
@@ -141,7 +143,7 @@ export function Action({
|
|||||||
* Note: The dialog will close automatically when the action is pressed, you
|
* Note: The dialog will close automatically when the action is pressed, you
|
||||||
* should NOT close the dialog as a side effect of this method.
|
* should NOT close the dialog as a side effect of this method.
|
||||||
*/
|
*/
|
||||||
onPress: ButtonProps['onPress']
|
onPress: (e: GestureResponderEvent) => void
|
||||||
color?: ButtonColor
|
color?: ButtonColor
|
||||||
/**
|
/**
|
||||||
* Optional i18n string. If undefined, it will default to "Confirm".
|
* Optional i18n string. If undefined, it will default to "Confirm".
|
||||||
@@ -181,6 +183,7 @@ export function Basic({
|
|||||||
onConfirm,
|
onConfirm,
|
||||||
confirmButtonColor,
|
confirmButtonColor,
|
||||||
showCancel = true,
|
showCancel = true,
|
||||||
|
Portal,
|
||||||
}: React.PropsWithChildren<{
|
}: React.PropsWithChildren<{
|
||||||
control: Dialog.DialogOuterProps['control']
|
control: Dialog.DialogOuterProps['control']
|
||||||
title: string
|
title: string
|
||||||
@@ -194,12 +197,13 @@ export function Basic({
|
|||||||
* Note: The dialog will close automatically when the action is pressed, you
|
* Note: The dialog will close automatically when the action is pressed, you
|
||||||
* should NOT close the dialog as a side effect of this method.
|
* should NOT close the dialog as a side effect of this method.
|
||||||
*/
|
*/
|
||||||
onConfirm: ButtonProps['onPress']
|
onConfirm: (e: GestureResponderEvent) => void
|
||||||
confirmButtonColor?: ButtonColor
|
confirmButtonColor?: ButtonColor
|
||||||
showCancel?: boolean
|
showCancel?: boolean
|
||||||
|
Portal?: PortalComponent
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<Outer control={control} testID="confirmModal">
|
<Outer control={control} testID="confirmModal" Portal={Portal}>
|
||||||
<TitleText>{title}</TitleText>
|
<TitleText>{title}</TitleText>
|
||||||
<DescriptionText>{description}</DescriptionText>
|
<DescriptionText>{description}</DescriptionText>
|
||||||
<Actions>
|
<Actions>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {AppBskyLabelerDefs} from '@atproto/api'
|
|||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
export {useDialogControl as useReportDialogControl} from '#/components/Dialog'
|
|
||||||
import {getLabelingServiceTitle} from '#/lib/moderation'
|
import {getLabelingServiceTitle} from '#/lib/moderation'
|
||||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||||
import {Button, useButtonContext} from '#/components/Button'
|
import {Button, useButtonContext} from '#/components/Button'
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {useLingui} from '@lingui/react'
|
|||||||
|
|
||||||
import {getLabelingServiceTitle} from '#/lib/moderation'
|
import {getLabelingServiceTitle} from '#/lib/moderation'
|
||||||
import {ReportOption} from '#/lib/moderation/useReportOptions'
|
import {ReportOption} from '#/lib/moderation/useReportOptions'
|
||||||
|
import {isAndroid} from '#/platform/detection'
|
||||||
import {useAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
|
import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
@@ -225,6 +226,8 @@ export function SubmitView({
|
|||||||
{submitting && <ButtonIcon icon={Loader} />}
|
{submitting && <ButtonIcon icon={Loader} />}
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
|
{/* Maybe fix this later -h */}
|
||||||
|
{isAndroid ? <View style={{height: 300}} /> : null}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {Pressable, View} from 'react-native'
|
import {Pressable, View} from 'react-native'
|
||||||
|
import {ScrollView} from 'react-native-gesture-handler'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
@@ -8,12 +9,10 @@ import {useMyLabelersQuery} from '#/state/queries/preferences'
|
|||||||
export {useDialogControl as useReportDialogControl} from '#/components/Dialog'
|
export {useDialogControl as useReportDialogControl} from '#/components/Dialog'
|
||||||
|
|
||||||
import {AppBskyLabelerDefs} from '@atproto/api'
|
import {AppBskyLabelerDefs} from '@atproto/api'
|
||||||
import {BottomSheetScrollViewMethods} from '@discord/bottom-sheet/src'
|
|
||||||
|
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import {useDelayedLoading} from '#/components/hooks/useDelayedLoading'
|
import {useDelayedLoading} from '#/components/hooks/useDelayedLoading'
|
||||||
import {useOnKeyboardDidShow} from '#/components/hooks/useOnKeyboard'
|
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {SelectLabelerView} from './SelectLabelerView'
|
import {SelectLabelerView} from './SelectLabelerView'
|
||||||
@@ -25,7 +24,6 @@ export function ReportDialog(props: ReportDialogProps) {
|
|||||||
return (
|
return (
|
||||||
<Dialog.Outer control={props.control}>
|
<Dialog.Outer control={props.control}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
|
|
||||||
<ReportDialogInner {...props} />
|
<ReportDialogInner {...props} />
|
||||||
</Dialog.Outer>
|
</Dialog.Outer>
|
||||||
)
|
)
|
||||||
@@ -40,10 +38,7 @@ function ReportDialogInner(props: ReportDialogProps) {
|
|||||||
} = useMyLabelersQuery()
|
} = useMyLabelersQuery()
|
||||||
const isLoading = useDelayedLoading(500, isLabelerLoading)
|
const isLoading = useDelayedLoading(500, isLabelerLoading)
|
||||||
|
|
||||||
const ref = React.useRef<BottomSheetScrollViewMethods>(null)
|
const ref = React.useRef<ScrollView>(null)
|
||||||
useOnKeyboardDidShow(() => {
|
|
||||||
ref.current?.scrollToEnd({animated: true})
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.ScrollableInner label={_(msg`Report dialog`)} ref={ref}>
|
<Dialog.ScrollableInner label={_(msg`Report dialog`)} ref={ref}>
|
||||||
|
|||||||
@@ -149,7 +149,6 @@ export function QrCodeDialog({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.Outer control={control}>
|
<Dialog.Outer control={control}>
|
||||||
<Dialog.Handle />
|
|
||||||
<Dialog.ScrollableInner
|
<Dialog.ScrollableInner
|
||||||
label={_(msg`Create a QR code for a starter pack`)}>
|
label={_(msg`Create a QR code for a starter pack`)}>
|
||||||
<View style={[a.flex_1, a.align_center, a.gap_5xl]}>
|
<View style={[a.flex_1, a.align_center, a.gap_5xl]}>
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ import {AppBskyGraphDefs} from '@atproto/api'
|
|||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||||
|
import {saveImageToMediaLibrary} from '#/lib/media/manip'
|
||||||
|
import {shareUrl} from '#/lib/sharing'
|
||||||
|
import {logEvent} from '#/lib/statsig/statsig'
|
||||||
|
import {getStarterPackOgCard} from '#/lib/strings/starter-pack'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
import {isNative, isWeb} from '#/platform/detection'
|
||||||
import {saveImageToMediaLibrary} from 'lib/media/manip'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {shareUrl} from 'lib/sharing'
|
|
||||||
import {logEvent} from 'lib/statsig/statsig'
|
|
||||||
import {getStarterPackOgCard} from 'lib/strings/starter-pack'
|
|
||||||
import {isNative, isWeb} from 'platform/detection'
|
|
||||||
import * as Toast from 'view/com/util/Toast'
|
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Button, ButtonText} from '#/components/Button'
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
import {DialogControlProps} from '#/components/Dialog'
|
import {DialogControlProps} from '#/components/Dialog'
|
||||||
@@ -32,6 +32,7 @@ interface Props {
|
|||||||
export function ShareDialog(props: Props) {
|
export function ShareDialog(props: Props) {
|
||||||
return (
|
return (
|
||||||
<Dialog.Outer control={props.control}>
|
<Dialog.Outer control={props.control}>
|
||||||
|
<Dialog.Handle />
|
||||||
<ShareDialogInner {...props} />
|
<ShareDialogInner {...props} />
|
||||||
</Dialog.Outer>
|
</Dialog.Outer>
|
||||||
)
|
)
|
||||||
@@ -84,7 +85,6 @@ function ShareDialogInner({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dialog.Handle />
|
|
||||||
<Dialog.ScrollableInner label={_(msg`Share link dialog`)}>
|
<Dialog.ScrollableInner label={_(msg`Share link dialog`)}>
|
||||||
{!imageLoaded || !link ? (
|
{!imageLoaded || !link ? (
|
||||||
<View style={[a.p_xl, a.align_center]}>
|
<View style={[a.p_xl, a.align_center]}>
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import type {ListRenderItemInfo} from 'react-native'
|
|||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {AppBskyActorDefs, ModerationOpts} from '@atproto/api'
|
import {AppBskyActorDefs, ModerationOpts} from '@atproto/api'
|
||||||
import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
|
import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
|
||||||
import {BottomSheetFlatListMethods} from '@discord/bottom-sheet'
|
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
|
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
||||||
import {isWeb} from 'platform/detection'
|
import {isWeb} from '#/platform/detection'
|
||||||
import {useSession} from 'state/session'
|
import {useSession} from '#/state/session'
|
||||||
|
import {ListMethods} from '#/view/com/util/List'
|
||||||
import {WizardAction, WizardState} from '#/screens/StarterPack/Wizard/State'
|
import {WizardAction, WizardState} from '#/screens/StarterPack/Wizard/State'
|
||||||
import {atoms as a, native, useTheme, web} from '#/alf'
|
import {atoms as a, native, useTheme, web} from '#/alf'
|
||||||
import {Button, ButtonText} from '#/components/Button'
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
@@ -45,7 +45,7 @@ export function WizardEditListDialog({
|
|||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const initialNumToRender = useInitialNumToRender()
|
const initialNumToRender = useInitialNumToRender()
|
||||||
|
|
||||||
const listRef = useRef<BottomSheetFlatListMethods>(null)
|
const listRef = useRef<ListMethods>(null)
|
||||||
|
|
||||||
const getData = () => {
|
const getData = () => {
|
||||||
if (state.currentStep === 'Feeds') return state.feeds
|
if (state.currentStep === 'Feeds') return state.feeds
|
||||||
@@ -76,10 +76,7 @@ export function WizardEditListDialog({
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.Outer
|
<Dialog.Outer control={control} testID="newChatDialog">
|
||||||
control={control}
|
|
||||||
testID="newChatDialog"
|
|
||||||
nativeOptions={{sheet: {snapPoints: ['95%']}}}>
|
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
<Dialog.InnerFlatList
|
<Dialog.InnerFlatList
|
||||||
ref={listRef}
|
ref={listRef}
|
||||||
@@ -89,6 +86,7 @@ export function WizardEditListDialog({
|
|||||||
ListHeaderComponent={
|
ListHeaderComponent={
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
|
native(a.pt_4xl),
|
||||||
a.flex_row,
|
a.flex_row,
|
||||||
a.justify_between,
|
a.justify_between,
|
||||||
a.border_b,
|
a.border_b,
|
||||||
@@ -103,13 +101,7 @@ export function WizardEditListDialog({
|
|||||||
height: 48,
|
height: 48,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: [
|
: [a.pb_sm, a.align_end],
|
||||||
a.pb_sm,
|
|
||||||
a.align_end,
|
|
||||||
{
|
|
||||||
height: 68,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
]}>
|
]}>
|
||||||
<View style={{width: 60}} />
|
<View style={{width: 60}} />
|
||||||
<Text style={[a.font_bold, a.text_xl]}>
|
<Text style={[a.font_bold, a.text_xl]}>
|
||||||
@@ -143,8 +135,6 @@ export function WizardEditListDialog({
|
|||||||
paddingHorizontal: 0,
|
paddingHorizontal: 0,
|
||||||
marginTop: 0,
|
marginTop: 0,
|
||||||
paddingTop: 0,
|
paddingTop: 0,
|
||||||
borderTopLeftRadius: 40,
|
|
||||||
borderTopRightRadius: 40,
|
|
||||||
}),
|
}),
|
||||||
]}
|
]}
|
||||||
webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]}
|
webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]}
|
||||||
|
|||||||
@@ -85,7 +85,6 @@ export function TagMenu({
|
|||||||
|
|
||||||
<Dialog.Outer control={control}>
|
<Dialog.Outer control={control}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
|
|
||||||
<Dialog.Inner label={_(msg`Tag menu: ${displayTag}`)}>
|
<Dialog.Inner label={_(msg`Tag menu: ${displayTag}`)}>
|
||||||
{isPreferencesLoading ? (
|
{isPreferencesLoading ? (
|
||||||
<View style={[a.w_full, a.align_center]}>
|
<View style={[a.w_full, a.align_center]}>
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ export function BirthDateSettingsDialog({
|
|||||||
return (
|
return (
|
||||||
<Dialog.Outer control={control}>
|
<Dialog.Outer control={control}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
|
|
||||||
<Dialog.ScrollableInner label={_(msg`My Birthday`)}>
|
<Dialog.ScrollableInner label={_(msg`My Birthday`)}>
|
||||||
<View style={[a.gap_sm, a.pb_lg]}>
|
<View style={[a.gap_sm, a.pb_lg]}>
|
||||||
<Text style={[a.text_2xl, a.font_bold]}>
|
<Text style={[a.text_2xl, a.font_bold]}>
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ export function EmbedConsentDialog({
|
|||||||
return (
|
return (
|
||||||
<Dialog.Outer control={control}>
|
<Dialog.Outer control={control}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
|
|
||||||
<Dialog.ScrollableInner
|
<Dialog.ScrollableInner
|
||||||
label={_(msg`External Media`)}
|
label={_(msg`External Media`)}
|
||||||
style={[gtMobile ? {width: 'auto', maxWidth: 400} : a.w_full]}>
|
style={[gtMobile ? {width: 'auto', maxWidth: 400} : a.w_full]}>
|
||||||
|
|||||||
@@ -1,255 +0,0 @@
|
|||||||
import React, {
|
|
||||||
useCallback,
|
|
||||||
useImperativeHandle,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react'
|
|
||||||
import {Modal, ScrollView, TextInput, View} from 'react-native'
|
|
||||||
import {msg, Trans} from '@lingui/macro'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
|
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
|
||||||
import {
|
|
||||||
Gif,
|
|
||||||
useFeaturedGifsQuery,
|
|
||||||
useGifSearchQuery,
|
|
||||||
} from '#/state/queries/tenor'
|
|
||||||
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
|
|
||||||
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
|
|
||||||
import {FlatList_INTERNAL} from '#/view/com/util/Views'
|
|
||||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
|
||||||
import * as TextField from '#/components/forms/TextField'
|
|
||||||
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
|
|
||||||
import {Button, ButtonText} from '../Button'
|
|
||||||
import {Handle} from '../Dialog'
|
|
||||||
import {useThrottledValue} from '../hooks/useThrottledValue'
|
|
||||||
import {ListFooter, ListMaybePlaceholder} from '../Lists'
|
|
||||||
import {GifPreview} from './GifSelect.shared'
|
|
||||||
|
|
||||||
export function GifSelectDialog({
|
|
||||||
controlRef,
|
|
||||||
onClose,
|
|
||||||
onSelectGif: onSelectGifProp,
|
|
||||||
}: {
|
|
||||||
controlRef: React.RefObject<{open: () => void}>
|
|
||||||
onClose: () => void
|
|
||||||
onSelectGif: (gif: Gif) => void
|
|
||||||
}) {
|
|
||||||
const t = useTheme()
|
|
||||||
const [open, setOpen] = useState(false)
|
|
||||||
|
|
||||||
useImperativeHandle(controlRef, () => ({
|
|
||||||
open: () => setOpen(true),
|
|
||||||
}))
|
|
||||||
|
|
||||||
const close = useCallback(() => {
|
|
||||||
setOpen(false)
|
|
||||||
onClose()
|
|
||||||
}, [onClose])
|
|
||||||
|
|
||||||
const onSelectGif = useCallback(
|
|
||||||
(gif: Gif) => {
|
|
||||||
onSelectGifProp(gif)
|
|
||||||
close()
|
|
||||||
},
|
|
||||||
[onSelectGifProp, close],
|
|
||||||
)
|
|
||||||
|
|
||||||
const renderErrorBoundary = useCallback(
|
|
||||||
(error: any) => <ModalError details={String(error)} close={close} />,
|
|
||||||
[close],
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
visible={open}
|
|
||||||
animationType="slide"
|
|
||||||
presentationStyle="formSheet"
|
|
||||||
onRequestClose={close}
|
|
||||||
aria-modal
|
|
||||||
accessibilityViewIsModal>
|
|
||||||
<View style={[a.flex_1, t.atoms.bg]}>
|
|
||||||
<Handle />
|
|
||||||
<ErrorBoundary renderError={renderErrorBoundary}>
|
|
||||||
<GifList onSelectGif={onSelectGif} close={close} />
|
|
||||||
</ErrorBoundary>
|
|
||||||
</View>
|
|
||||||
</Modal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function GifList({
|
|
||||||
onSelectGif,
|
|
||||||
}: {
|
|
||||||
close: () => void
|
|
||||||
onSelectGif: (gif: Gif) => void
|
|
||||||
}) {
|
|
||||||
const {_} = useLingui()
|
|
||||||
const t = useTheme()
|
|
||||||
const {gtMobile} = useBreakpoints()
|
|
||||||
const textInputRef = useRef<TextInput>(null)
|
|
||||||
const listRef = useRef<FlatList_INTERNAL>(null)
|
|
||||||
const [undeferredSearch, setSearch] = useState('')
|
|
||||||
const search = useThrottledValue(undeferredSearch, 500)
|
|
||||||
|
|
||||||
const isSearching = search.length > 0
|
|
||||||
|
|
||||||
const trendingQuery = useFeaturedGifsQuery()
|
|
||||||
const searchQuery = useGifSearchQuery(search)
|
|
||||||
|
|
||||||
const {
|
|
||||||
data,
|
|
||||||
fetchNextPage,
|
|
||||||
isFetchingNextPage,
|
|
||||||
hasNextPage,
|
|
||||||
error,
|
|
||||||
isLoading,
|
|
||||||
isError,
|
|
||||||
refetch,
|
|
||||||
} = isSearching ? searchQuery : trendingQuery
|
|
||||||
|
|
||||||
const flattenedData = useMemo(() => {
|
|
||||||
return data?.pages.flatMap(page => page.results) || []
|
|
||||||
}, [data])
|
|
||||||
|
|
||||||
const renderItem = useCallback(
|
|
||||||
({item}: {item: Gif}) => {
|
|
||||||
return <GifPreview gif={item} onSelectGif={onSelectGif} />
|
|
||||||
},
|
|
||||||
[onSelectGif],
|
|
||||||
)
|
|
||||||
|
|
||||||
const onEndReached = React.useCallback(() => {
|
|
||||||
if (isFetchingNextPage || !hasNextPage || error) return
|
|
||||||
fetchNextPage()
|
|
||||||
}, [isFetchingNextPage, hasNextPage, error, fetchNextPage])
|
|
||||||
|
|
||||||
const hasData = flattenedData.length > 0
|
|
||||||
|
|
||||||
const onGoBack = useCallback(() => {
|
|
||||||
if (isSearching) {
|
|
||||||
// clear the input and reset the state
|
|
||||||
textInputRef.current?.clear()
|
|
||||||
setSearch('')
|
|
||||||
} else {
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
}, [isSearching])
|
|
||||||
|
|
||||||
const listHeader = useMemo(() => {
|
|
||||||
return (
|
|
||||||
<View style={[a.relative, a.mb_lg, a.pt_4xl, a.flex_row, a.align_center]}>
|
|
||||||
{/* cover top corners */}
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.absolute,
|
|
||||||
a.inset_0,
|
|
||||||
{
|
|
||||||
borderBottomLeftRadius: 8,
|
|
||||||
borderBottomRightRadius: 8,
|
|
||||||
},
|
|
||||||
t.atoms.bg,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField.Root>
|
|
||||||
<TextField.Icon icon={Search} />
|
|
||||||
<TextField.Input
|
|
||||||
label={_(msg`Search GIFs`)}
|
|
||||||
placeholder={_(msg`Search Tenor`)}
|
|
||||||
onChangeText={text => {
|
|
||||||
setSearch(text)
|
|
||||||
listRef.current?.scrollToOffset({offset: 0, animated: false})
|
|
||||||
}}
|
|
||||||
returnKeyType="search"
|
|
||||||
clearButtonMode="while-editing"
|
|
||||||
inputRef={textInputRef}
|
|
||||||
maxLength={50}
|
|
||||||
/>
|
|
||||||
</TextField.Root>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}, [t.atoms.bg, _])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FlatList_INTERNAL
|
|
||||||
ref={listRef}
|
|
||||||
key={gtMobile ? '3 cols' : '2 cols'}
|
|
||||||
data={flattenedData}
|
|
||||||
renderItem={renderItem}
|
|
||||||
numColumns={gtMobile ? 3 : 2}
|
|
||||||
columnWrapperStyle={a.gap_sm}
|
|
||||||
contentContainerStyle={a.px_lg}
|
|
||||||
ListHeaderComponent={
|
|
||||||
<>
|
|
||||||
{listHeader}
|
|
||||||
{!hasData && (
|
|
||||||
<ListMaybePlaceholder
|
|
||||||
isLoading={isLoading}
|
|
||||||
isError={isError}
|
|
||||||
onRetry={refetch}
|
|
||||||
onGoBack={onGoBack}
|
|
||||||
emptyType="results"
|
|
||||||
sideBorders={false}
|
|
||||||
topBorder={false}
|
|
||||||
errorTitle={_(msg`Failed to load GIFs`)}
|
|
||||||
errorMessage={_(msg`There was an issue connecting to Tenor.`)}
|
|
||||||
emptyMessage={
|
|
||||||
isSearching
|
|
||||||
? _(msg`No search results found for "${search}".`)
|
|
||||||
: _(
|
|
||||||
msg`No featured GIFs found. There may be an issue with Tenor.`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
stickyHeaderIndices={[0]}
|
|
||||||
onEndReached={onEndReached}
|
|
||||||
onEndReachedThreshold={4}
|
|
||||||
keyExtractor={(item: Gif) => item.id}
|
|
||||||
keyboardDismissMode="on-drag"
|
|
||||||
ListFooterComponent={
|
|
||||||
hasData ? (
|
|
||||||
<ListFooter
|
|
||||||
isFetchingNextPage={isFetchingNextPage}
|
|
||||||
error={cleanError(error)}
|
|
||||||
onRetry={fetchNextPage}
|
|
||||||
style={{borderTopWidth: 0}}
|
|
||||||
/>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ModalError({details, close}: {details?: string; close: () => void}) {
|
|
||||||
const {_} = useLingui()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ScrollView
|
|
||||||
style={[a.flex_1, a.gap_md]}
|
|
||||||
centerContent
|
|
||||||
contentContainerStyle={a.px_lg}>
|
|
||||||
<ErrorScreen
|
|
||||||
title={_(msg`Oh no!`)}
|
|
||||||
message={_(
|
|
||||||
msg`There was an unexpected issue in the application. Please let us know if this happened to you!`,
|
|
||||||
)}
|
|
||||||
details={details}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
label={_(msg`Close dialog`)}
|
|
||||||
onPress={close}
|
|
||||||
color="primary"
|
|
||||||
size="large"
|
|
||||||
variant="solid">
|
|
||||||
<ButtonText>
|
|
||||||
<Trans>Close</Trans>
|
|
||||||
</ButtonText>
|
|
||||||
</Button>
|
|
||||||
</ScrollView>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import React, {useCallback} from 'react'
|
|
||||||
import {Image} from 'expo-image'
|
|
||||||
import {msg} from '@lingui/macro'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
|
|
||||||
import {logEvent} from '#/lib/statsig/statsig'
|
|
||||||
import {Gif} from '#/state/queries/tenor'
|
|
||||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
|
||||||
import {Button} from '../Button'
|
|
||||||
|
|
||||||
export function GifPreview({
|
|
||||||
gif,
|
|
||||||
onSelectGif,
|
|
||||||
}: {
|
|
||||||
gif: Gif
|
|
||||||
onSelectGif: (gif: Gif) => void
|
|
||||||
}) {
|
|
||||||
const {gtTablet} = useBreakpoints()
|
|
||||||
const {_} = useLingui()
|
|
||||||
const t = useTheme()
|
|
||||||
|
|
||||||
const onPress = useCallback(() => {
|
|
||||||
logEvent('composer:gif:select', {})
|
|
||||||
onSelectGif(gif)
|
|
||||||
}, [onSelectGif, gif])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
label={_(msg`Select GIF "${gif.title}"`)}
|
|
||||||
style={[a.flex_1, gtTablet ? {maxWidth: '33%'} : {maxWidth: '50%'}]}
|
|
||||||
onPress={onPress}>
|
|
||||||
{({pressed}) => (
|
|
||||||
<Image
|
|
||||||
style={[
|
|
||||||
a.flex_1,
|
|
||||||
a.mb_sm,
|
|
||||||
a.rounded_sm,
|
|
||||||
{aspectRatio: 1, opacity: pressed ? 0.8 : 1},
|
|
||||||
t.atoms.bg_contrast_25,
|
|
||||||
]}
|
|
||||||
source={{
|
|
||||||
uri: gif.media_formats.tinygif.url,
|
|
||||||
}}
|
|
||||||
contentFit="cover"
|
|
||||||
accessibilityLabel={gif.title}
|
|
||||||
accessibilityHint=""
|
|
||||||
cachePolicy="none"
|
|
||||||
accessibilityIgnoresInvertColors
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -6,10 +6,12 @@ import React, {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import {TextInput, View} from 'react-native'
|
import {TextInput, View} from 'react-native'
|
||||||
import {BottomSheetFlatListMethods} from '@discord/bottom-sheet'
|
import {useWindowDimensions} from 'react-native'
|
||||||
|
import {Image} from 'expo-image'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {logEvent} from '#/lib/statsig/statsig'
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
import {isWeb} from '#/platform/detection'
|
import {isWeb} from '#/platform/detection'
|
||||||
import {
|
import {
|
||||||
@@ -19,7 +21,8 @@ import {
|
|||||||
} from '#/state/queries/tenor'
|
} from '#/state/queries/tenor'
|
||||||
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
|
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
|
||||||
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
|
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
|
||||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
import {ListMethods} from '#/view/com/util/List'
|
||||||
|
import {atoms as a, ios, native, useBreakpoints, useTheme} from '#/alf'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import * as TextField from '#/components/forms/TextField'
|
import * as TextField from '#/components/forms/TextField'
|
||||||
import {useThrottledValue} from '#/components/hooks/useThrottledValue'
|
import {useThrottledValue} from '#/components/hooks/useThrottledValue'
|
||||||
@@ -27,16 +30,18 @@ import {ArrowLeft_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arr
|
|||||||
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
|
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
|
||||||
import {Button, ButtonIcon, ButtonText} from '../Button'
|
import {Button, ButtonIcon, ButtonText} from '../Button'
|
||||||
import {ListFooter, ListMaybePlaceholder} from '../Lists'
|
import {ListFooter, ListMaybePlaceholder} from '../Lists'
|
||||||
import {GifPreview} from './GifSelect.shared'
|
import {PortalComponent} from '../Portal'
|
||||||
|
|
||||||
export function GifSelectDialog({
|
export function GifSelectDialog({
|
||||||
controlRef,
|
controlRef,
|
||||||
onClose,
|
onClose,
|
||||||
onSelectGif: onSelectGifProp,
|
onSelectGif: onSelectGifProp,
|
||||||
|
Portal,
|
||||||
}: {
|
}: {
|
||||||
controlRef: React.RefObject<{open: () => void}>
|
controlRef: React.RefObject<{open: () => void}>
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onSelectGif: (gif: Gif) => void
|
onSelectGif: (gif: Gif) => void
|
||||||
|
Portal?: PortalComponent
|
||||||
}) {
|
}) {
|
||||||
const control = Dialog.useDialogControl()
|
const control = Dialog.useDialogControl()
|
||||||
|
|
||||||
@@ -59,8 +64,13 @@ export function GifSelectDialog({
|
|||||||
return (
|
return (
|
||||||
<Dialog.Outer
|
<Dialog.Outer
|
||||||
control={control}
|
control={control}
|
||||||
nativeOptions={{sheet: {snapPoints: ['100%']}}}
|
onClose={onClose}
|
||||||
onClose={onClose}>
|
Portal={Portal}
|
||||||
|
nativeOptions={{
|
||||||
|
bottomInset: 0,
|
||||||
|
// use system corner radius on iOS
|
||||||
|
...ios({cornerRadius: undefined}),
|
||||||
|
}}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
<ErrorBoundary renderError={renderErrorBoundary}>
|
<ErrorBoundary renderError={renderErrorBoundary}>
|
||||||
<GifList control={control} onSelectGif={onSelectGif} />
|
<GifList control={control} onSelectGif={onSelectGif} />
|
||||||
@@ -80,9 +90,10 @@ function GifList({
|
|||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
const textInputRef = useRef<TextInput>(null)
|
const textInputRef = useRef<TextInput>(null)
|
||||||
const listRef = useRef<BottomSheetFlatListMethods>(null)
|
const listRef = useRef<ListMethods>(null)
|
||||||
const [undeferredSearch, setSearch] = useState('')
|
const [undeferredSearch, setSearch] = useState('')
|
||||||
const search = useThrottledValue(undeferredSearch, 500)
|
const search = useThrottledValue(undeferredSearch, 500)
|
||||||
|
const {height} = useWindowDimensions()
|
||||||
|
|
||||||
const isSearching = search.length > 0
|
const isSearching = search.length > 0
|
||||||
|
|
||||||
@@ -95,7 +106,7 @@ function GifList({
|
|||||||
isFetchingNextPage,
|
isFetchingNextPage,
|
||||||
hasNextPage,
|
hasNextPage,
|
||||||
error,
|
error,
|
||||||
isLoading,
|
isPending,
|
||||||
isError,
|
isError,
|
||||||
refetch,
|
refetch,
|
||||||
} = isSearching ? searchQuery : trendingQuery
|
} = isSearching ? searchQuery : trendingQuery
|
||||||
@@ -132,6 +143,7 @@ function GifList({
|
|||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
|
native(a.pt_4xl),
|
||||||
a.relative,
|
a.relative,
|
||||||
a.mb_lg,
|
a.mb_lg,
|
||||||
a.flex_row,
|
a.flex_row,
|
||||||
@@ -196,13 +208,14 @@ function GifList({
|
|||||||
data={flattenedData}
|
data={flattenedData}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
numColumns={gtMobile ? 3 : 2}
|
numColumns={gtMobile ? 3 : 2}
|
||||||
columnWrapperStyle={a.gap_sm}
|
columnWrapperStyle={[a.gap_sm]}
|
||||||
|
contentContainerStyle={[native([a.px_xl, {minHeight: height}])]}
|
||||||
ListHeaderComponent={
|
ListHeaderComponent={
|
||||||
<>
|
<>
|
||||||
{listHeader}
|
{listHeader}
|
||||||
{!hasData && (
|
{!hasData && (
|
||||||
<ListMaybePlaceholder
|
<ListMaybePlaceholder
|
||||||
isLoading={isLoading}
|
isLoading={isPending}
|
||||||
isError={isError}
|
isError={isError}
|
||||||
onRetry={refetch}
|
onRetry={refetch}
|
||||||
onGoBack={onGoBack}
|
onGoBack={onGoBack}
|
||||||
@@ -273,3 +286,47 @@ function DialogError({details}: {details?: string}) {
|
|||||||
</Dialog.ScrollableInner>
|
</Dialog.ScrollableInner>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function GifPreview({
|
||||||
|
gif,
|
||||||
|
onSelectGif,
|
||||||
|
}: {
|
||||||
|
gif: Gif
|
||||||
|
onSelectGif: (gif: Gif) => void
|
||||||
|
}) {
|
||||||
|
const {gtTablet} = useBreakpoints()
|
||||||
|
const {_} = useLingui()
|
||||||
|
const t = useTheme()
|
||||||
|
|
||||||
|
const onPress = useCallback(() => {
|
||||||
|
logEvent('composer:gif:select', {})
|
||||||
|
onSelectGif(gif)
|
||||||
|
}, [onSelectGif, gif])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
label={_(msg`Select GIF "${gif.title}"`)}
|
||||||
|
style={[a.flex_1, gtTablet ? {maxWidth: '33%'} : {maxWidth: '50%'}]}
|
||||||
|
onPress={onPress}>
|
||||||
|
{({pressed}) => (
|
||||||
|
<Image
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.mb_sm,
|
||||||
|
a.rounded_sm,
|
||||||
|
{aspectRatio: 1, opacity: pressed ? 0.8 : 1},
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
]}
|
||||||
|
source={{
|
||||||
|
uri: gif.media_formats.tinygif.url,
|
||||||
|
}}
|
||||||
|
contentFit="cover"
|
||||||
|
accessibilityLabel={gif.title}
|
||||||
|
accessibilityHint=""
|
||||||
|
cachePolicy="none"
|
||||||
|
accessibilityIgnoresInvertColors
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,11 +30,14 @@ import {PageText_Stroke2_Corner0_Rounded as PageText} from '#/components/icons/P
|
|||||||
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
|
import {createPortalGroup} from '#/components/Portal'
|
||||||
import * as Prompt from '#/components/Prompt'
|
import * as Prompt from '#/components/Prompt'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
const ONE_DAY = 24 * 60 * 60 * 1000
|
const ONE_DAY = 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
|
const Portal = createPortalGroup()
|
||||||
|
|
||||||
export function MutedWordsDialog() {
|
export function MutedWordsDialog() {
|
||||||
const {mutedWordsDialogControl: control} = useGlobalDialogsControlContext()
|
const {mutedWordsDialogControl: control} = useGlobalDialogsControlContext()
|
||||||
return (
|
return (
|
||||||
@@ -105,307 +108,349 @@ function MutedWordsInner() {
|
|||||||
}, [_, field, targets, addMutedWord, setField, durations, excludeFollowing])
|
}, [_, field, targets, addMutedWord, setField, durations, excludeFollowing])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.ScrollableInner label={_(msg`Manage your muted words and tags`)}>
|
<Portal.Provider>
|
||||||
<View>
|
<Dialog.ScrollableInner label={_(msg`Manage your muted words and tags`)}>
|
||||||
<Text
|
<View>
|
||||||
style={[a.text_md, a.font_bold, a.pb_sm, t.atoms.text_contrast_high]}>
|
|
||||||
<Trans>Add muted words and tags</Trans>
|
|
||||||
</Text>
|
|
||||||
<Text style={[a.pb_lg, a.leading_snug, t.atoms.text_contrast_medium]}>
|
|
||||||
<Trans>
|
|
||||||
Posts can be muted based on their text, their tags, or both. We
|
|
||||||
recommend avoiding common words that appear in many posts, since it
|
|
||||||
can result in no posts being shown.
|
|
||||||
</Trans>
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<View style={[a.pb_sm]}>
|
|
||||||
<Dialog.Input
|
|
||||||
autoCorrect={false}
|
|
||||||
autoCapitalize="none"
|
|
||||||
autoComplete="off"
|
|
||||||
label={_(msg`Enter a word or tag`)}
|
|
||||||
placeholder={_(msg`Enter a word or tag`)}
|
|
||||||
value={field}
|
|
||||||
onChangeText={value => {
|
|
||||||
if (error) {
|
|
||||||
setError('')
|
|
||||||
}
|
|
||||||
setField(value)
|
|
||||||
}}
|
|
||||||
onSubmitEditing={submit}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={[a.pb_xl, a.gap_sm]}>
|
|
||||||
<Toggle.Group
|
|
||||||
label={_(msg`Select how long to mute this word for.`)}
|
|
||||||
type="radio"
|
|
||||||
values={durations}
|
|
||||||
onChange={setDurations}>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.pb_xs,
|
|
||||||
a.text_sm,
|
|
||||||
a.font_bold,
|
|
||||||
t.atoms.text_contrast_medium,
|
|
||||||
]}>
|
|
||||||
<Trans>Duration:</Trans>
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
gtMobile && [a.flex_row, a.align_center, a.justify_start],
|
|
||||||
a.gap_sm,
|
|
||||||
]}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.flex_1,
|
|
||||||
a.flex_row,
|
|
||||||
a.justify_start,
|
|
||||||
a.align_center,
|
|
||||||
a.gap_sm,
|
|
||||||
]}>
|
|
||||||
<Toggle.Item
|
|
||||||
label={_(msg`Mute this word until you unmute it`)}
|
|
||||||
name="forever"
|
|
||||||
style={[a.flex_1]}>
|
|
||||||
<TargetToggle>
|
|
||||||
<View
|
|
||||||
style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
|
|
||||||
<Toggle.Radio />
|
|
||||||
<Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
|
|
||||||
<Trans>Forever</Trans>
|
|
||||||
</Toggle.LabelText>
|
|
||||||
</View>
|
|
||||||
</TargetToggle>
|
|
||||||
</Toggle.Item>
|
|
||||||
|
|
||||||
<Toggle.Item
|
|
||||||
label={_(msg`Mute this word for 24 hours`)}
|
|
||||||
name="24_hours"
|
|
||||||
style={[a.flex_1]}>
|
|
||||||
<TargetToggle>
|
|
||||||
<View
|
|
||||||
style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
|
|
||||||
<Toggle.Radio />
|
|
||||||
<Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
|
|
||||||
<Trans>24 hours</Trans>
|
|
||||||
</Toggle.LabelText>
|
|
||||||
</View>
|
|
||||||
</TargetToggle>
|
|
||||||
</Toggle.Item>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.flex_1,
|
|
||||||
a.flex_row,
|
|
||||||
a.justify_start,
|
|
||||||
a.align_center,
|
|
||||||
a.gap_sm,
|
|
||||||
]}>
|
|
||||||
<Toggle.Item
|
|
||||||
label={_(msg`Mute this word for 7 days`)}
|
|
||||||
name="7_days"
|
|
||||||
style={[a.flex_1]}>
|
|
||||||
<TargetToggle>
|
|
||||||
<View
|
|
||||||
style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
|
|
||||||
<Toggle.Radio />
|
|
||||||
<Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
|
|
||||||
<Trans>7 days</Trans>
|
|
||||||
</Toggle.LabelText>
|
|
||||||
</View>
|
|
||||||
</TargetToggle>
|
|
||||||
</Toggle.Item>
|
|
||||||
|
|
||||||
<Toggle.Item
|
|
||||||
label={_(msg`Mute this word for 30 days`)}
|
|
||||||
name="30_days"
|
|
||||||
style={[a.flex_1]}>
|
|
||||||
<TargetToggle>
|
|
||||||
<View
|
|
||||||
style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
|
|
||||||
<Toggle.Radio />
|
|
||||||
<Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
|
|
||||||
<Trans>30 days</Trans>
|
|
||||||
</Toggle.LabelText>
|
|
||||||
</View>
|
|
||||||
</TargetToggle>
|
|
||||||
</Toggle.Item>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</Toggle.Group>
|
|
||||||
|
|
||||||
<Toggle.Group
|
|
||||||
label={_(msg`Select what content this mute word should apply to.`)}
|
|
||||||
type="radio"
|
|
||||||
values={targets}
|
|
||||||
onChange={setTargets}>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.pb_xs,
|
|
||||||
a.text_sm,
|
|
||||||
a.font_bold,
|
|
||||||
t.atoms.text_contrast_medium,
|
|
||||||
]}>
|
|
||||||
<Trans>Mute in:</Trans>
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<View style={[a.flex_row, a.align_center, a.gap_sm, a.flex_wrap]}>
|
|
||||||
<Toggle.Item
|
|
||||||
label={_(msg`Mute this word in post text and tags`)}
|
|
||||||
name="content"
|
|
||||||
style={[a.flex_1]}>
|
|
||||||
<TargetToggle>
|
|
||||||
<View
|
|
||||||
style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
|
|
||||||
<Toggle.Radio />
|
|
||||||
<Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
|
|
||||||
<Trans>Text & tags</Trans>
|
|
||||||
</Toggle.LabelText>
|
|
||||||
</View>
|
|
||||||
<PageText size="sm" />
|
|
||||||
</TargetToggle>
|
|
||||||
</Toggle.Item>
|
|
||||||
|
|
||||||
<Toggle.Item
|
|
||||||
label={_(msg`Mute this word in tags only`)}
|
|
||||||
name="tag"
|
|
||||||
style={[a.flex_1]}>
|
|
||||||
<TargetToggle>
|
|
||||||
<View
|
|
||||||
style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
|
|
||||||
<Toggle.Radio />
|
|
||||||
<Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
|
|
||||||
<Trans>Tags only</Trans>
|
|
||||||
</Toggle.LabelText>
|
|
||||||
</View>
|
|
||||||
<Hashtag size="sm" />
|
|
||||||
</TargetToggle>
|
|
||||||
</Toggle.Item>
|
|
||||||
</View>
|
|
||||||
</Toggle.Group>
|
|
||||||
|
|
||||||
<View>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.pb_xs,
|
|
||||||
a.text_sm,
|
|
||||||
a.font_bold,
|
|
||||||
t.atoms.text_contrast_medium,
|
|
||||||
]}>
|
|
||||||
<Trans>Options:</Trans>
|
|
||||||
</Text>
|
|
||||||
<Toggle.Item
|
|
||||||
label={_(msg`Do not apply this mute word to users you follow`)}
|
|
||||||
name="exclude_following"
|
|
||||||
style={[a.flex_row, a.justify_between]}
|
|
||||||
value={excludeFollowing}
|
|
||||||
onChange={setExcludeFollowing}>
|
|
||||||
<TargetToggle>
|
|
||||||
<View style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
|
|
||||||
<Toggle.Checkbox />
|
|
||||||
<Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
|
|
||||||
<Trans>Exclude users you follow</Trans>
|
|
||||||
</Toggle.LabelText>
|
|
||||||
</View>
|
|
||||||
</TargetToggle>
|
|
||||||
</Toggle.Item>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={[a.pt_xs]}>
|
|
||||||
<Button
|
|
||||||
disabled={isPending || !field}
|
|
||||||
label={_(msg`Add mute word for configured settings`)}
|
|
||||||
size="large"
|
|
||||||
color="primary"
|
|
||||||
variant="solid"
|
|
||||||
style={[]}
|
|
||||||
onPress={submit}>
|
|
||||||
<ButtonText>
|
|
||||||
<Trans>Add</Trans>
|
|
||||||
</ButtonText>
|
|
||||||
<ButtonIcon icon={isPending ? Loader : Plus} position="right" />
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.mb_lg,
|
|
||||||
a.flex_row,
|
|
||||||
a.rounded_sm,
|
|
||||||
a.p_md,
|
|
||||||
a.mb_xs,
|
|
||||||
t.atoms.bg_contrast_25,
|
|
||||||
{
|
|
||||||
backgroundColor: t.palette.negative_400,
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.italic,
|
|
||||||
{color: t.palette.white},
|
|
||||||
native({marginTop: 2}),
|
|
||||||
]}>
|
|
||||||
{error}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<View style={[a.pt_2xl]}>
|
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
a.text_md,
|
a.text_md,
|
||||||
a.font_bold,
|
a.font_bold,
|
||||||
a.pb_md,
|
a.pb_sm,
|
||||||
t.atoms.text_contrast_high,
|
t.atoms.text_contrast_high,
|
||||||
]}>
|
]}>
|
||||||
<Trans>Your muted words</Trans>
|
<Trans>Add muted words and tags</Trans>
|
||||||
|
</Text>
|
||||||
|
<Text style={[a.pb_lg, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||||
|
<Trans>
|
||||||
|
Posts can be muted based on their text, their tags, or both. We
|
||||||
|
recommend avoiding common words that appear in many posts, since
|
||||||
|
it can result in no posts being shown.
|
||||||
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{isPreferencesLoading ? (
|
<View style={[a.pb_sm]}>
|
||||||
<Loader />
|
<Dialog.Input
|
||||||
) : preferencesError || !preferences ? (
|
autoCorrect={false}
|
||||||
<View
|
autoCapitalize="none"
|
||||||
style={[a.py_md, a.px_lg, a.rounded_md, t.atoms.bg_contrast_25]}>
|
autoComplete="off"
|
||||||
<Text style={[a.italic, t.atoms.text_contrast_high]}>
|
label={_(msg`Enter a word or tag`)}
|
||||||
<Trans>
|
placeholder={_(msg`Enter a word or tag`)}
|
||||||
We're sorry, but we weren't able to load your muted words at
|
value={field}
|
||||||
this time. Please try again.
|
onChangeText={value => {
|
||||||
</Trans>
|
if (error) {
|
||||||
|
setError('')
|
||||||
|
}
|
||||||
|
setField(value)
|
||||||
|
}}
|
||||||
|
onSubmitEditing={submit}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={[a.pb_xl, a.gap_sm]}>
|
||||||
|
<Toggle.Group
|
||||||
|
label={_(msg`Select how long to mute this word for.`)}
|
||||||
|
type="radio"
|
||||||
|
values={durations}
|
||||||
|
onChange={setDurations}>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.pb_xs,
|
||||||
|
a.text_sm,
|
||||||
|
a.font_bold,
|
||||||
|
t.atoms.text_contrast_medium,
|
||||||
|
]}>
|
||||||
|
<Trans>Duration:</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
|
||||||
) : preferences.moderationPrefs.mutedWords.length ? (
|
<View
|
||||||
[...preferences.moderationPrefs.mutedWords]
|
style={[
|
||||||
.reverse()
|
gtMobile && [a.flex_row, a.align_center, a.justify_start],
|
||||||
.map((word, i) => (
|
a.gap_sm,
|
||||||
<MutedWordRow
|
]}>
|
||||||
key={word.value + i}
|
<View
|
||||||
word={word}
|
style={[
|
||||||
style={[i % 2 === 0 && t.atoms.bg_contrast_25]}
|
a.flex_1,
|
||||||
/>
|
a.flex_row,
|
||||||
))
|
a.justify_start,
|
||||||
) : (
|
a.align_center,
|
||||||
<View
|
a.gap_sm,
|
||||||
style={[a.py_md, a.px_lg, a.rounded_md, t.atoms.bg_contrast_25]}>
|
]}>
|
||||||
<Text style={[a.italic, t.atoms.text_contrast_high]}>
|
<Toggle.Item
|
||||||
<Trans>You haven't muted any words or tags yet</Trans>
|
label={_(msg`Mute this word until you unmute it`)}
|
||||||
|
name="forever"
|
||||||
|
style={[a.flex_1]}>
|
||||||
|
<TargetToggle>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.gap_sm,
|
||||||
|
]}>
|
||||||
|
<Toggle.Radio />
|
||||||
|
<Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
|
||||||
|
<Trans>Forever</Trans>
|
||||||
|
</Toggle.LabelText>
|
||||||
|
</View>
|
||||||
|
</TargetToggle>
|
||||||
|
</Toggle.Item>
|
||||||
|
|
||||||
|
<Toggle.Item
|
||||||
|
label={_(msg`Mute this word for 24 hours`)}
|
||||||
|
name="24_hours"
|
||||||
|
style={[a.flex_1]}>
|
||||||
|
<TargetToggle>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.gap_sm,
|
||||||
|
]}>
|
||||||
|
<Toggle.Radio />
|
||||||
|
<Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
|
||||||
|
<Trans>24 hours</Trans>
|
||||||
|
</Toggle.LabelText>
|
||||||
|
</View>
|
||||||
|
</TargetToggle>
|
||||||
|
</Toggle.Item>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.flex_row,
|
||||||
|
a.justify_start,
|
||||||
|
a.align_center,
|
||||||
|
a.gap_sm,
|
||||||
|
]}>
|
||||||
|
<Toggle.Item
|
||||||
|
label={_(msg`Mute this word for 7 days`)}
|
||||||
|
name="7_days"
|
||||||
|
style={[a.flex_1]}>
|
||||||
|
<TargetToggle>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.gap_sm,
|
||||||
|
]}>
|
||||||
|
<Toggle.Radio />
|
||||||
|
<Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
|
||||||
|
<Trans>7 days</Trans>
|
||||||
|
</Toggle.LabelText>
|
||||||
|
</View>
|
||||||
|
</TargetToggle>
|
||||||
|
</Toggle.Item>
|
||||||
|
|
||||||
|
<Toggle.Item
|
||||||
|
label={_(msg`Mute this word for 30 days`)}
|
||||||
|
name="30_days"
|
||||||
|
style={[a.flex_1]}>
|
||||||
|
<TargetToggle>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.gap_sm,
|
||||||
|
]}>
|
||||||
|
<Toggle.Radio />
|
||||||
|
<Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
|
||||||
|
<Trans>30 days</Trans>
|
||||||
|
</Toggle.LabelText>
|
||||||
|
</View>
|
||||||
|
</TargetToggle>
|
||||||
|
</Toggle.Item>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Toggle.Group>
|
||||||
|
|
||||||
|
<Toggle.Group
|
||||||
|
label={_(
|
||||||
|
msg`Select what content this mute word should apply to.`,
|
||||||
|
)}
|
||||||
|
type="radio"
|
||||||
|
values={targets}
|
||||||
|
onChange={setTargets}>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.pb_xs,
|
||||||
|
a.text_sm,
|
||||||
|
a.font_bold,
|
||||||
|
t.atoms.text_contrast_medium,
|
||||||
|
]}>
|
||||||
|
<Trans>Mute in:</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
|
<View style={[a.flex_row, a.align_center, a.gap_sm, a.flex_wrap]}>
|
||||||
|
<Toggle.Item
|
||||||
|
label={_(msg`Mute this word in post text and tags`)}
|
||||||
|
name="content"
|
||||||
|
style={[a.flex_1]}>
|
||||||
|
<TargetToggle>
|
||||||
|
<View
|
||||||
|
style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
|
||||||
|
<Toggle.Radio />
|
||||||
|
<Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
|
||||||
|
<Trans>Text & tags</Trans>
|
||||||
|
</Toggle.LabelText>
|
||||||
|
</View>
|
||||||
|
<PageText size="sm" />
|
||||||
|
</TargetToggle>
|
||||||
|
</Toggle.Item>
|
||||||
|
|
||||||
|
<Toggle.Item
|
||||||
|
label={_(msg`Mute this word in tags only`)}
|
||||||
|
name="tag"
|
||||||
|
style={[a.flex_1]}>
|
||||||
|
<TargetToggle>
|
||||||
|
<View
|
||||||
|
style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
|
||||||
|
<Toggle.Radio />
|
||||||
|
<Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
|
||||||
|
<Trans>Tags only</Trans>
|
||||||
|
</Toggle.LabelText>
|
||||||
|
</View>
|
||||||
|
<Hashtag size="sm" />
|
||||||
|
</TargetToggle>
|
||||||
|
</Toggle.Item>
|
||||||
|
</View>
|
||||||
|
</Toggle.Group>
|
||||||
|
|
||||||
|
<View>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.pb_xs,
|
||||||
|
a.text_sm,
|
||||||
|
a.font_bold,
|
||||||
|
t.atoms.text_contrast_medium,
|
||||||
|
]}>
|
||||||
|
<Trans>Options:</Trans>
|
||||||
|
</Text>
|
||||||
|
<Toggle.Item
|
||||||
|
label={_(msg`Do not apply this mute word to users you follow`)}
|
||||||
|
name="exclude_following"
|
||||||
|
style={[a.flex_row, a.justify_between]}
|
||||||
|
value={excludeFollowing}
|
||||||
|
onChange={setExcludeFollowing}>
|
||||||
|
<TargetToggle>
|
||||||
|
<View
|
||||||
|
style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
|
||||||
|
<Toggle.Checkbox />
|
||||||
|
<Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
|
||||||
|
<Trans>Exclude users you follow</Trans>
|
||||||
|
</Toggle.LabelText>
|
||||||
|
</View>
|
||||||
|
</TargetToggle>
|
||||||
|
</Toggle.Item>
|
||||||
</View>
|
</View>
|
||||||
)}
|
|
||||||
|
<View style={[a.pt_xs]}>
|
||||||
|
<Button
|
||||||
|
disabled={isPending || !field}
|
||||||
|
label={_(msg`Add mute word for configured settings`)}
|
||||||
|
size="large"
|
||||||
|
color="primary"
|
||||||
|
variant="solid"
|
||||||
|
style={[]}
|
||||||
|
onPress={submit}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Add</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
<ButtonIcon icon={isPending ? Loader : Plus} position="right" />
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.mb_lg,
|
||||||
|
a.flex_row,
|
||||||
|
a.rounded_sm,
|
||||||
|
a.p_md,
|
||||||
|
a.mb_xs,
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
{
|
||||||
|
backgroundColor: t.palette.negative_400,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.italic,
|
||||||
|
{color: t.palette.white},
|
||||||
|
native({marginTop: 2}),
|
||||||
|
]}>
|
||||||
|
{error}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<View style={[a.pt_2xl]}>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.text_md,
|
||||||
|
a.font_bold,
|
||||||
|
a.pb_md,
|
||||||
|
t.atoms.text_contrast_high,
|
||||||
|
]}>
|
||||||
|
<Trans>Your muted words</Trans>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{isPreferencesLoading ? (
|
||||||
|
<Loader />
|
||||||
|
) : preferencesError || !preferences ? (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.py_md,
|
||||||
|
a.px_lg,
|
||||||
|
a.rounded_md,
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
]}>
|
||||||
|
<Text style={[a.italic, t.atoms.text_contrast_high]}>
|
||||||
|
<Trans>
|
||||||
|
We're sorry, but we weren't able to load your muted words at
|
||||||
|
this time. Please try again.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : preferences.moderationPrefs.mutedWords.length ? (
|
||||||
|
[...preferences.moderationPrefs.mutedWords]
|
||||||
|
.reverse()
|
||||||
|
.map((word, i) => (
|
||||||
|
<MutedWordRow
|
||||||
|
key={word.value + i}
|
||||||
|
word={word}
|
||||||
|
style={[i % 2 === 0 && t.atoms.bg_contrast_25]}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.py_md,
|
||||||
|
a.px_lg,
|
||||||
|
a.rounded_md,
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
]}>
|
||||||
|
<Text style={[a.italic, t.atoms.text_contrast_high]}>
|
||||||
|
<Trans>You haven't muted any words or tags yet</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{isNative && <View style={{height: 20}} />}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{isNative && <View style={{height: 20}} />}
|
<Dialog.Close />
|
||||||
</View>
|
</Dialog.ScrollableInner>
|
||||||
|
|
||||||
<Dialog.Close />
|
<Portal.Outlet />
|
||||||
</Dialog.ScrollableInner>
|
</Portal.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,6 +482,7 @@ function MutedWordRow({
|
|||||||
onConfirm={remove}
|
onConfirm={remove}
|
||||||
confirmButtonCta={_(msg`Remove`)}
|
confirmButtonCta={_(msg`Remove`)}
|
||||||
confirmButtonColor="negative"
|
confirmButtonColor="negative"
|
||||||
|
Portal={Portal.Portal}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import * as Toggle from '#/components/forms/Toggle'
|
|||||||
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||||
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
|
import {PortalComponent} from '#/components/Portal'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
export type PostInteractionSettingsFormProps = {
|
export type PostInteractionSettingsFormProps = {
|
||||||
@@ -54,13 +55,15 @@ export type PostInteractionSettingsFormProps = {
|
|||||||
|
|
||||||
export function PostInteractionSettingsControlledDialog({
|
export function PostInteractionSettingsControlledDialog({
|
||||||
control,
|
control,
|
||||||
|
Portal,
|
||||||
...rest
|
...rest
|
||||||
}: PostInteractionSettingsFormProps & {
|
}: PostInteractionSettingsFormProps & {
|
||||||
control: Dialog.DialogControlProps
|
control: Dialog.DialogControlProps
|
||||||
|
Portal?: PortalComponent
|
||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
return (
|
return (
|
||||||
<Dialog.Outer control={control}>
|
<Dialog.Outer control={control} Portal={Portal}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
<Dialog.ScrollableInner
|
<Dialog.ScrollableInner
|
||||||
label={_(msg`Edit post interaction settings`)}
|
label={_(msg`Edit post interaction settings`)}
|
||||||
@@ -231,7 +234,6 @@ export function PostInteractionSettingsForm({
|
|||||||
}: PostInteractionSettingsFormProps) {
|
}: PostInteractionSettingsFormProps) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const control = Dialog.useDialogContext()
|
|
||||||
const {data: lists} = useMyListsQuery('curate')
|
const {data: lists} = useMyListsQuery('curate')
|
||||||
const [quotesEnabled, setQuotesEnabled] = React.useState(
|
const [quotesEnabled, setQuotesEnabled] = React.useState(
|
||||||
!(
|
!(
|
||||||
@@ -437,7 +439,6 @@ export function PostInteractionSettingsForm({
|
|||||||
<Button
|
<Button
|
||||||
label={_(msg`Save`)}
|
label={_(msg`Save`)}
|
||||||
onPress={onSave}
|
onPress={onSave}
|
||||||
onAccessibilityEscape={control.close}
|
|
||||||
color="primary"
|
color="primary"
|
||||||
size="large"
|
size="large"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ export function SwitchAccountDialog({
|
|||||||
return (
|
return (
|
||||||
<Dialog.Outer control={control}>
|
<Dialog.Outer control={control}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
|
|
||||||
<Dialog.ScrollableInner label={_(msg`Switch Account`)}>
|
<Dialog.ScrollableInner label={_(msg`Switch Account`)}>
|
||||||
<View style={[a.gap_lg]}>
|
<View style={[a.gap_lg]}>
|
||||||
<Text style={[a.text_2xl, a.font_bold]}>
|
<Text style={[a.text_2xl, a.font_bold]}>
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ export function NeueTypography() {
|
|||||||
return (
|
return (
|
||||||
<Dialog.Outer control={control} onClose={onClose}>
|
<Dialog.Outer control={control} onClose={onClose}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
|
|
||||||
<Dialog.ScrollableInner label={_(msg`Introducing new font settings`)}>
|
<Dialog.ScrollableInner label={_(msg`Introducing new font settings`)}>
|
||||||
<View style={[a.gap_xl]}>
|
<View style={[a.gap_xl]}>
|
||||||
<View style={[a.gap_md]}>
|
<View style={[a.gap_md]}>
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ let ConvoMenu = ({
|
|||||||
<Menu.Outer>
|
<Menu.Outer>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
label={_(msg`Leave conversation`)}
|
label={_(msg`Leave conversation`)}
|
||||||
onPress={leaveConvoControl.open}>
|
onPress={() => leaveConvoControl.open()}>
|
||||||
<Menu.ItemText>
|
<Menu.ItemText>
|
||||||
<Trans>Leave conversation</Trans>
|
<Trans>Leave conversation</Trans>
|
||||||
</Menu.ItemText>
|
</Menu.ItemText>
|
||||||
@@ -195,7 +195,7 @@ let ConvoMenu = ({
|
|||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
label={_(msg`Report conversation`)}
|
label={_(msg`Report conversation`)}
|
||||||
onPress={reportControl.open}>
|
onPress={() => reportControl.open()}>
|
||||||
<Menu.ItemText>
|
<Menu.ItemText>
|
||||||
<Trans>Report conversation</Trans>
|
<Trans>Report conversation</Trans>
|
||||||
</Menu.ItemText>
|
</Menu.ItemText>
|
||||||
@@ -206,7 +206,7 @@ let ConvoMenu = ({
|
|||||||
<Menu.Group>
|
<Menu.Group>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
label={_(msg`Leave conversation`)}
|
label={_(msg`Leave conversation`)}
|
||||||
onPress={leaveConvoControl.open}>
|
onPress={() => leaveConvoControl.open()}>
|
||||||
<Menu.ItemText>
|
<Menu.ItemText>
|
||||||
<Trans>Leave conversation</Trans>
|
<Trans>Leave conversation</Trans>
|
||||||
</Menu.ItemText>
|
</Menu.ItemText>
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ import {useLingui} from '@lingui/react'
|
|||||||
|
|
||||||
import {richTextToString} from '#/lib/strings/rich-text-helpers'
|
import {richTextToString} from '#/lib/strings/rich-text-helpers'
|
||||||
import {getTranslatorLink} from '#/locale/helpers'
|
import {getTranslatorLink} from '#/locale/helpers'
|
||||||
|
import {isWeb} from '#/platform/detection'
|
||||||
|
import {useConvoActive} from '#/state/messages/convo'
|
||||||
import {useLanguagePrefs} from '#/state/preferences'
|
import {useLanguagePrefs} from '#/state/preferences'
|
||||||
import {useOpenLink} from '#/state/preferences/in-app-browser'
|
import {useOpenLink} from '#/state/preferences/in-app-browser'
|
||||||
import {isWeb} from 'platform/detection'
|
import {useSession} from '#/state/session'
|
||||||
import {useConvoActive} from 'state/messages/convo'
|
|
||||||
import {useSession} from 'state/session'
|
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {ReportDialog} from '#/components/dms/ReportDialog'
|
import {ReportDialog} from '#/components/dms/ReportDialog'
|
||||||
@@ -120,7 +120,7 @@ export let MessageMenu = ({
|
|||||||
<Menu.Item
|
<Menu.Item
|
||||||
testID="messageDropdownDeleteBtn"
|
testID="messageDropdownDeleteBtn"
|
||||||
label={_(msg`Delete message for me`)}
|
label={_(msg`Delete message for me`)}
|
||||||
onPress={deleteControl.open}>
|
onPress={() => deleteControl.open()}>
|
||||||
<Menu.ItemText>{_(msg`Delete for me`)}</Menu.ItemText>
|
<Menu.ItemText>{_(msg`Delete for me`)}</Menu.ItemText>
|
||||||
<Menu.ItemIcon icon={Trash} position="right" />
|
<Menu.ItemIcon icon={Trash} position="right" />
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
@@ -128,7 +128,7 @@ export let MessageMenu = ({
|
|||||||
<Menu.Item
|
<Menu.Item
|
||||||
testID="messageDropdownReportBtn"
|
testID="messageDropdownReportBtn"
|
||||||
label={_(msg`Report message`)}
|
label={_(msg`Report message`)}
|
||||||
onPress={reportControl.open}>
|
onPress={() => reportControl.open()}>
|
||||||
<Menu.ItemText>{_(msg`Report`)}</Menu.ItemText>
|
<Menu.ItemText>{_(msg`Report`)}</Menu.ItemText>
|
||||||
<Menu.ItemIcon icon={Warning} position="right" />
|
<Menu.ItemIcon icon={Warning} position="right" />
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
|||||||
@@ -10,13 +10,11 @@ import {useLingui} from '@lingui/react'
|
|||||||
import {useMutation} from '@tanstack/react-query'
|
import {useMutation} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {ReportOption} from '#/lib/moderation/useReportOptions'
|
import {ReportOption} from '#/lib/moderation/useReportOptions'
|
||||||
import {isAndroid} from '#/platform/detection'
|
|
||||||
import {useAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
|
import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import {KeyboardControllerPadding} from '#/components/KeyboardControllerPadding'
|
|
||||||
import {Button, ButtonIcon, ButtonText} from '../Button'
|
import {Button, ButtonIcon, ButtonText} from '../Button'
|
||||||
import {Divider} from '../Divider'
|
import {Divider} from '../Divider'
|
||||||
import {ChevronLeft_Stroke2_Corner0_Rounded as Chevron} from '../icons/Chevron'
|
import {ChevronLeft_Stroke2_Corner0_Rounded as Chevron} from '../icons/Chevron'
|
||||||
@@ -41,14 +39,11 @@ let ReportDialog = ({
|
|||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
return (
|
return (
|
||||||
<Dialog.Outer
|
<Dialog.Outer control={control}>
|
||||||
control={control}
|
|
||||||
nativeOptions={isAndroid ? {sheet: {snapPoints: ['100%']}} : {}}>
|
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
<Dialog.ScrollableInner label={_(msg`Report this message`)}>
|
<Dialog.ScrollableInner label={_(msg`Report this message`)}>
|
||||||
<DialogInner params={params} />
|
<DialogInner params={params} />
|
||||||
<Dialog.Close />
|
<Dialog.Close />
|
||||||
<KeyboardControllerPadding />
|
|
||||||
</Dialog.ScrollableInner>
|
</Dialog.ScrollableInner>
|
||||||
</Dialog.Outer>
|
</Dialog.Outer>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import React, {useCallback} from 'react'
|
|||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {logEvent} from '#/lib/statsig/statsig'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
|
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
|
||||||
import {logEvent} from 'lib/statsig/statsig'
|
|
||||||
import {FAB} from '#/view/com/util/fab/FAB'
|
import {FAB} from '#/view/com/util/fab/FAB'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {useTheme} from '#/alf'
|
import {useTheme} from '#/alf'
|
||||||
@@ -55,10 +55,8 @@ export function NewChat({
|
|||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Dialog.Outer
|
<Dialog.Outer control={control} testID="newChatDialog">
|
||||||
control={control}
|
<Dialog.Handle />
|
||||||
testID="newChatDialog"
|
|
||||||
nativeOptions={{sheet: {snapPoints: ['100%']}}}>
|
|
||||||
<SearchablePeopleList
|
<SearchablePeopleList
|
||||||
title={_(msg`Start a new chat`)}
|
title={_(msg`Start a new chat`)}
|
||||||
onSelectChat={onCreateChat}
|
onSelectChat={onCreateChat}
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ import React, {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import type {TextInput as TextInputType} from 'react-native'
|
import {TextInput, View} from 'react-native'
|
||||||
import {View} from 'react-native'
|
|
||||||
import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
|
import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
|
||||||
import {BottomSheetFlatListMethods} from '@discord/bottom-sheet'
|
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
@@ -16,18 +14,17 @@ import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
|||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
import {isWeb} from '#/platform/detection'
|
import {isWeb} from '#/platform/detection'
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
|
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
|
||||||
import {useListConvosQuery} from '#/state/queries/messages/list-converations'
|
import {useListConvosQuery} from '#/state/queries/messages/list-converations'
|
||||||
import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
|
import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {useActorAutocompleteQuery} from 'state/queries/actor-autocomplete'
|
import {ListMethods} from '#/view/com/util/List'
|
||||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {atoms as a, native, useTheme, web} from '#/alf'
|
import {atoms as a, native, useTheme, web} from '#/alf'
|
||||||
import {Button} from '#/components/Button'
|
import {Button, ButtonIcon} from '#/components/Button'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import {TextInput} from '#/components/dms/dialogs/TextInput'
|
|
||||||
import {canBeMessaged} from '#/components/dms/util'
|
import {canBeMessaged} from '#/components/dms/util'
|
||||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||||
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
|
|
||||||
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
|
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
|
||||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
@@ -66,9 +63,9 @@ export function SearchablePeopleList({
|
|||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const moderationOpts = useModerationOpts()
|
const moderationOpts = useModerationOpts()
|
||||||
const control = Dialog.useDialogContext()
|
const control = Dialog.useDialogContext()
|
||||||
const listRef = useRef<BottomSheetFlatListMethods>(null)
|
const listRef = useRef<ListMethods>(null)
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const inputRef = useRef<TextInputType>(null)
|
const inputRef = useRef<TextInput>(null)
|
||||||
|
|
||||||
const [searchText, setSearchText] = useState('')
|
const [searchText, setSearchText] = useState('')
|
||||||
|
|
||||||
@@ -101,15 +98,15 @@ export function SearchablePeopleList({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
_items = _items.sort(a => {
|
_items = _items.sort(item => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return a.enabled ? -1 : 1
|
return item.enabled ? -1 : 1
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const placeholders: Item[] = Array(10)
|
const placeholders: Item[] = Array(10)
|
||||||
.fill(0)
|
.fill(0)
|
||||||
.map((_, i) => ({
|
.map((__, i) => ({
|
||||||
type: 'placeholder',
|
type: 'placeholder',
|
||||||
key: i + '',
|
key: i + '',
|
||||||
}))
|
}))
|
||||||
@@ -155,9 +152,9 @@ export function SearchablePeopleList({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// only sort follows
|
// only sort follows
|
||||||
followsItems = followsItems.sort(a => {
|
followsItems = followsItems.sort(item => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return a.enabled ? -1 : 1
|
return item.enabled ? -1 : 1
|
||||||
})
|
})
|
||||||
|
|
||||||
// then append
|
// then append
|
||||||
@@ -177,9 +174,9 @@ export function SearchablePeopleList({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_items = _items.sort(a => {
|
_items = _items.sort(item => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return a.enabled ? -1 : 1
|
return item.enabled ? -1 : 1
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
_items.push(...placeholders)
|
_items.push(...placeholders)
|
||||||
@@ -242,57 +239,46 @@ export function SearchablePeopleList({
|
|||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.relative,
|
a.relative,
|
||||||
a.pt_md,
|
web(a.pt_lg),
|
||||||
|
native(a.pt_4xl),
|
||||||
a.pb_xs,
|
a.pb_xs,
|
||||||
a.px_lg,
|
a.px_lg,
|
||||||
a.border_b,
|
a.border_b,
|
||||||
t.atoms.border_contrast_low,
|
t.atoms.border_contrast_low,
|
||||||
t.atoms.bg,
|
t.atoms.bg,
|
||||||
native([a.pt_lg]),
|
|
||||||
]}>
|
]}>
|
||||||
<View
|
<View style={[a.relative, native(a.align_center), a.justify_center]}>
|
||||||
style={[
|
|
||||||
a.relative,
|
|
||||||
native(a.align_center),
|
|
||||||
a.justify_center,
|
|
||||||
{height: 32},
|
|
||||||
]}>
|
|
||||||
<Button
|
|
||||||
label={_(msg`Close`)}
|
|
||||||
size="small"
|
|
||||||
shape="round"
|
|
||||||
variant="ghost"
|
|
||||||
color="secondary"
|
|
||||||
style={[
|
|
||||||
a.absolute,
|
|
||||||
a.z_20,
|
|
||||||
native({
|
|
||||||
left: -7,
|
|
||||||
}),
|
|
||||||
web({
|
|
||||||
right: -4,
|
|
||||||
}),
|
|
||||||
]}
|
|
||||||
onPress={() => control.close()}>
|
|
||||||
{isWeb ? (
|
|
||||||
<X size="md" fill={t.palette.contrast_500} />
|
|
||||||
) : (
|
|
||||||
<ChevronLeft size="md" fill={t.palette.contrast_500} />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
a.z_10,
|
a.z_10,
|
||||||
a.text_lg,
|
a.text_lg,
|
||||||
a.font_bold,
|
a.font_heavy,
|
||||||
a.leading_tight,
|
a.leading_tight,
|
||||||
t.atoms.text_contrast_high,
|
t.atoms.text_contrast_high,
|
||||||
]}>
|
]}>
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
|
{isWeb ? (
|
||||||
|
<Button
|
||||||
|
label={_(msg`Close`)}
|
||||||
|
size="small"
|
||||||
|
shape="round"
|
||||||
|
variant={isWeb ? 'ghost' : 'solid'}
|
||||||
|
color="secondary"
|
||||||
|
style={[
|
||||||
|
a.absolute,
|
||||||
|
a.z_20,
|
||||||
|
web({right: -4}),
|
||||||
|
native({right: 0}),
|
||||||
|
native({height: 32, width: 32, borderRadius: 16}),
|
||||||
|
]}
|
||||||
|
onPress={() => control.close()}>
|
||||||
|
<ButtonIcon icon={X} size="md" />
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={[native([a.pt_sm]), web([a.pt_xs])]}>
|
<View style={[, web([a.pt_xs])]}>
|
||||||
<SearchInput
|
<SearchInput
|
||||||
inputRef={inputRef}
|
inputRef={inputRef}
|
||||||
value={searchText}
|
value={searchText}
|
||||||
@@ -309,7 +295,6 @@ export function SearchablePeopleList({
|
|||||||
t.atoms.border_contrast_low,
|
t.atoms.border_contrast_low,
|
||||||
t.atoms.bg,
|
t.atoms.bg,
|
||||||
t.atoms.text_contrast_high,
|
t.atoms.text_contrast_high,
|
||||||
t.palette.contrast_500,
|
|
||||||
_,
|
_,
|
||||||
title,
|
title,
|
||||||
searchText,
|
searchText,
|
||||||
@@ -326,14 +311,7 @@ export function SearchablePeopleList({
|
|||||||
keyExtractor={(item: Item) => item.key}
|
keyExtractor={(item: Item) => item.key}
|
||||||
style={[
|
style={[
|
||||||
web([a.py_0, {height: '100vh', maxHeight: 600}, a.px_0]),
|
web([a.py_0, {height: '100vh', maxHeight: 600}, a.px_0]),
|
||||||
native({
|
native({height: '100%'}),
|
||||||
height: '100%',
|
|
||||||
paddingHorizontal: 0,
|
|
||||||
marginTop: 0,
|
|
||||||
paddingTop: 0,
|
|
||||||
borderTopLeftRadius: 40,
|
|
||||||
borderTopRightRadius: 40,
|
|
||||||
}),
|
|
||||||
]}
|
]}
|
||||||
webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]}
|
webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]}
|
||||||
keyboardDismissMode="on-drag"
|
keyboardDismissMode="on-drag"
|
||||||
@@ -396,7 +374,8 @@ function ProfileCard({
|
|||||||
<View style={[a.flex_1, a.gap_2xs]}>
|
<View style={[a.flex_1, a.gap_2xs]}>
|
||||||
<Text
|
<Text
|
||||||
style={[t.atoms.text, a.font_bold, a.leading_tight, a.self_start]}
|
style={[t.atoms.text, a.font_bold, a.leading_tight, a.self_start]}
|
||||||
numberOfLines={1}>
|
numberOfLines={1}
|
||||||
|
emoji>
|
||||||
{displayName}
|
{displayName}
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
@@ -474,7 +453,7 @@ function SearchInput({
|
|||||||
value: string
|
value: string
|
||||||
onChangeText: (text: string) => void
|
onChangeText: (text: string) => void
|
||||||
onEscape: () => void
|
onEscape: () => void
|
||||||
inputRef: React.RefObject<TextInputType>
|
inputRef: React.RefObject<TextInput>
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import React, {useCallback} from 'react'
|
|||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {logEvent} from '#/lib/statsig/statsig'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
|
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
|
||||||
import {logEvent} from 'lib/statsig/statsig'
|
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import {SearchablePeopleList} from './SearchablePeopleList'
|
import {SearchablePeopleList} from './SearchablePeopleList'
|
||||||
@@ -17,10 +17,8 @@ export function SendViaChatDialog({
|
|||||||
onSelectChat: (chatId: string) => void
|
onSelectChat: (chatId: string) => void
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Dialog.Outer
|
<Dialog.Outer control={control} testID="sendViaChatChatDialog">
|
||||||
control={control}
|
<Dialog.Handle />
|
||||||
testID="sendViaChatChatDialog"
|
|
||||||
nativeOptions={{sheet: {snapPoints: ['100%']}}}>
|
|
||||||
<SendViaChatDialogInner control={control} onSelectChat={onSelectChat} />
|
<SendViaChatDialogInner control={control} onSelectChat={onSelectChat} />
|
||||||
</Dialog.Outer>
|
</Dialog.Outer>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import React from 'react'
|
|||||||
import {Pressable, View, ViewStyle} from 'react-native'
|
import {Pressable, View, ViewStyle} from 'react-native'
|
||||||
import Animated, {LinearTransition} from 'react-native-reanimated'
|
import Animated, {LinearTransition} from 'react-native-reanimated'
|
||||||
|
|
||||||
|
import {HITSLOP_10} from '#/lib/constants'
|
||||||
import {isNative} from '#/platform/detection'
|
import {isNative} from '#/platform/detection'
|
||||||
import {HITSLOP_10} from 'lib/constants'
|
|
||||||
import {
|
import {
|
||||||
atoms as a,
|
atoms as a,
|
||||||
flatten,
|
flatten,
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ export function LabelsOnMeDialog(props: LabelsOnMeDialogProps) {
|
|||||||
return (
|
return (
|
||||||
<Dialog.Outer control={props.control}>
|
<Dialog.Outer control={props.control}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
|
|
||||||
<LabelsOnMeDialogInner {...props} />
|
<LabelsOnMeDialogInner {...props} />
|
||||||
</Dialog.Outer>
|
</Dialog.Outer>
|
||||||
)
|
)
|
||||||
@@ -158,23 +157,25 @@ function Label({
|
|||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<View style={[a.px_md, a.py_sm, t.atoms.bg_contrast_25]}>
|
<View style={[a.px_md, a.py_sm, t.atoms.bg_contrast_25]}>
|
||||||
<Text style={[t.atoms.text_contrast_medium]}>
|
{isSelfLabel ? (
|
||||||
{isSelfLabel ? (
|
<Text style={[t.atoms.text_contrast_medium]}>
|
||||||
<Trans>This label was applied by you.</Trans>
|
<Trans>This label was applied by you.</Trans>
|
||||||
) : (
|
</Text>
|
||||||
<Trans>
|
) : (
|
||||||
Source:{' '}
|
<View style={{flexDirection: 'row'}}>
|
||||||
<InlineLinkText
|
<Text style={[t.atoms.text_contrast_medium]}>
|
||||||
label={sourceName}
|
<Trans>Source: </Trans>{' '}
|
||||||
to={makeProfileLink(
|
</Text>
|
||||||
labeler ? labeler.creator : {did: label.src, handle: ''},
|
<InlineLinkText
|
||||||
)}
|
label={sourceName}
|
||||||
onPress={() => control.close()}>
|
to={makeProfileLink(
|
||||||
{sourceName}
|
labeler ? labeler.creator : {did: label.src, handle: ''},
|
||||||
</InlineLinkText>
|
)}
|
||||||
</Trans>
|
onPress={() => control.close()}>
|
||||||
)}
|
{sourceName}
|
||||||
</Text>
|
</InlineLinkText>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
@@ -236,24 +237,24 @@ function AppealForm({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Text style={[a.text_2xl, a.font_bold, a.pb_xs, a.leading_tight]}>
|
<View style={{flexWrap: 'wrap', flexDirection: 'row'}}>
|
||||||
<Trans>Appeal "{strings.name}" label</Trans>
|
<Text style={[a.text_2xl, a.font_bold, a.pb_xs, a.leading_tight]}>
|
||||||
</Text>
|
<Trans>Appeal "{strings.name}" label</Trans>
|
||||||
<Text style={[a.text_md, a.leading_snug]}>
|
</Text>
|
||||||
<Trans>
|
<Text style={[a.text_md, a.leading_snug]}>
|
||||||
This appeal will be sent to{' '}
|
<Trans>This appeal will be sent to</Trans>{' '}
|
||||||
<InlineLinkText
|
</Text>
|
||||||
label={sourceName}
|
<InlineLinkText
|
||||||
to={makeProfileLink(
|
label={sourceName}
|
||||||
labeler ? labeler.creator : {did: label.src, handle: ''},
|
to={makeProfileLink(
|
||||||
)}
|
labeler ? labeler.creator : {did: label.src, handle: ''},
|
||||||
onPress={() => control.close()}
|
)}
|
||||||
style={[a.text_md, a.leading_snug]}>
|
onPress={() => control.close()}
|
||||||
{sourceName}
|
style={[a.text_md, a.leading_snug]}>
|
||||||
</InlineLinkText>
|
{sourceName}
|
||||||
.
|
</InlineLinkText>
|
||||||
</Trans>
|
<Text style={[a.text_md, a.leading_snug]}>.</Text>
|
||||||
</Text>
|
</View>
|
||||||
<View style={[a.my_md]}>
|
<View style={[a.my_md]}>
|
||||||
<Dialog.Input
|
<Dialog.Input
|
||||||
label={_(msg`Text input field`)}
|
label={_(msg`Text input field`)}
|
||||||
|
|||||||
@@ -141,23 +141,24 @@ function ModerationDetailsDialogInner({
|
|||||||
{modcause?.type === 'label' && (
|
{modcause?.type === 'label' && (
|
||||||
<View style={[a.pt_lg]}>
|
<View style={[a.pt_lg]}>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Text style={[t.atoms.text, a.text_md, a.leading_snug, a.mt_lg]}>
|
{modcause.source.type === 'user' ? (
|
||||||
{modcause.source.type === 'user' ? (
|
<Text style={[t.atoms.text, a.text_md, a.leading_snug, a.mt_lg]}>
|
||||||
<Trans>This label was applied by the author.</Trans>
|
<Trans>This label was applied by the author.</Trans>
|
||||||
) : (
|
</Text>
|
||||||
<Trans>
|
) : (
|
||||||
This label was applied by{' '}
|
<>
|
||||||
<InlineLinkText
|
<Text style={[t.atoms.text, a.text_md, a.leading_snug, a.mt_lg]}>
|
||||||
label={desc.source || _(msg`an unknown labeler`)}
|
<Trans>This label was applied by </Trans>
|
||||||
to={makeProfileLink({did: modcause.label.src, handle: ''})}
|
</Text>
|
||||||
onPress={() => control.close()}
|
<InlineLinkText
|
||||||
style={a.text_md}>
|
label={desc.source || _(msg`an unknown labeler`)}
|
||||||
{desc.source || _(msg`an unknown labeler`)}
|
to={makeProfileLink({did: modcause.label.src, handle: ''})}
|
||||||
</InlineLinkText>
|
onPress={() => control.close()}
|
||||||
.
|
style={a.text_md}>
|
||||||
</Trans>
|
{desc.source || _(msg`an unknown labeler`)}
|
||||||
)}
|
</InlineLinkText>
|
||||||
</Text>
|
</>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import {nanoid} from 'nanoid/non-secure'
|
|||||||
import {AbortError} from '#/lib/async/cancelable'
|
import {AbortError} from '#/lib/async/cancelable'
|
||||||
import {ServerError} from '#/lib/media/video/errors'
|
import {ServerError} from '#/lib/media/video/errors'
|
||||||
import {CompressedVideo} from '#/lib/media/video/types'
|
import {CompressedVideo} from '#/lib/media/video/types'
|
||||||
import {createVideoEndpointUrl, mimeToExt} from './util'
|
|
||||||
import {getServiceAuthToken, getVideoUploadLimits} from './upload.shared'
|
import {getServiceAuthToken, getVideoUploadLimits} from './upload.shared'
|
||||||
|
import {createVideoEndpointUrl, mimeToExt} from './util'
|
||||||
|
|
||||||
export async function uploadVideo({
|
export async function uploadVideo({
|
||||||
video,
|
video,
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import {nanoid} from 'nanoid/non-secure'
|
|||||||
import {AbortError} from '#/lib/async/cancelable'
|
import {AbortError} from '#/lib/async/cancelable'
|
||||||
import {ServerError} from '#/lib/media/video/errors'
|
import {ServerError} from '#/lib/media/video/errors'
|
||||||
import {CompressedVideo} from '#/lib/media/video/types'
|
import {CompressedVideo} from '#/lib/media/video/types'
|
||||||
import {createVideoEndpointUrl, mimeToExt} from './util'
|
|
||||||
import {getServiceAuthToken, getVideoUploadLimits} from './upload.shared'
|
import {getServiceAuthToken, getVideoUploadLimits} from './upload.shared'
|
||||||
|
import {createVideoEndpointUrl, mimeToExt} from './util'
|
||||||
|
|
||||||
export async function uploadVideo({
|
export async function uploadVideo({
|
||||||
video,
|
video,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
|
import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
|
||||||
import {IconCircle} from '#/components/IconCircle'
|
import {IconCircle} from '#/components/IconCircle'
|
||||||
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
||||||
import {CircleInfo_Stroke2_Corner0_Rounded} from '#/components/icons/CircleInfo'
|
import {CircleInfo_Stroke2_Corner0_Rounded} from '#/components/icons/CircleInfo'
|
||||||
@@ -89,15 +90,18 @@ export function StepProfile() {
|
|||||||
requestNotificationsPermission('StartOnboarding')
|
requestNotificationsPermission('StartOnboarding')
|
||||||
}, [gate, requestNotificationsPermission])
|
}, [gate, requestNotificationsPermission])
|
||||||
|
|
||||||
|
const sheetWrapper = useSheetWrapper()
|
||||||
const openPicker = React.useCallback(
|
const openPicker = React.useCallback(
|
||||||
async (opts?: ImagePickerOptions) => {
|
async (opts?: ImagePickerOptions) => {
|
||||||
const response = await launchImageLibraryAsync({
|
const response = await sheetWrapper(
|
||||||
exif: false,
|
launchImageLibraryAsync({
|
||||||
mediaTypes: MediaTypeOptions.Images,
|
exif: false,
|
||||||
quality: 1,
|
mediaTypes: MediaTypeOptions.Images,
|
||||||
...opts,
|
quality: 1,
|
||||||
legacy: true,
|
...opts,
|
||||||
})
|
legacy: true,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
return (response.assets ?? [])
|
return (response.assets ?? [])
|
||||||
.slice(0, 1)
|
.slice(0, 1)
|
||||||
@@ -121,7 +125,7 @@ export function StepProfile() {
|
|||||||
size: getDataUriSize(image.uri),
|
size: getDataUriSize(image.uri),
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
[_, setError],
|
[_, setError, sheetWrapper],
|
||||||
)
|
)
|
||||||
|
|
||||||
const onContinue = React.useCallback(async () => {
|
const onContinue = React.useCallback(async () => {
|
||||||
@@ -168,9 +172,11 @@ export function StepProfile() {
|
|||||||
|
|
||||||
setError('')
|
setError('')
|
||||||
|
|
||||||
const items = await openPicker({
|
const items = await sheetWrapper(
|
||||||
aspect: [1, 1],
|
openPicker({
|
||||||
})
|
aspect: [1, 1],
|
||||||
|
}),
|
||||||
|
)
|
||||||
let image = items[0]
|
let image = items[0]
|
||||||
if (!image) return
|
if (!image) return
|
||||||
|
|
||||||
@@ -196,7 +202,13 @@ export function StepProfile() {
|
|||||||
image,
|
image,
|
||||||
useCreatedAvatar: false,
|
useCreatedAvatar: false,
|
||||||
}))
|
}))
|
||||||
}, [requestPhotoAccessIfNeeded, setAvatar, openPicker, setError])
|
}, [
|
||||||
|
requestPhotoAccessIfNeeded,
|
||||||
|
setAvatar,
|
||||||
|
openPicker,
|
||||||
|
setError,
|
||||||
|
sheetWrapper,
|
||||||
|
])
|
||||||
|
|
||||||
const onSecondaryPress = React.useCallback(() => {
|
const onSecondaryPress = React.useCallback(() => {
|
||||||
if (avatar.useCreatedAvatar) {
|
if (avatar.useCreatedAvatar) {
|
||||||
@@ -286,7 +298,6 @@ export function StepProfile() {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Dialog.Outer control={creatorControl}>
|
<Dialog.Outer control={creatorControl}>
|
||||||
<Dialog.Handle />
|
|
||||||
<Dialog.Inner
|
<Dialog.Inner
|
||||||
label="Avatar creator"
|
label="Avatar creator"
|
||||||
style={[
|
style={[
|
||||||
|
|||||||
@@ -15,35 +15,35 @@ import {useNavigation} from '@react-navigation/native'
|
|||||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {batchedUpdates} from '#/lib/batchedUpdates'
|
||||||
|
import {HITSLOP_20} from '#/lib/constants'
|
||||||
|
import {isBlockedOrBlocking, isMuted} from '#/lib/moderation/blocked-and-muted'
|
||||||
|
import {makeProfileLink, makeStarterPackLink} from '#/lib/routes/links'
|
||||||
|
import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types'
|
||||||
|
import {logEvent} from '#/lib/statsig/statsig'
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
|
import {getStarterPackOgCard} from '#/lib/strings/starter-pack'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
|
import {isWeb} from '#/platform/detection'
|
||||||
|
import {updateProfileShadow} from '#/state/cache/profile-shadow'
|
||||||
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
|
import {getAllListMembers} from '#/state/queries/list-members'
|
||||||
|
import {useResolvedStarterPackShortLink} from '#/state/queries/resolve-short-link'
|
||||||
|
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
|
||||||
|
import {useShortenLink} from '#/state/queries/shorten-link'
|
||||||
import {useDeleteStarterPackMutation} from '#/state/queries/starter-packs'
|
import {useDeleteStarterPackMutation} from '#/state/queries/starter-packs'
|
||||||
|
import {useStarterPackQuery} from '#/state/queries/starter-packs'
|
||||||
|
import {useAgent, useSession} from '#/state/session'
|
||||||
|
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
||||||
import {
|
import {
|
||||||
ProgressGuideAction,
|
ProgressGuideAction,
|
||||||
useProgressGuideControls,
|
useProgressGuideControls,
|
||||||
} from '#/state/shell/progress-guide'
|
} from '#/state/shell/progress-guide'
|
||||||
import {batchedUpdates} from 'lib/batchedUpdates'
|
import {useSetActiveStarterPack} from '#/state/shell/starter-pack'
|
||||||
import {HITSLOP_20} from 'lib/constants'
|
import {PagerWithHeader} from '#/view/com/pager/PagerWithHeader'
|
||||||
import {isBlockedOrBlocking, isMuted} from 'lib/moderation/blocked-and-muted'
|
import {ProfileSubpageHeader} from '#/view/com/profile/ProfileSubpageHeader'
|
||||||
import {makeProfileLink, makeStarterPackLink} from 'lib/routes/links'
|
|
||||||
import {CommonNavigatorParams, NavigationProp} from 'lib/routes/types'
|
|
||||||
import {logEvent} from 'lib/statsig/statsig'
|
|
||||||
import {getStarterPackOgCard} from 'lib/strings/starter-pack'
|
|
||||||
import {isWeb} from 'platform/detection'
|
|
||||||
import {updateProfileShadow} from 'state/cache/profile-shadow'
|
|
||||||
import {useModerationOpts} from 'state/preferences/moderation-opts'
|
|
||||||
import {getAllListMembers} from 'state/queries/list-members'
|
|
||||||
import {useResolvedStarterPackShortLink} from 'state/queries/resolve-short-link'
|
|
||||||
import {useResolveDidQuery} from 'state/queries/resolve-uri'
|
|
||||||
import {useShortenLink} from 'state/queries/shorten-link'
|
|
||||||
import {useStarterPackQuery} from 'state/queries/starter-packs'
|
|
||||||
import {useAgent, useSession} from 'state/session'
|
|
||||||
import {useLoggedOutViewControls} from 'state/shell/logged-out'
|
|
||||||
import {useSetActiveStarterPack} from 'state/shell/starter-pack'
|
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
|
import {CenteredView} from '#/view/com/util/Views'
|
||||||
import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader'
|
|
||||||
import {CenteredView} from 'view/com/util/Views'
|
|
||||||
import {bulkWriteFollows} from '#/screens/Onboarding/util'
|
import {bulkWriteFollows} from '#/screens/Onboarding/util'
|
||||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
@@ -591,7 +591,7 @@ function OverflowMenu({
|
|||||||
|
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
label={_(msg`Report starter pack`)}
|
label={_(msg`Report starter pack`)}
|
||||||
onPress={reportDialogControl.open}>
|
onPress={() => reportDialogControl.open()}>
|
||||||
<Menu.ItemText>
|
<Menu.ItemText>
|
||||||
<Trans>Report starter pack</Trans>
|
<Trans>Report starter pack</Trans>
|
||||||
</Menu.ItemText>
|
</Menu.ItemText>
|
||||||
|
|||||||
+47
-40
@@ -1,8 +1,9 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {SharedValue, useSharedValue} from 'react-native-reanimated'
|
|
||||||
|
|
||||||
|
import {isWeb} from '#/platform/detection'
|
||||||
import {DialogControlRefProps} from '#/components/Dialog'
|
import {DialogControlRefProps} from '#/components/Dialog'
|
||||||
import {Provider as GlobalDialogsProvider} from '#/components/dialogs/Context'
|
import {Provider as GlobalDialogsProvider} from '#/components/dialogs/Context'
|
||||||
|
import {BottomSheet} from '../../../modules/bottom-sheet'
|
||||||
|
|
||||||
interface IDialogContext {
|
interface IDialogContext {
|
||||||
/**
|
/**
|
||||||
@@ -16,25 +17,24 @@ interface IDialogContext {
|
|||||||
* `useId`.
|
* `useId`.
|
||||||
*/
|
*/
|
||||||
openDialogs: React.MutableRefObject<Set<string>>
|
openDialogs: React.MutableRefObject<Set<string>>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IDialogControlContext {
|
||||||
|
closeAllDialogs(): boolean
|
||||||
|
setDialogIsOpen(id: string, isOpen: boolean): void
|
||||||
/**
|
/**
|
||||||
* The counterpart to `accessibilityViewIsModal` for Android. This property
|
* The number of dialogs that are fully expanded. This is used to determine the backgground color of the status bar
|
||||||
* applies to the parent of all non-modal views, and prevents TalkBack from
|
* on iOS.
|
||||||
* navigating within content beneath an open dialog.
|
|
||||||
*
|
|
||||||
* @see https://reactnative.dev/docs/accessibility#importantforaccessibility-android
|
|
||||||
*/
|
*/
|
||||||
importantForAccessibility: SharedValue<'auto' | 'no-hide-descendants'>
|
fullyExpandedCount: number
|
||||||
|
setFullyExpandedCount: React.Dispatch<React.SetStateAction<number>>
|
||||||
}
|
}
|
||||||
|
|
||||||
const DialogContext = React.createContext<IDialogContext>({} as IDialogContext)
|
const DialogContext = React.createContext<IDialogContext>({} as IDialogContext)
|
||||||
|
|
||||||
const DialogControlContext = React.createContext<{
|
const DialogControlContext = React.createContext<IDialogControlContext>(
|
||||||
closeAllDialogs(): boolean
|
{} as IDialogControlContext,
|
||||||
setDialogIsOpen(id: string, isOpen: boolean): void
|
)
|
||||||
}>({
|
|
||||||
closeAllDialogs: () => false,
|
|
||||||
setDialogIsOpen: () => {},
|
|
||||||
})
|
|
||||||
|
|
||||||
export function useDialogStateContext() {
|
export function useDialogStateContext() {
|
||||||
return React.useContext(DialogContext)
|
return React.useContext(DialogContext)
|
||||||
@@ -45,48 +45,55 @@ export function useDialogStateControlContext() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Provider({children}: React.PropsWithChildren<{}>) {
|
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
|
const [fullyExpandedCount, setFullyExpandedCount] = React.useState(0)
|
||||||
|
|
||||||
const activeDialogs = React.useRef<
|
const activeDialogs = React.useRef<
|
||||||
Map<string, React.MutableRefObject<DialogControlRefProps>>
|
Map<string, React.MutableRefObject<DialogControlRefProps>>
|
||||||
>(new Map())
|
>(new Map())
|
||||||
const openDialogs = React.useRef<Set<string>>(new Set())
|
const openDialogs = React.useRef<Set<string>>(new Set())
|
||||||
const importantForAccessibility = useSharedValue<
|
|
||||||
'auto' | 'no-hide-descendants'
|
|
||||||
>('auto')
|
|
||||||
|
|
||||||
const closeAllDialogs = React.useCallback(() => {
|
const closeAllDialogs = React.useCallback(() => {
|
||||||
openDialogs.current.forEach(id => {
|
if (isWeb) {
|
||||||
const dialog = activeDialogs.current.get(id)
|
openDialogs.current.forEach(id => {
|
||||||
if (dialog) dialog.current.close()
|
const dialog = activeDialogs.current.get(id)
|
||||||
})
|
if (dialog) dialog.current.close()
|
||||||
return openDialogs.current.size > 0
|
})
|
||||||
|
|
||||||
|
return openDialogs.current.size > 0
|
||||||
|
} else {
|
||||||
|
BottomSheet.dismissAll()
|
||||||
|
return false
|
||||||
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const setDialogIsOpen = React.useCallback(
|
const setDialogIsOpen = React.useCallback((id: string, isOpen: boolean) => {
|
||||||
(id: string, isOpen: boolean) => {
|
if (isOpen) {
|
||||||
if (isOpen) {
|
openDialogs.current.add(id)
|
||||||
openDialogs.current.add(id)
|
} else {
|
||||||
importantForAccessibility.value = 'no-hide-descendants'
|
openDialogs.current.delete(id)
|
||||||
} else {
|
}
|
||||||
openDialogs.current.delete(id)
|
}, [])
|
||||||
if (openDialogs.current.size < 1) {
|
|
||||||
importantForAccessibility.value = 'auto'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[importantForAccessibility],
|
|
||||||
)
|
|
||||||
|
|
||||||
const context = React.useMemo<IDialogContext>(
|
const context = React.useMemo<IDialogContext>(
|
||||||
() => ({
|
() => ({
|
||||||
activeDialogs,
|
activeDialogs,
|
||||||
openDialogs,
|
openDialogs,
|
||||||
importantForAccessibility,
|
|
||||||
}),
|
}),
|
||||||
[importantForAccessibility, activeDialogs, openDialogs],
|
[activeDialogs, openDialogs],
|
||||||
)
|
)
|
||||||
const controls = React.useMemo(
|
const controls = React.useMemo(
|
||||||
() => ({closeAllDialogs, setDialogIsOpen}),
|
() => ({
|
||||||
[closeAllDialogs, setDialogIsOpen],
|
closeAllDialogs,
|
||||||
|
setDialogIsOpen,
|
||||||
|
fullyExpandedCount,
|
||||||
|
setFullyExpandedCount,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
closeAllDialogs,
|
||||||
|
setDialogIsOpen,
|
||||||
|
fullyExpandedCount,
|
||||||
|
setFullyExpandedCount,
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import React from 'react'
|
|||||||
import {Linking} from 'react-native'
|
import {Linking} from 'react-native'
|
||||||
import * as WebBrowser from 'expo-web-browser'
|
import * as WebBrowser from 'expo-web-browser'
|
||||||
|
|
||||||
import {isNative} from '#/platform/detection'
|
import {usePalette} from '#/lib/hooks/usePalette'
|
||||||
import * as persisted from '#/state/persisted'
|
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
|
||||||
import {
|
import {
|
||||||
createBskyAppAbsoluteUrl,
|
createBskyAppAbsoluteUrl,
|
||||||
isBskyRSSUrl,
|
isBskyRSSUrl,
|
||||||
isRelativeUrl,
|
isRelativeUrl,
|
||||||
} from 'lib/strings/url-helpers'
|
} from '#/lib/strings/url-helpers'
|
||||||
|
import {isNative} from '#/platform/detection'
|
||||||
|
import * as persisted from '#/state/persisted'
|
||||||
import {useModalControls} from '../modals'
|
import {useModalControls} from '../modals'
|
||||||
|
|
||||||
type StateContext = persisted.Schema['useInAppBrowser']
|
type StateContext = persisted.Schema['useInAppBrowser']
|
||||||
@@ -62,7 +62,7 @@ export function useOpenLink() {
|
|||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
|
||||||
const openLink = React.useCallback(
|
const openLink = React.useCallback(
|
||||||
(url: string, override?: boolean) => {
|
async (url: string, override?: boolean) => {
|
||||||
if (isBskyRSSUrl(url) && isRelativeUrl(url)) {
|
if (isBskyRSSUrl(url) && isRelativeUrl(url)) {
|
||||||
url = createBskyAppAbsoluteUrl(url)
|
url = createBskyAppAbsoluteUrl(url)
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@ export function useOpenLink() {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
} else if (override ?? enabled) {
|
} else if (override ?? enabled) {
|
||||||
WebBrowser.openBrowserAsync(url, {
|
await WebBrowser.openBrowserAsync(url, {
|
||||||
presentationStyle:
|
presentationStyle:
|
||||||
WebBrowser.WebBrowserPresentationStyle.FULL_SCREEN,
|
WebBrowser.WebBrowserPresentationStyle.FULL_SCREEN,
|
||||||
toolbarColor: pal.colors.backgroundLight,
|
toolbarColor: pal.colors.backgroundLight,
|
||||||
|
|||||||
@@ -66,12 +66,8 @@ export function ServerInputDialog({
|
|||||||
])
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.Outer
|
<Dialog.Outer control={control} onClose={onClose}>
|
||||||
control={control}
|
|
||||||
nativeOptions={{sheet: {snapPoints: ['100%']}}}
|
|
||||||
onClose={onClose}>
|
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
|
|
||||||
<Dialog.ScrollableInner
|
<Dialog.ScrollableInner
|
||||||
accessibilityDescribedBy="dialog-description"
|
accessibilityDescribedBy="dialog-description"
|
||||||
accessibilityLabelledBy="dialog-title">
|
accessibilityLabelledBy="dialog-title">
|
||||||
|
|||||||
+296
-276
@@ -114,11 +114,14 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
|||||||
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
||||||
import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji'
|
import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji'
|
||||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||||
|
import {createPortalGroup} from '#/components/Portal'
|
||||||
import * as Prompt from '#/components/Prompt'
|
import * as Prompt from '#/components/Prompt'
|
||||||
import {Text as NewText} from '#/components/Typography'
|
import {Text as NewText} from '#/components/Typography'
|
||||||
import {composerReducer, createComposerState} from './state/composer'
|
import {composerReducer, createComposerState} from './state/composer'
|
||||||
import {NO_VIDEO, NoVideoState, processVideo, VideoState} from './state/video'
|
import {NO_VIDEO, NoVideoState, processVideo, VideoState} from './state/video'
|
||||||
|
|
||||||
|
const Portal = createPortalGroup()
|
||||||
|
|
||||||
const MAX_IMAGES = 4
|
const MAX_IMAGES = 4
|
||||||
|
|
||||||
type CancelRef = {
|
type CancelRef = {
|
||||||
@@ -613,296 +616,313 @@ export const ComposePost = ({
|
|||||||
const keyboardVerticalOffset = useKeyboardVerticalOffset()
|
const keyboardVerticalOffset = useKeyboardVerticalOffset()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<KeyboardAvoidingView
|
<Portal.Provider>
|
||||||
testID="composePostView"
|
<KeyboardAvoidingView
|
||||||
behavior={isIOS ? 'padding' : 'height'}
|
testID="composePostView"
|
||||||
keyboardVerticalOffset={keyboardVerticalOffset}
|
behavior={isIOS ? 'padding' : 'height'}
|
||||||
style={a.flex_1}>
|
keyboardVerticalOffset={keyboardVerticalOffset}
|
||||||
<View style={[a.flex_1, viewStyles]} aria-modal accessibilityViewIsModal>
|
style={a.flex_1}>
|
||||||
<Animated.View
|
<View
|
||||||
style={topBarAnimatedStyle}
|
style={[a.flex_1, viewStyles]}
|
||||||
layout={native(LinearTransition)}>
|
aria-modal
|
||||||
<View style={styles.topbarInner}>
|
accessibilityViewIsModal>
|
||||||
<Button
|
<Animated.View
|
||||||
label={_(msg`Cancel`)}
|
style={topBarAnimatedStyle}
|
||||||
variant="ghost"
|
layout={native(LinearTransition)}>
|
||||||
color="primary"
|
<View style={styles.topbarInner}>
|
||||||
shape="default"
|
<Button
|
||||||
size="small"
|
label={_(msg`Cancel`)}
|
||||||
style={[
|
variant="ghost"
|
||||||
a.rounded_full,
|
color="primary"
|
||||||
a.py_sm,
|
shape="default"
|
||||||
{paddingLeft: 7, paddingRight: 7},
|
size="small"
|
||||||
]}
|
style={[
|
||||||
onPress={onPressCancel}
|
a.rounded_full,
|
||||||
accessibilityHint={_(
|
a.py_sm,
|
||||||
msg`Closes post composer and discards post draft`,
|
{paddingLeft: 7, paddingRight: 7},
|
||||||
)}>
|
]}
|
||||||
<ButtonText style={[a.text_md]}>
|
onPress={onPressCancel}
|
||||||
<Trans>Cancel</Trans>
|
accessibilityHint={_(
|
||||||
</ButtonText>
|
msg`Closes post composer and discards post draft`,
|
||||||
</Button>
|
)}>
|
||||||
<View style={a.flex_1} />
|
<ButtonText style={[a.text_md]}>
|
||||||
{isProcessing ? (
|
<Trans>Cancel</Trans>
|
||||||
<>
|
</ButtonText>
|
||||||
<Text style={pal.textLight}>{processingState}</Text>
|
</Button>
|
||||||
<View style={styles.postBtn}>
|
<View style={a.flex_1} />
|
||||||
<ActivityIndicator />
|
{isProcessing ? (
|
||||||
</View>
|
<>
|
||||||
</>
|
<Text style={pal.textLight}>{processingState}</Text>
|
||||||
) : (
|
<View style={styles.postBtn}>
|
||||||
<View style={[styles.postBtnWrapper]}>
|
<ActivityIndicator />
|
||||||
<LabelsBtn
|
|
||||||
labels={labels}
|
|
||||||
onChange={setLabels}
|
|
||||||
hasMedia={hasMedia}
|
|
||||||
/>
|
|
||||||
{canPost ? (
|
|
||||||
<Button
|
|
||||||
testID="composerPublishBtn"
|
|
||||||
label={
|
|
||||||
replyTo ? _(msg`Publish reply`) : _(msg`Publish post`)
|
|
||||||
}
|
|
||||||
variant="solid"
|
|
||||||
color="primary"
|
|
||||||
shape="default"
|
|
||||||
size="small"
|
|
||||||
style={[a.rounded_full, a.py_sm]}
|
|
||||||
onPress={() => onPressPublish()}
|
|
||||||
disabled={videoState.status !== 'idle' && publishOnUpload}>
|
|
||||||
<ButtonText style={[a.text_md]}>
|
|
||||||
{replyTo ? (
|
|
||||||
<Trans context="action">Reply</Trans>
|
|
||||||
) : (
|
|
||||||
<Trans context="action">Post</Trans>
|
|
||||||
)}
|
|
||||||
</ButtonText>
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<View style={[styles.postBtn, pal.btn]}>
|
|
||||||
<Text style={[pal.textLight, s.f16, s.bold]}>
|
|
||||||
<Trans context="action">Post</Trans>
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
)}
|
</>
|
||||||
|
) : (
|
||||||
|
<View style={[styles.postBtnWrapper]}>
|
||||||
|
<LabelsBtn
|
||||||
|
labels={labels}
|
||||||
|
onChange={setLabels}
|
||||||
|
hasMedia={hasMedia}
|
||||||
|
/>
|
||||||
|
{canPost ? (
|
||||||
|
<Button
|
||||||
|
testID="composerPublishBtn"
|
||||||
|
label={
|
||||||
|
replyTo ? _(msg`Publish reply`) : _(msg`Publish post`)
|
||||||
|
}
|
||||||
|
variant="solid"
|
||||||
|
color="primary"
|
||||||
|
shape="default"
|
||||||
|
size="small"
|
||||||
|
style={[a.rounded_full, a.py_sm]}
|
||||||
|
onPress={() => onPressPublish()}
|
||||||
|
disabled={
|
||||||
|
videoState.status !== 'idle' && publishOnUpload
|
||||||
|
}>
|
||||||
|
<ButtonText style={[a.text_md]}>
|
||||||
|
{replyTo ? (
|
||||||
|
<Trans context="action">Reply</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans context="action">Post</Trans>
|
||||||
|
)}
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<View style={[styles.postBtn, pal.btn]}>
|
||||||
|
<Text style={[pal.textLight, s.f16, s.bold]}>
|
||||||
|
<Trans context="action">Post</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{isAltTextRequiredAndMissing && (
|
||||||
|
<View style={[styles.reminderLine, pal.viewLight]}>
|
||||||
|
<View style={styles.errorIcon}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="exclamation"
|
||||||
|
style={{color: colors.red4}}
|
||||||
|
size={10}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<Text style={[pal.text, a.flex_1]}>
|
||||||
|
<Trans>One or more images is missing alt text.</Trans>
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
<ErrorBanner
|
||||||
|
error={error}
|
||||||
{isAltTextRequiredAndMissing && (
|
videoState={videoState}
|
||||||
<View style={[styles.reminderLine, pal.viewLight]}>
|
clearError={() => setError('')}
|
||||||
<View style={styles.errorIcon}>
|
clearVideo={clearVideo}
|
||||||
<FontAwesomeIcon
|
|
||||||
icon="exclamation"
|
|
||||||
style={{color: colors.red4}}
|
|
||||||
size={10}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<Text style={[pal.text, a.flex_1]}>
|
|
||||||
<Trans>One or more images is missing alt text.</Trans>
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
<ErrorBanner
|
|
||||||
error={error}
|
|
||||||
videoState={videoState}
|
|
||||||
clearError={() => setError('')}
|
|
||||||
clearVideo={clearVideo}
|
|
||||||
/>
|
|
||||||
</Animated.View>
|
|
||||||
<Animated.ScrollView
|
|
||||||
layout={native(LinearTransition)}
|
|
||||||
onScroll={scrollHandler}
|
|
||||||
style={styles.scrollView}
|
|
||||||
keyboardShouldPersistTaps="always"
|
|
||||||
onContentSizeChange={onScrollViewContentSizeChange}
|
|
||||||
onLayout={onScrollViewLayout}>
|
|
||||||
{replyTo ? <ComposerReplyTo replyTo={replyTo} /> : undefined}
|
|
||||||
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.textInputLayout,
|
|
||||||
isNative && styles.textInputLayoutMobile,
|
|
||||||
]}>
|
|
||||||
<UserAvatar
|
|
||||||
avatar={currentProfile?.avatar}
|
|
||||||
size={50}
|
|
||||||
type={currentProfile?.associated?.labeler ? 'labeler' : 'user'}
|
|
||||||
/>
|
/>
|
||||||
<TextInput
|
</Animated.View>
|
||||||
ref={textInput}
|
<Animated.ScrollView
|
||||||
richtext={richtext}
|
layout={native(LinearTransition)}
|
||||||
placeholder={selectTextInputPlaceholder}
|
onScroll={scrollHandler}
|
||||||
autoFocus
|
style={styles.scrollView}
|
||||||
setRichText={setRichText}
|
keyboardShouldPersistTaps="always"
|
||||||
onPhotoPasted={onPhotoPasted}
|
onContentSizeChange={onScrollViewContentSizeChange}
|
||||||
onPressPublish={() => onPressPublish()}
|
onLayout={onScrollViewLayout}>
|
||||||
onNewLink={onNewLink}
|
{replyTo ? <ComposerReplyTo replyTo={replyTo} /> : undefined}
|
||||||
onError={setError}
|
|
||||||
accessible={true}
|
|
||||||
accessibilityLabel={_(msg`Write post`)}
|
|
||||||
accessibilityHint={_(
|
|
||||||
msg`Compose posts up to ${MAX_GRAPHEME_LENGTH} characters in length`,
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Gallery images={images} dispatch={dispatch} />
|
<View
|
||||||
{images.length === 0 && extLink && (
|
style={[
|
||||||
<View style={a.relative}>
|
styles.textInputLayout,
|
||||||
<ExternalEmbed
|
isNative && styles.textInputLayoutMobile,
|
||||||
link={extLink}
|
]}>
|
||||||
gif={extGif}
|
<UserAvatar
|
||||||
onRemove={() => {
|
avatar={currentProfile?.avatar}
|
||||||
if (extGif) {
|
size={50}
|
||||||
dispatch({type: 'embed_remove_gif'})
|
type={currentProfile?.associated?.labeler ? 'labeler' : 'user'}
|
||||||
} else {
|
|
||||||
dispatch({type: 'embed_remove_link'})
|
|
||||||
}
|
|
||||||
setExtLink(undefined)
|
|
||||||
setExtGif(undefined)
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<GifAltText
|
<TextInput
|
||||||
link={extLink}
|
ref={textInput}
|
||||||
gif={extGif}
|
richtext={richtext}
|
||||||
onSubmit={handleChangeGifAltText}
|
placeholder={selectTextInputPlaceholder}
|
||||||
|
autoFocus
|
||||||
|
setRichText={setRichText}
|
||||||
|
onPhotoPasted={onPhotoPasted}
|
||||||
|
onPressPublish={() => onPressPublish()}
|
||||||
|
onNewLink={onNewLink}
|
||||||
|
onError={setError}
|
||||||
|
accessible={true}
|
||||||
|
accessibilityLabel={_(msg`Write post`)}
|
||||||
|
accessibilityHint={_(
|
||||||
|
msg`Compose posts up to ${MAX_GRAPHEME_LENGTH} characters in length`,
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)}
|
|
||||||
<LayoutAnimationConfig skipExiting>
|
<Gallery
|
||||||
{hasVideo && (
|
images={images}
|
||||||
<Animated.View
|
dispatch={dispatch}
|
||||||
style={[a.w_full, a.mt_lg]}
|
Portal={Portal.Portal}
|
||||||
entering={native(ZoomIn)}
|
/>
|
||||||
exiting={native(ZoomOut)}>
|
{images.length === 0 && extLink && (
|
||||||
{videoState.asset &&
|
<View style={a.relative}>
|
||||||
(videoState.status === 'compressing' ? (
|
<ExternalEmbed
|
||||||
<VideoTranscodeProgress
|
link={extLink}
|
||||||
asset={videoState.asset}
|
gif={extGif}
|
||||||
progress={videoState.progress}
|
onRemove={() => {
|
||||||
clear={clearVideo}
|
if (extGif) {
|
||||||
/>
|
dispatch({type: 'embed_remove_gif'})
|
||||||
) : videoState.video ? (
|
} else {
|
||||||
<VideoPreview
|
dispatch({type: 'embed_remove_link'})
|
||||||
asset={videoState.asset}
|
}
|
||||||
video={videoState.video}
|
setExtLink(undefined)
|
||||||
setDimensions={updateVideoDimensions}
|
setExtGif(undefined)
|
||||||
clear={clearVideo}
|
|
||||||
/>
|
|
||||||
) : null)}
|
|
||||||
<SubtitleDialogBtn
|
|
||||||
defaultAltText={videoState.altText}
|
|
||||||
saveAltText={altText =>
|
|
||||||
dispatch({
|
|
||||||
type: 'embed_update_video',
|
|
||||||
videoAction: {
|
|
||||||
type: 'update_alt_text',
|
|
||||||
altText,
|
|
||||||
signal: videoState.abortController.signal,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
captions={videoState.captions}
|
|
||||||
setCaptions={updater => {
|
|
||||||
dispatch({
|
|
||||||
type: 'embed_update_video',
|
|
||||||
videoAction: {
|
|
||||||
type: 'update_captions',
|
|
||||||
updater,
|
|
||||||
signal: videoState.abortController.signal,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Animated.View>
|
<GifAltText
|
||||||
)}
|
link={extLink}
|
||||||
</LayoutAnimationConfig>
|
gif={extGif}
|
||||||
<View style={!hasVideo ? [a.mt_md] : []}>
|
onSubmit={handleChangeGifAltText}
|
||||||
{quote ? (
|
Portal={Portal.Portal}
|
||||||
<View style={[s.mt5, s.mb2, isWeb && s.mb10]}>
|
/>
|
||||||
<View style={{pointerEvents: 'none'}}>
|
|
||||||
<QuoteEmbed quote={quote} />
|
|
||||||
</View>
|
|
||||||
{quote.uri !== initQuote?.uri && (
|
|
||||||
<QuoteX
|
|
||||||
onRemove={() => {
|
|
||||||
dispatch({type: 'embed_remove_quote'})
|
|
||||||
setQuote(undefined)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
)}
|
||||||
</View>
|
<LayoutAnimationConfig skipExiting>
|
||||||
</Animated.ScrollView>
|
{hasVideo && (
|
||||||
<SuggestedLanguage text={richtext.text} />
|
<Animated.View
|
||||||
|
style={[a.w_full, a.mt_lg]}
|
||||||
{replyTo ? null : (
|
entering={native(ZoomIn)}
|
||||||
<ThreadgateBtn
|
exiting={native(ZoomOut)}>
|
||||||
postgate={postgate}
|
{videoState.asset &&
|
||||||
onChangePostgate={setPostgate}
|
(videoState.status === 'compressing' ? (
|
||||||
threadgateAllowUISettings={threadgateAllowUISettings}
|
<VideoTranscodeProgress
|
||||||
onChangeThreadgateAllowUISettings={
|
asset={videoState.asset}
|
||||||
onChangeThreadgateAllowUISettings
|
progress={videoState.progress}
|
||||||
}
|
clear={clearVideo}
|
||||||
style={bottomBarAnimatedStyle}
|
/>
|
||||||
/>
|
) : videoState.video ? (
|
||||||
)}
|
<VideoPreview
|
||||||
<View
|
asset={videoState.asset}
|
||||||
style={[
|
video={videoState.video}
|
||||||
t.atoms.bg,
|
setDimensions={updateVideoDimensions}
|
||||||
t.atoms.border_contrast_medium,
|
clear={clearVideo}
|
||||||
styles.bottomBar,
|
/>
|
||||||
]}>
|
) : null)}
|
||||||
{videoState.status !== 'idle' && videoState.status !== 'done' ? (
|
<SubtitleDialogBtn
|
||||||
<VideoUploadToolbar state={videoState} />
|
defaultAltText={videoState.altText}
|
||||||
) : (
|
saveAltText={altText =>
|
||||||
<ToolbarWrapper style={[a.flex_row, a.align_center, a.gap_xs]}>
|
dispatch({
|
||||||
<SelectPhotoBtn
|
type: 'embed_update_video',
|
||||||
size={images.length}
|
videoAction: {
|
||||||
disabled={!canSelectImages}
|
type: 'update_alt_text',
|
||||||
onAdd={onImageAdd}
|
altText,
|
||||||
/>
|
signal: videoState.abortController.signal,
|
||||||
<SelectVideoBtn
|
},
|
||||||
onSelectVideo={selectVideo}
|
})
|
||||||
disabled={!canSelectImages || images?.length > 0}
|
}
|
||||||
setError={setError}
|
captions={videoState.captions}
|
||||||
/>
|
setCaptions={updater => {
|
||||||
<OpenCameraBtn disabled={!canSelectImages} onAdd={onImageAdd} />
|
dispatch({
|
||||||
<SelectGifBtn
|
type: 'embed_update_video',
|
||||||
onClose={focusTextInput}
|
videoAction: {
|
||||||
onSelectGif={onSelectGif}
|
type: 'update_captions',
|
||||||
disabled={hasMedia}
|
updater,
|
||||||
/>
|
signal: videoState.abortController.signal,
|
||||||
{!isMobile ? (
|
},
|
||||||
<Button
|
})
|
||||||
onPress={onEmojiButtonPress}
|
}}
|
||||||
style={a.p_sm}
|
Portal={Portal.Portal}
|
||||||
label={_(msg`Open emoji picker`)}
|
/>
|
||||||
accessibilityHint={_(msg`Open emoji picker`)}
|
</Animated.View>
|
||||||
variant="ghost"
|
)}
|
||||||
shape="round"
|
</LayoutAnimationConfig>
|
||||||
color="primary">
|
<View style={!hasVideo ? [a.mt_md] : []}>
|
||||||
<EmojiSmile size="lg" />
|
{quote ? (
|
||||||
</Button>
|
<View style={[s.mt5, s.mb2, isWeb && s.mb10]}>
|
||||||
|
<View style={{pointerEvents: 'none'}}>
|
||||||
|
<QuoteEmbed quote={quote} />
|
||||||
|
</View>
|
||||||
|
{quote.uri !== initQuote?.uri && (
|
||||||
|
<QuoteX
|
||||||
|
onRemove={() => {
|
||||||
|
dispatch({type: 'embed_remove_quote'})
|
||||||
|
setQuote(undefined)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
</ToolbarWrapper>
|
</View>
|
||||||
|
</Animated.ScrollView>
|
||||||
|
<SuggestedLanguage text={richtext.text} />
|
||||||
|
|
||||||
|
{replyTo ? null : (
|
||||||
|
<ThreadgateBtn
|
||||||
|
postgate={postgate}
|
||||||
|
onChangePostgate={setPostgate}
|
||||||
|
threadgateAllowUISettings={threadgateAllowUISettings}
|
||||||
|
onChangeThreadgateAllowUISettings={
|
||||||
|
onChangeThreadgateAllowUISettings
|
||||||
|
}
|
||||||
|
style={bottomBarAnimatedStyle}
|
||||||
|
Portal={Portal.Portal}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<View style={a.flex_1} />
|
<View
|
||||||
<SelectLangBtn />
|
style={[
|
||||||
<CharProgress count={graphemeLength} />
|
t.atoms.bg,
|
||||||
|
t.atoms.border_contrast_medium,
|
||||||
|
styles.bottomBar,
|
||||||
|
]}>
|
||||||
|
{videoState.status !== 'idle' && videoState.status !== 'done' ? (
|
||||||
|
<VideoUploadToolbar state={videoState} />
|
||||||
|
) : (
|
||||||
|
<ToolbarWrapper style={[a.flex_row, a.align_center, a.gap_xs]}>
|
||||||
|
<SelectPhotoBtn
|
||||||
|
size={images.length}
|
||||||
|
disabled={!canSelectImages}
|
||||||
|
onAdd={onImageAdd}
|
||||||
|
/>
|
||||||
|
<SelectVideoBtn
|
||||||
|
onSelectVideo={selectVideo}
|
||||||
|
disabled={!canSelectImages || images?.length > 0}
|
||||||
|
setError={setError}
|
||||||
|
/>
|
||||||
|
<OpenCameraBtn disabled={!canSelectImages} onAdd={onImageAdd} />
|
||||||
|
<SelectGifBtn
|
||||||
|
onClose={focusTextInput}
|
||||||
|
onSelectGif={onSelectGif}
|
||||||
|
disabled={hasMedia}
|
||||||
|
Portal={Portal.Portal}
|
||||||
|
/>
|
||||||
|
{!isMobile ? (
|
||||||
|
<Button
|
||||||
|
onPress={onEmojiButtonPress}
|
||||||
|
style={a.p_sm}
|
||||||
|
label={_(msg`Open emoji picker`)}
|
||||||
|
accessibilityHint={_(msg`Open emoji picker`)}
|
||||||
|
variant="ghost"
|
||||||
|
shape="round"
|
||||||
|
color="primary">
|
||||||
|
<EmojiSmile size="lg" />
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</ToolbarWrapper>
|
||||||
|
)}
|
||||||
|
<View style={a.flex_1} />
|
||||||
|
<SelectLangBtn />
|
||||||
|
<CharProgress count={graphemeLength} />
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
<Prompt.Basic
|
||||||
<Prompt.Basic
|
control={discardPromptControl}
|
||||||
control={discardPromptControl}
|
title={_(msg`Discard draft?`)}
|
||||||
title={_(msg`Discard draft?`)}
|
description={_(msg`Are you sure you'd like to discard this draft?`)}
|
||||||
description={_(msg`Are you sure you'd like to discard this draft?`)}
|
onConfirm={onClose}
|
||||||
onConfirm={onClose}
|
confirmButtonCta={_(msg`Discard`)}
|
||||||
confirmButtonCta={_(msg`Discard`)}
|
confirmButtonColor="negative"
|
||||||
confirmButtonColor="negative"
|
Portal={Portal.Portal}
|
||||||
/>
|
/>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
|
<Portal.Outlet />
|
||||||
|
</Portal.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import * as Dialog from '#/components/Dialog'
|
|||||||
import * as TextField from '#/components/forms/TextField'
|
import * as TextField from '#/components/forms/TextField'
|
||||||
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||||
import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||||
|
import {PortalComponent} from '#/components/Portal'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {GifEmbed} from '../util/post-embeds/GifEmbed'
|
import {GifEmbed} from '../util/post-embeds/GifEmbed'
|
||||||
import {AltTextReminder} from './photos/Gallery'
|
import {AltTextReminder} from './photos/Gallery'
|
||||||
@@ -28,10 +29,12 @@ export function GifAltText({
|
|||||||
link: linkProp,
|
link: linkProp,
|
||||||
gif,
|
gif,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
Portal,
|
||||||
}: {
|
}: {
|
||||||
link: ExternalEmbedDraft
|
link: ExternalEmbedDraft
|
||||||
gif?: Gif
|
gif?: Gif
|
||||||
onSubmit: (alt: string) => void
|
onSubmit: (alt: string) => void
|
||||||
|
Portal: PortalComponent
|
||||||
}) {
|
}) {
|
||||||
const control = Dialog.useDialogControl()
|
const control = Dialog.useDialogControl()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
@@ -96,9 +99,7 @@ export function GifAltText({
|
|||||||
|
|
||||||
<AltTextReminder />
|
<AltTextReminder />
|
||||||
|
|
||||||
<Dialog.Outer
|
<Dialog.Outer control={control} Portal={Portal}>
|
||||||
control={control}
|
|
||||||
nativeOptions={isAndroid ? {sheet: {snapPoints: ['100%']}} : {}}>
|
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
<AltTextInner
|
<AltTextInner
|
||||||
onSubmit={onPressSubmit}
|
onSubmit={onPressSubmit}
|
||||||
@@ -185,6 +186,8 @@ function AltTextInner({
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<Dialog.Close />
|
<Dialog.Close />
|
||||||
|
{/* Maybe fix this later -h */}
|
||||||
|
{isAndroid ? <View style={{height: 300}} /> : null}
|
||||||
</Dialog.ScrollableInner>
|
</Dialog.ScrollableInner>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {EditImageDialogProps} from './EditImageDialog'
|
|||||||
export const EditImageDialog = (props: EditImageDialogProps) => {
|
export const EditImageDialog = (props: EditImageDialogProps) => {
|
||||||
return (
|
return (
|
||||||
<Dialog.Outer control={props.control}>
|
<Dialog.Outer control={props.control}>
|
||||||
|
<Dialog.Handle />
|
||||||
<EditImageInner key={props.image.source.id} {...props} />
|
<EditImageInner key={props.image.source.id} {...props} />
|
||||||
</Dialog.Outer>
|
</Dialog.Outer>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {ComposerImage, cropImage} from '#/state/gallery'
|
|||||||
import {Text} from '#/view/com/util/text/Text'
|
import {Text} from '#/view/com/util/text/Text'
|
||||||
import {useTheme} from '#/alf'
|
import {useTheme} from '#/alf'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
|
import {PortalComponent} from '#/components/Portal'
|
||||||
import {ComposerAction} from '../state/composer'
|
import {ComposerAction} from '../state/composer'
|
||||||
import {EditImageDialog} from './EditImageDialog'
|
import {EditImageDialog} from './EditImageDialog'
|
||||||
import {ImageAltTextDialog} from './ImageAltTextDialog'
|
import {ImageAltTextDialog} from './ImageAltTextDialog'
|
||||||
@@ -30,6 +31,7 @@ const IMAGE_GAP = 8
|
|||||||
interface GalleryProps {
|
interface GalleryProps {
|
||||||
images: ComposerImage[]
|
images: ComposerImage[]
|
||||||
dispatch: (action: ComposerAction) => void
|
dispatch: (action: ComposerAction) => void
|
||||||
|
Portal: PortalComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
export let Gallery = (props: GalleryProps): React.ReactNode => {
|
export let Gallery = (props: GalleryProps): React.ReactNode => {
|
||||||
@@ -57,7 +59,12 @@ interface GalleryInnerProps extends GalleryProps {
|
|||||||
containerInfo: Dimensions
|
containerInfo: Dimensions
|
||||||
}
|
}
|
||||||
|
|
||||||
const GalleryInner = ({images, containerInfo, dispatch}: GalleryInnerProps) => {
|
const GalleryInner = ({
|
||||||
|
images,
|
||||||
|
containerInfo,
|
||||||
|
dispatch,
|
||||||
|
Portal,
|
||||||
|
}: GalleryInnerProps) => {
|
||||||
const {isMobile} = useWebMediaQueries()
|
const {isMobile} = useWebMediaQueries()
|
||||||
|
|
||||||
const {altTextControlStyle, imageControlsStyle, imageStyle} =
|
const {altTextControlStyle, imageControlsStyle, imageStyle} =
|
||||||
@@ -111,6 +118,7 @@ const GalleryInner = ({images, containerInfo, dispatch}: GalleryInnerProps) => {
|
|||||||
onRemove={() => {
|
onRemove={() => {
|
||||||
dispatch({type: 'embed_remove_image', image})
|
dispatch({type: 'embed_remove_image', image})
|
||||||
}}
|
}}
|
||||||
|
Portal={Portal}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@@ -127,6 +135,7 @@ type GalleryItemProps = {
|
|||||||
imageStyle?: ViewStyle
|
imageStyle?: ViewStyle
|
||||||
onChange: (next: ComposerImage) => void
|
onChange: (next: ComposerImage) => void
|
||||||
onRemove: () => void
|
onRemove: () => void
|
||||||
|
Portal: PortalComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
const GalleryItem = ({
|
const GalleryItem = ({
|
||||||
@@ -136,6 +145,7 @@ const GalleryItem = ({
|
|||||||
imageStyle,
|
imageStyle,
|
||||||
onChange,
|
onChange,
|
||||||
onRemove,
|
onRemove,
|
||||||
|
Portal,
|
||||||
}: GalleryItemProps): React.ReactNode => {
|
}: GalleryItemProps): React.ReactNode => {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
@@ -230,6 +240,7 @@ const GalleryItem = ({
|
|||||||
control={altTextControl}
|
control={altTextControl}
|
||||||
image={image}
|
image={image}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
Portal={Portal}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<EditImageDialog
|
<EditImageDialog
|
||||||
|
|||||||
@@ -5,25 +5,26 @@ import {msg, Trans} from '@lingui/macro'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {MAX_ALT_TEXT} from '#/lib/constants'
|
import {MAX_ALT_TEXT} from '#/lib/constants'
|
||||||
import {isWeb} from '#/platform/detection'
|
import {isAndroid, isWeb} from '#/platform/detection'
|
||||||
import {ComposerImage} from '#/state/gallery'
|
import {ComposerImage} from '#/state/gallery'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Button, ButtonText} from '#/components/Button'
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import * as TextField from '#/components/forms/TextField'
|
import * as TextField from '#/components/forms/TextField'
|
||||||
|
import {PortalComponent} from '#/components/Portal'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
control: Dialog.DialogOuterProps['control']
|
control: Dialog.DialogOuterProps['control']
|
||||||
image: ComposerImage
|
image: ComposerImage
|
||||||
onChange: (next: ComposerImage) => void
|
onChange: (next: ComposerImage) => void
|
||||||
|
Portal: PortalComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ImageAltTextDialog = (props: Props): React.ReactNode => {
|
export const ImageAltTextDialog = (props: Props): React.ReactNode => {
|
||||||
return (
|
return (
|
||||||
<Dialog.Outer control={props.control}>
|
<Dialog.Outer control={props.control} Portal={props.Portal}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
|
|
||||||
<ImageAltTextInner {...props} />
|
<ImageAltTextInner {...props} />
|
||||||
</Dialog.Outer>
|
</Dialog.Outer>
|
||||||
)
|
)
|
||||||
@@ -116,6 +117,8 @@ const ImageAltTextInner = ({
|
|||||||
</ButtonText>
|
</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
|
{/* Maybe fix this later -h */}
|
||||||
|
{isAndroid ? <View style={{height: 300}} /> : null}
|
||||||
</Dialog.ScrollableInner>
|
</Dialog.ScrollableInner>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,14 +9,16 @@ import {atoms as a, useTheme} from '#/alf'
|
|||||||
import {Button} from '#/components/Button'
|
import {Button} from '#/components/Button'
|
||||||
import {GifSelectDialog} from '#/components/dialogs/GifSelect'
|
import {GifSelectDialog} from '#/components/dialogs/GifSelect'
|
||||||
import {GifSquare_Stroke2_Corner0_Rounded as GifIcon} from '#/components/icons/Gif'
|
import {GifSquare_Stroke2_Corner0_Rounded as GifIcon} from '#/components/icons/Gif'
|
||||||
|
import {PortalComponent} from '#/components/Portal'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onSelectGif: (gif: Gif) => void
|
onSelectGif: (gif: Gif) => void
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
Portal?: PortalComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SelectGifBtn({onClose, onSelectGif, disabled}: Props) {
|
export function SelectGifBtn({onClose, onSelectGif, disabled, Portal}: Props) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const ref = useRef<{open: () => void}>(null)
|
const ref = useRef<{open: () => void}>(null)
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
@@ -46,6 +48,7 @@ export function SelectGifBtn({onClose, onSelectGif, disabled}: Props) {
|
|||||||
controlRef={ref}
|
controlRef={ref}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onSelectGif={onSelectGif}
|
onSelectGif={onSelectGif}
|
||||||
|
Portal={Portal}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {isNative} from '#/platform/detection'
|
|||||||
import {ComposerImage, createComposerImage} from '#/state/gallery'
|
import {ComposerImage, createComposerImage} from '#/state/gallery'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Button} from '#/components/Button'
|
import {Button} from '#/components/Button'
|
||||||
|
import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
|
||||||
import {Image_Stroke2_Corner0_Rounded as Image} from '#/components/icons/Image'
|
import {Image_Stroke2_Corner0_Rounded as Image} from '#/components/icons/Image'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -21,23 +22,26 @@ export function SelectPhotoBtn({size, disabled, onAdd}: Props) {
|
|||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
|
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
const sheetWrapper = useSheetWrapper()
|
||||||
|
|
||||||
const onPressSelectPhotos = useCallback(async () => {
|
const onPressSelectPhotos = useCallback(async () => {
|
||||||
if (isNative && !(await requestPhotoAccessIfNeeded())) {
|
if (isNative && !(await requestPhotoAccessIfNeeded())) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const images = await openPicker({
|
const images = await sheetWrapper(
|
||||||
selectionLimit: 4 - size,
|
openPicker({
|
||||||
allowsMultipleSelection: true,
|
selectionLimit: 4 - size,
|
||||||
})
|
allowsMultipleSelection: true,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
const results = await Promise.all(
|
const results = await Promise.all(
|
||||||
images.map(img => createComposerImage(img)),
|
images.map(img => createComposerImage(img)),
|
||||||
)
|
)
|
||||||
|
|
||||||
onAdd(results)
|
onAdd(results)
|
||||||
}, [requestPhotoAccessIfNeeded, size, onAdd])
|
}, [requestPhotoAccessIfNeeded, size, onAdd, sheetWrapper])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import * as Dialog from '#/components/Dialog'
|
|||||||
import {PostInteractionSettingsControlledDialog} from '#/components/dialogs/PostInteractionSettingsDialog'
|
import {PostInteractionSettingsControlledDialog} from '#/components/dialogs/PostInteractionSettingsDialog'
|
||||||
import {Earth_Stroke2_Corner0_Rounded as Earth} from '#/components/icons/Globe'
|
import {Earth_Stroke2_Corner0_Rounded as Earth} from '#/components/icons/Globe'
|
||||||
import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
|
import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
|
||||||
|
import {PortalComponent} from '#/components/Portal'
|
||||||
|
|
||||||
export function ThreadgateBtn({
|
export function ThreadgateBtn({
|
||||||
postgate,
|
postgate,
|
||||||
@@ -20,6 +21,7 @@ export function ThreadgateBtn({
|
|||||||
threadgateAllowUISettings,
|
threadgateAllowUISettings,
|
||||||
onChangeThreadgateAllowUISettings,
|
onChangeThreadgateAllowUISettings,
|
||||||
style,
|
style,
|
||||||
|
Portal,
|
||||||
}: {
|
}: {
|
||||||
postgate: AppBskyFeedPostgate.Record
|
postgate: AppBskyFeedPostgate.Record
|
||||||
onChangePostgate: (v: AppBskyFeedPostgate.Record) => void
|
onChangePostgate: (v: AppBskyFeedPostgate.Record) => void
|
||||||
@@ -28,6 +30,8 @@ export function ThreadgateBtn({
|
|||||||
onChangeThreadgateAllowUISettings: (v: ThreadgateAllowUISetting[]) => void
|
onChangeThreadgateAllowUISettings: (v: ThreadgateAllowUISetting[]) => void
|
||||||
|
|
||||||
style?: StyleProp<AnimatedStyle<ViewStyle>>
|
style?: StyleProp<AnimatedStyle<ViewStyle>>
|
||||||
|
|
||||||
|
Portal: PortalComponent
|
||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
@@ -77,6 +81,7 @@ export function ThreadgateBtn({
|
|||||||
onChangePostgate={onChangePostgate}
|
onChangePostgate={onChangePostgate}
|
||||||
threadgateAllowUISettings={threadgateAllowUISettings}
|
threadgateAllowUISettings={threadgateAllowUISettings}
|
||||||
onChangeThreadgateAllowUISettings={onChangeThreadgateAllowUISettings}
|
onChangeThreadgateAllowUISettings={onChangeThreadgateAllowUISettings}
|
||||||
|
Portal={Portal}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {useLingui} from '@lingui/react'
|
|||||||
import {MAX_ALT_TEXT} from '#/lib/constants'
|
import {MAX_ALT_TEXT} from '#/lib/constants'
|
||||||
import {useEnforceMaxGraphemeCount} from '#/lib/strings/helpers'
|
import {useEnforceMaxGraphemeCount} from '#/lib/strings/helpers'
|
||||||
import {LANGUAGES} from '#/locale/languages'
|
import {LANGUAGES} from '#/locale/languages'
|
||||||
import {isAndroid, isWeb} from '#/platform/detection'
|
import {isWeb} from '#/platform/detection'
|
||||||
import {useLanguagePrefs} from '#/state/preferences'
|
import {useLanguagePrefs} from '#/state/preferences'
|
||||||
import {atoms as a, useTheme, web} from '#/alf'
|
import {atoms as a, useTheme, web} from '#/alf'
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
@@ -17,6 +17,7 @@ import {CC_Stroke2_Corner0_Rounded as CCIcon} from '#/components/icons/CC'
|
|||||||
import {PageText_Stroke2_Corner0_Rounded as PageTextIcon} from '#/components/icons/PageText'
|
import {PageText_Stroke2_Corner0_Rounded as PageTextIcon} from '#/components/icons/PageText'
|
||||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||||
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
|
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
|
||||||
|
import {PortalComponent} from '#/components/Portal'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {SubtitleFilePicker} from './SubtitleFilePicker'
|
import {SubtitleFilePicker} from './SubtitleFilePicker'
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@ interface Props {
|
|||||||
captions: CaptionsTrack[]
|
captions: CaptionsTrack[]
|
||||||
saveAltText: (altText: string) => void
|
saveAltText: (altText: string) => void
|
||||||
setCaptions: (updater: (prev: CaptionsTrack[]) => CaptionsTrack[]) => void
|
setCaptions: (updater: (prev: CaptionsTrack[]) => CaptionsTrack[]) => void
|
||||||
|
Portal: PortalComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SubtitleDialogBtn(props: Props) {
|
export function SubtitleDialogBtn(props: Props) {
|
||||||
@@ -56,9 +58,7 @@ export function SubtitleDialogBtn(props: Props) {
|
|||||||
{isWeb ? <Trans>Captions & alt text</Trans> : <Trans>Alt text</Trans>}
|
{isWeb ? <Trans>Captions & alt text</Trans> : <Trans>Alt text</Trans>}
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
<Dialog.Outer
|
<Dialog.Outer control={control} Portal={props.Portal}>
|
||||||
control={control}
|
|
||||||
nativeOptions={isAndroid ? {sheet: {snapPoints: ['60%']}} : {}}>
|
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
<SubtitleDialogInner {...props} />
|
<SubtitleDialogInner {...props} />
|
||||||
</Dialog.Outer>
|
</Dialog.Outer>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {isAndroid, isNative, isWeb} from '#/platform/detection'
|
|||||||
import {precacheProfile} from '#/state/queries/profile'
|
import {precacheProfile} from '#/state/queries/profile'
|
||||||
import {HighPriorityImage} from '#/view/com/util/images/Image'
|
import {HighPriorityImage} from '#/view/com/util/images/Image'
|
||||||
import {tokens, useTheme} from '#/alf'
|
import {tokens, useTheme} from '#/alf'
|
||||||
|
import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
|
||||||
import {
|
import {
|
||||||
Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled,
|
Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled,
|
||||||
Camera_Stroke2_Corner0_Rounded as Camera,
|
Camera_Stroke2_Corner0_Rounded as Camera,
|
||||||
@@ -271,6 +272,7 @@ let EditableUserAvatar = ({
|
|||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {requestCameraAccessIfNeeded} = useCameraPermission()
|
const {requestCameraAccessIfNeeded} = useCameraPermission()
|
||||||
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
|
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
|
||||||
|
const sheetWrapper = useSheetWrapper()
|
||||||
|
|
||||||
const aviStyle = useMemo(() => {
|
const aviStyle = useMemo(() => {
|
||||||
if (type === 'algo' || type === 'list') {
|
if (type === 'algo' || type === 'list') {
|
||||||
@@ -306,9 +308,11 @@ let EditableUserAvatar = ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = await openPicker({
|
const items = await sheetWrapper(
|
||||||
aspect: [1, 1],
|
openPicker({
|
||||||
})
|
aspect: [1, 1],
|
||||||
|
}),
|
||||||
|
)
|
||||||
const item = items[0]
|
const item = items[0]
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return
|
return
|
||||||
@@ -332,7 +336,7 @@ let EditableUserAvatar = ({
|
|||||||
logger.error('Failed to crop banner', {error: e})
|
logger.error('Failed to crop banner', {error: e})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [onSelectNewAvatar, requestPhotoAccessIfNeeded])
|
}, [onSelectNewAvatar, requestPhotoAccessIfNeeded, sheetWrapper])
|
||||||
|
|
||||||
const onRemoveAvatar = React.useCallback(() => {
|
const onRemoveAvatar = React.useCallback(() => {
|
||||||
onSelectNewAvatar(null)
|
onSelectNewAvatar(null)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {logger} from '#/logger'
|
|||||||
import {isAndroid, isNative} from '#/platform/detection'
|
import {isAndroid, isNative} from '#/platform/detection'
|
||||||
import {EventStopper} from '#/view/com/util/EventStopper'
|
import {EventStopper} from '#/view/com/util/EventStopper'
|
||||||
import {tokens, useTheme as useAlfTheme} from '#/alf'
|
import {tokens, useTheme as useAlfTheme} from '#/alf'
|
||||||
|
import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
|
||||||
import {
|
import {
|
||||||
Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled,
|
Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled,
|
||||||
Camera_Stroke2_Corner0_Rounded as Camera,
|
Camera_Stroke2_Corner0_Rounded as Camera,
|
||||||
@@ -43,6 +44,7 @@ export function UserBanner({
|
|||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {requestCameraAccessIfNeeded} = useCameraPermission()
|
const {requestCameraAccessIfNeeded} = useCameraPermission()
|
||||||
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
|
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
|
||||||
|
const sheetWrapper = useSheetWrapper()
|
||||||
|
|
||||||
const onOpenCamera = React.useCallback(async () => {
|
const onOpenCamera = React.useCallback(async () => {
|
||||||
if (!(await requestCameraAccessIfNeeded())) {
|
if (!(await requestCameraAccessIfNeeded())) {
|
||||||
@@ -60,7 +62,7 @@ export function UserBanner({
|
|||||||
if (!(await requestPhotoAccessIfNeeded())) {
|
if (!(await requestPhotoAccessIfNeeded())) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const items = await openPicker()
|
const items = await sheetWrapper(openPicker())
|
||||||
if (!items[0]) {
|
if (!items[0]) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -80,7 +82,7 @@ export function UserBanner({
|
|||||||
logger.error('Failed to crop banner', {error: e})
|
logger.error('Failed to crop banner', {error: e})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [onSelectNewBanner, requestPhotoAccessIfNeeded])
|
}, [onSelectNewBanner, requestPhotoAccessIfNeeded, sheetWrapper])
|
||||||
|
|
||||||
const onRemoveBanner = React.useCallback(() => {
|
const onRemoveBanner = React.useCallback(() => {
|
||||||
onSelectNewBanner?.(null)
|
onSelectNewBanner?.(null)
|
||||||
|
|||||||
@@ -240,8 +240,8 @@ let PostDropdownBtn = ({
|
|||||||
Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
|
Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
|
||||||
}, [_, richText])
|
}, [_, richText])
|
||||||
|
|
||||||
const onPressTranslate = React.useCallback(() => {
|
const onPressTranslate = React.useCallback(async () => {
|
||||||
openLink(translatorUrl)
|
await openLink(translatorUrl)
|
||||||
}, [openLink, translatorUrl])
|
}, [openLink, translatorUrl])
|
||||||
|
|
||||||
const onHidePost = React.useCallback(() => {
|
const onHidePost = React.useCallback(() => {
|
||||||
@@ -439,7 +439,7 @@ let PostDropdownBtn = ({
|
|||||||
<Menu.Item
|
<Menu.Item
|
||||||
testID="postDropdownSendViaDMBtn"
|
testID="postDropdownSendViaDMBtn"
|
||||||
label={_(msg`Send via direct message`)}
|
label={_(msg`Send via direct message`)}
|
||||||
onPress={sendViaChatControl.open}>
|
onPress={() => sendViaChatControl.open()}>
|
||||||
<Menu.ItemText>
|
<Menu.ItemText>
|
||||||
<Trans>Send via direct message</Trans>
|
<Trans>Send via direct message</Trans>
|
||||||
</Menu.ItemText>
|
</Menu.ItemText>
|
||||||
@@ -467,7 +467,7 @@ let PostDropdownBtn = ({
|
|||||||
<Menu.Item
|
<Menu.Item
|
||||||
testID="postDropdownEmbedBtn"
|
testID="postDropdownEmbedBtn"
|
||||||
label={_(msg`Embed post`)}
|
label={_(msg`Embed post`)}
|
||||||
onPress={embedPostControl.open}>
|
onPress={() => embedPostControl.open()}>
|
||||||
<Menu.ItemText>{_(msg`Embed post`)}</Menu.ItemText>
|
<Menu.ItemText>{_(msg`Embed post`)}</Menu.ItemText>
|
||||||
<Menu.ItemIcon icon={CodeBrackets} position="right" />
|
<Menu.ItemIcon icon={CodeBrackets} position="right" />
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
@@ -542,7 +542,7 @@ let PostDropdownBtn = ({
|
|||||||
? _(msg`Hide reply for me`)
|
? _(msg`Hide reply for me`)
|
||||||
: _(msg`Hide post for me`)
|
: _(msg`Hide post for me`)
|
||||||
}
|
}
|
||||||
onPress={hidePromptControl.open}>
|
onPress={() => hidePromptControl.open()}>
|
||||||
<Menu.ItemText>
|
<Menu.ItemText>
|
||||||
{isReply
|
{isReply
|
||||||
? _(msg`Hide reply for me`)
|
? _(msg`Hide reply for me`)
|
||||||
@@ -630,7 +630,9 @@ let PostDropdownBtn = ({
|
|||||||
<Menu.Item
|
<Menu.Item
|
||||||
testID="postDropdownEditPostInteractions"
|
testID="postDropdownEditPostInteractions"
|
||||||
label={_(msg`Edit interaction settings`)}
|
label={_(msg`Edit interaction settings`)}
|
||||||
onPress={postInteractionSettingsDialogControl.open}
|
onPress={() =>
|
||||||
|
postInteractionSettingsDialogControl.open()
|
||||||
|
}
|
||||||
{...(isAuthor
|
{...(isAuthor
|
||||||
? Platform.select({
|
? Platform.select({
|
||||||
web: {
|
web: {
|
||||||
@@ -649,7 +651,7 @@ let PostDropdownBtn = ({
|
|||||||
<Menu.Item
|
<Menu.Item
|
||||||
testID="postDropdownDeleteBtn"
|
testID="postDropdownDeleteBtn"
|
||||||
label={_(msg`Delete post`)}
|
label={_(msg`Delete post`)}
|
||||||
onPress={deletePromptControl.open}>
|
onPress={() => deletePromptControl.open()}>
|
||||||
<Menu.ItemText>{_(msg`Delete post`)}</Menu.ItemText>
|
<Menu.ItemText>{_(msg`Delete post`)}</Menu.ItemText>
|
||||||
<Menu.ItemIcon icon={Trash} position="right" />
|
<Menu.ItemIcon icon={Trash} position="right" />
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
|||||||
@@ -86,7 +86,9 @@ let RepostButton = ({
|
|||||||
</Text>
|
</Text>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
</Button>
|
</Button>
|
||||||
<Dialog.Outer control={dialogControl}>
|
<Dialog.Outer
|
||||||
|
control={dialogControl}
|
||||||
|
nativeOptions={{preventExpansion: true}}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
<Dialog.Inner label={_(msg`Repost or quote post`)}>
|
<Dialog.Inner label={_(msg`Repost or quote post`)}>
|
||||||
<View style={a.gap_xl}>
|
<View style={a.gap_xl}>
|
||||||
@@ -155,7 +157,6 @@ let RepostButton = ({
|
|||||||
</View>
|
</View>
|
||||||
<Button
|
<Button
|
||||||
label={_(msg`Cancel quote post`)}
|
label={_(msg`Cancel quote post`)}
|
||||||
onAccessibilityEscape={close}
|
|
||||||
onPress={close}
|
onPress={close}
|
||||||
size="large"
|
size="large"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
|
|||||||
@@ -79,7 +79,6 @@ export function DisableEmail2FADialog({
|
|||||||
return (
|
return (
|
||||||
<Dialog.Outer control={control}>
|
<Dialog.Outer control={control}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
|
|
||||||
<Dialog.ScrollableInner
|
<Dialog.ScrollableInner
|
||||||
accessibilityDescribedBy="dialog-description"
|
accessibilityDescribedBy="dialog-description"
|
||||||
accessibilityLabelledBy="dialog-title">
|
accessibilityLabelledBy="dialog-title">
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ export function ExportCarDialog({
|
|||||||
return (
|
return (
|
||||||
<Dialog.Outer control={control}>
|
<Dialog.Outer control={control}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
|
|
||||||
<Dialog.ScrollableInner
|
<Dialog.ScrollableInner
|
||||||
accessibilityDescribedBy="dialog-description"
|
accessibilityDescribedBy="dialog-description"
|
||||||
accessibilityLabelledBy="dialog-title">
|
accessibilityLabelledBy="dialog-title">
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import React from 'react'
|
|||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {useNavigation} from '@react-navigation/native'
|
import {useNavigation} from '@react-navigation/native'
|
||||||
|
|
||||||
|
import {NavigationProp} from '#/lib/routes/types'
|
||||||
import {useDialogStateControlContext} from '#/state/dialogs'
|
import {useDialogStateControlContext} from '#/state/dialogs'
|
||||||
import {NavigationProp} from 'lib/routes/types'
|
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
import {Button, ButtonText} from '#/components/Button'
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
@@ -179,19 +179,13 @@ export function Dialogs() {
|
|||||||
</Prompt.Outer>
|
</Prompt.Outer>
|
||||||
|
|
||||||
<Dialog.Outer control={basic}>
|
<Dialog.Outer control={basic}>
|
||||||
<Dialog.Handle />
|
|
||||||
|
|
||||||
<Dialog.Inner label="test">
|
<Dialog.Inner label="test">
|
||||||
<H3 nativeID="dialog-title">Dialog</H3>
|
<H3 nativeID="dialog-title">Dialog</H3>
|
||||||
<P nativeID="dialog-description">A basic dialog</P>
|
<P nativeID="dialog-description">A basic dialog</P>
|
||||||
</Dialog.Inner>
|
</Dialog.Inner>
|
||||||
</Dialog.Outer>
|
</Dialog.Outer>
|
||||||
|
|
||||||
<Dialog.Outer
|
<Dialog.Outer control={scrollable}>
|
||||||
control={scrollable}
|
|
||||||
nativeOptions={{sheet: {snapPoints: ['100%']}}}>
|
|
||||||
<Dialog.Handle />
|
|
||||||
|
|
||||||
<Dialog.ScrollableInner
|
<Dialog.ScrollableInner
|
||||||
accessibilityDescribedBy="dialog-description"
|
accessibilityDescribedBy="dialog-description"
|
||||||
accessibilityLabelledBy="dialog-title">
|
accessibilityLabelledBy="dialog-title">
|
||||||
@@ -230,8 +224,6 @@ export function Dialogs() {
|
|||||||
</Dialog.Outer>
|
</Dialog.Outer>
|
||||||
|
|
||||||
<Dialog.Outer control={testDialog}>
|
<Dialog.Outer control={testDialog}>
|
||||||
<Dialog.Handle />
|
|
||||||
|
|
||||||
<Dialog.ScrollableInner
|
<Dialog.ScrollableInner
|
||||||
accessibilityDescribedBy="dialog-description"
|
accessibilityDescribedBy="dialog-description"
|
||||||
accessibilityLabelledBy="dialog-title">
|
accessibilityLabelledBy="dialog-title">
|
||||||
@@ -356,8 +348,6 @@ export function Dialogs() {
|
|||||||
|
|
||||||
{shouldRenderUnmountTest && (
|
{shouldRenderUnmountTest && (
|
||||||
<Dialog.Outer control={unmountTestDialog}>
|
<Dialog.Outer control={unmountTestDialog}>
|
||||||
<Dialog.Handle />
|
|
||||||
|
|
||||||
<Dialog.Inner label="test">
|
<Dialog.Inner label="test">
|
||||||
<H3 nativeID="dialog-title">Unmount Test Dialog</H3>
|
<H3 nativeID="dialog-title">Unmount Test Dialog</H3>
|
||||||
<P nativeID="dialog-description">Will unmount in about 5 seconds</P>
|
<P nativeID="dialog-description">Will unmount in about 5 seconds</P>
|
||||||
|
|||||||
@@ -1,19 +1,28 @@
|
|||||||
import React, {useLayoutEffect} from 'react'
|
import React from 'react'
|
||||||
import {Modal, View} from 'react-native'
|
import {Modal, View} from 'react-native'
|
||||||
import {StatusBar} from 'expo-status-bar'
|
|
||||||
import * as SystemUI from 'expo-system-ui'
|
|
||||||
|
|
||||||
|
import {useDialogStateControlContext} from '#/state/dialogs'
|
||||||
import {useComposerState} from '#/state/shell/composer'
|
import {useComposerState} from '#/state/shell/composer'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {getBackgroundColor, useThemeName} from '#/alf/util/useColorModeTheme'
|
|
||||||
import {ComposePost, useComposerCancelRef} from '../com/composer/Composer'
|
import {ComposePost, useComposerCancelRef} from '../com/composer/Composer'
|
||||||
|
|
||||||
export function Composer({}: {winHeight: number}) {
|
export function Composer({}: {winHeight: number}) {
|
||||||
|
const {setFullyExpandedCount} = useDialogStateControlContext()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const state = useComposerState()
|
const state = useComposerState()
|
||||||
const ref = useComposerCancelRef()
|
const ref = useComposerCancelRef()
|
||||||
|
|
||||||
const open = !!state
|
const open = !!state
|
||||||
|
const prevOpen = React.useRef(open)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (open && !prevOpen.current) {
|
||||||
|
setFullyExpandedCount(c => c + 1)
|
||||||
|
} else if (!open && prevOpen.current) {
|
||||||
|
setFullyExpandedCount(c => c - 1)
|
||||||
|
}
|
||||||
|
prevOpen.current = open
|
||||||
|
}, [open, setFullyExpandedCount])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -24,56 +33,18 @@ export function Composer({}: {winHeight: number}) {
|
|||||||
animationType="slide"
|
animationType="slide"
|
||||||
onRequestClose={() => ref.current?.onPressCancel()}>
|
onRequestClose={() => ref.current?.onPressCancel()}>
|
||||||
<View style={[t.atoms.bg, a.flex_1]}>
|
<View style={[t.atoms.bg, a.flex_1]}>
|
||||||
<Providers open={open}>
|
<ComposePost
|
||||||
<ComposePost
|
cancelRef={ref}
|
||||||
cancelRef={ref}
|
replyTo={state?.replyTo}
|
||||||
replyTo={state?.replyTo}
|
onPost={state?.onPost}
|
||||||
onPost={state?.onPost}
|
quote={state?.quote}
|
||||||
quote={state?.quote}
|
quoteCount={state?.quoteCount}
|
||||||
quoteCount={state?.quoteCount}
|
mention={state?.mention}
|
||||||
mention={state?.mention}
|
text={state?.text}
|
||||||
text={state?.text}
|
imageUris={state?.imageUris}
|
||||||
imageUris={state?.imageUris}
|
videoUri={state?.videoUri}
|
||||||
videoUri={state?.videoUri}
|
/>
|
||||||
/>
|
|
||||||
</Providers>
|
|
||||||
</View>
|
</View>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Providers({
|
|
||||||
children,
|
|
||||||
open,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode
|
|
||||||
open: boolean
|
|
||||||
}) {
|
|
||||||
// on iOS, it's a native formSheet. We use FullWindowOverlay to make
|
|
||||||
// the dialogs appear over it
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{children}
|
|
||||||
<IOSModalBackground active={open} />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generally, the backdrop of the app is the theme color, but when this is open
|
|
||||||
// we want it to be black due to the modal being a form sheet.
|
|
||||||
function IOSModalBackground({active}: {active: boolean}) {
|
|
||||||
const theme = useThemeName()
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
SystemUI.setBackgroundColorAsync('black')
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
SystemUI.setBackgroundColorAsync(getBackgroundColor(theme))
|
|
||||||
}
|
|
||||||
}, [theme])
|
|
||||||
|
|
||||||
// Set the status bar to light - however, only if the modal is active
|
|
||||||
// If we rely on this component being mounted to set this,
|
|
||||||
// there'll be a delay before it switches back to default.
|
|
||||||
return active ? <StatusBar style="light" animated /> : null
|
|
||||||
}
|
|
||||||
|
|||||||
+21
-16
@@ -13,6 +13,14 @@ import * as NavigationBar from 'expo-navigation-bar'
|
|||||||
import {StatusBar} from 'expo-status-bar'
|
import {StatusBar} from 'expo-status-bar'
|
||||||
import {useNavigation, useNavigationState} from '@react-navigation/native'
|
import {useNavigation, useNavigationState} from '@react-navigation/native'
|
||||||
|
|
||||||
|
import {useDedupe} from '#/lib/hooks/useDedupe'
|
||||||
|
import {useNotificationsHandler} from '#/lib/hooks/useNotificationHandler'
|
||||||
|
import {usePalette} from '#/lib/hooks/usePalette'
|
||||||
|
import {useNotificationsRegistration} from '#/lib/notifications/notifications'
|
||||||
|
import {isStateAtTabRoot} from '#/lib/routes/helpers'
|
||||||
|
import {useTheme} from '#/lib/ThemeContext'
|
||||||
|
import {isAndroid, isIOS} from '#/platform/detection'
|
||||||
|
import {useDialogStateControlContext} from '#/state/dialogs'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {
|
import {
|
||||||
useIsDrawerOpen,
|
useIsDrawerOpen,
|
||||||
@@ -20,17 +28,9 @@ import {
|
|||||||
useSetDrawerOpen,
|
useSetDrawerOpen,
|
||||||
} from '#/state/shell'
|
} from '#/state/shell'
|
||||||
import {useCloseAnyActiveElement} from '#/state/util'
|
import {useCloseAnyActiveElement} from '#/state/util'
|
||||||
import {useDedupe} from 'lib/hooks/useDedupe'
|
import {Lightbox} from '#/view/com/lightbox/Lightbox'
|
||||||
import {useNotificationsHandler} from 'lib/hooks/useNotificationHandler'
|
import {ModalsContainer} from '#/view/com/modals/Modal'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
|
||||||
import {useNotificationsRegistration} from 'lib/notifications/notifications'
|
|
||||||
import {isStateAtTabRoot} from 'lib/routes/helpers'
|
|
||||||
import {useTheme} from 'lib/ThemeContext'
|
|
||||||
import {isAndroid} from 'platform/detection'
|
|
||||||
import {useDialogStateContext} from 'state/dialogs'
|
|
||||||
import {Lightbox} from 'view/com/lightbox/Lightbox'
|
|
||||||
import {ModalsContainer} from 'view/com/modals/Modal'
|
|
||||||
import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
|
|
||||||
import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
|
import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
|
||||||
import {SigninDialog} from '#/components/dialogs/Signin'
|
import {SigninDialog} from '#/components/dialogs/Signin'
|
||||||
import {Outlet as PortalOutlet} from '#/components/Portal'
|
import {Outlet as PortalOutlet} from '#/components/Portal'
|
||||||
@@ -61,7 +61,6 @@ function ShellInner() {
|
|||||||
const canGoBack = useNavigationState(state => !isStateAtTabRoot(state))
|
const canGoBack = useNavigationState(state => !isStateAtTabRoot(state))
|
||||||
const {hasSession} = useSession()
|
const {hasSession} = useSession()
|
||||||
const closeAnyActiveElement = useCloseAnyActiveElement()
|
const closeAnyActiveElement = useCloseAnyActiveElement()
|
||||||
const {importantForAccessibility} = useDialogStateContext()
|
|
||||||
|
|
||||||
useNotificationsRegistration()
|
useNotificationsRegistration()
|
||||||
useNotificationsHandler()
|
useNotificationsHandler()
|
||||||
@@ -101,9 +100,7 @@ function ShellInner() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Animated.View
|
<Animated.View style={containerPadding}>
|
||||||
style={containerPadding}
|
|
||||||
importantForAccessibility={importantForAccessibility}>
|
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Drawer
|
<Drawer
|
||||||
renderDrawerContent={renderDrawerContent}
|
renderDrawerContent={renderDrawerContent}
|
||||||
@@ -127,6 +124,7 @@ function ShellInner() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Shell: React.FC = function ShellImpl() {
|
export const Shell: React.FC = function ShellImpl() {
|
||||||
|
const {fullyExpandedCount} = useDialogStateControlContext()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -140,7 +138,14 @@ export const Shell: React.FC = function ShellImpl() {
|
|||||||
}, [theme])
|
}, [theme])
|
||||||
return (
|
return (
|
||||||
<View testID="mobileShellView" style={[styles.outerContainer, pal.view]}>
|
<View testID="mobileShellView" style={[styles.outerContainer, pal.view]}>
|
||||||
<StatusBar style={theme.colorScheme === 'dark' ? 'light' : 'dark'} />
|
<StatusBar
|
||||||
|
style={
|
||||||
|
theme.colorScheme === 'dark' || (isIOS && fullyExpandedCount > 0)
|
||||||
|
? 'light'
|
||||||
|
: 'dark'
|
||||||
|
}
|
||||||
|
animated
|
||||||
|
/>
|
||||||
<RoutesContainer>
|
<RoutesContainer>
|
||||||
<ShellInner />
|
<ShellInner />
|
||||||
</RoutesContainer>
|
</RoutesContainer>
|
||||||
|
|||||||
@@ -11450,14 +11450,6 @@ expo-structured-headers@~3.8.0:
|
|||||||
resolved "https://registry.yarnpkg.com/expo-structured-headers/-/expo-structured-headers-3.8.0.tgz#11797a4c3a7a6770b21126cecffcda148030e361"
|
resolved "https://registry.yarnpkg.com/expo-structured-headers/-/expo-structured-headers-3.8.0.tgz#11797a4c3a7a6770b21126cecffcda148030e361"
|
||||||
integrity sha512-R+gFGn0x5CWl4OVlk2j1bJTJIz4KO8mPoCHpRHmfqMjmrMvrOM0qQSY3V5NHXwp1yT/L2v8aUmFQsBRIdvi1XA==
|
integrity sha512-R+gFGn0x5CWl4OVlk2j1bJTJIz4KO8mPoCHpRHmfqMjmrMvrOM0qQSY3V5NHXwp1yT/L2v8aUmFQsBRIdvi1XA==
|
||||||
|
|
||||||
expo-system-ui@~3.0.4:
|
|
||||||
version "3.0.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/expo-system-ui/-/expo-system-ui-3.0.4.tgz#5ace49d38eb03c09a8041b3b82c581a6b974741a"
|
|
||||||
integrity sha512-v1n6hBO30k9qw6RE8/au4yNoovs71ExGuXizJUlR5KSo4Ruogpb+0/2q3uRZMDIYWWCANvms8L0UOh6fQJ5TXg==
|
|
||||||
dependencies:
|
|
||||||
"@react-native/normalize-colors" "~0.74.83"
|
|
||||||
debug "^4.3.2"
|
|
||||||
|
|
||||||
expo-task-manager@~11.8.1:
|
expo-task-manager@~11.8.1:
|
||||||
version "11.8.1"
|
version "11.8.1"
|
||||||
resolved "https://registry.yarnpkg.com/expo-task-manager/-/expo-task-manager-11.8.1.tgz#33089e78ee3fbd83327fb403bce12d69baf7d21b"
|
resolved "https://registry.yarnpkg.com/expo-task-manager/-/expo-task-manager-11.8.1.tgz#33089e78ee3fbd83327fb403bce12d69baf7d21b"
|
||||||
@@ -18057,15 +18049,14 @@ react-native-drawer-layout@^4.0.0-alpha.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
use-latest-callback "^0.1.9"
|
use-latest-callback "^0.1.9"
|
||||||
|
|
||||||
react-native-gesture-handler@~2.16.2:
|
react-native-gesture-handler@2.20.0:
|
||||||
version "2.16.2"
|
version "2.20.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.16.2.tgz#032bd2a07334292d7f6cff1dc9d1ec928f72e26d"
|
resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.20.0.tgz#2d9ec4e9bd22619ebe36269dda3ecb1173928276"
|
||||||
integrity sha512-vGFlrDKlmyI+BT+FemqVxmvO7nqxU33cgXVsn6IKAFishvlG3oV2Ds67D5nPkHMea8T+s1IcuMm0bF8ntZtAyg==
|
integrity sha512-rFKqgHRfxQ7uSAivk8vxCiW4SB3G0U7jnv7kZD4Y90K5kp6YrU8Q3tWhxe3Rx55BIvSd3mBe9ZWbWVJ0FsSHPA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@egjs/hammerjs" "^2.0.17"
|
"@egjs/hammerjs" "^2.0.17"
|
||||||
hoist-non-react-statics "^3.3.0"
|
hoist-non-react-statics "^3.3.0"
|
||||||
invariant "^2.2.4"
|
invariant "^2.2.4"
|
||||||
lodash "^4.17.21"
|
|
||||||
prop-types "^15.7.2"
|
prop-types "^15.7.2"
|
||||||
|
|
||||||
react-native-get-random-values@^1.6.0:
|
react-native-get-random-values@^1.6.0:
|
||||||
@@ -18094,10 +18085,10 @@ react-native-ios-context-menu@^1.15.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@dominicstop/ts-event-emitter" "^1.1.0"
|
"@dominicstop/ts-event-emitter" "^1.1.0"
|
||||||
|
|
||||||
react-native-keyboard-controller@^1.12.1:
|
react-native-keyboard-controller@^1.14.0:
|
||||||
version "1.12.1"
|
version "1.14.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-native-keyboard-controller/-/react-native-keyboard-controller-1.12.1.tgz#6de22ed4d060528a0dd25621eeaa7f71772ce35f"
|
resolved "https://registry.yarnpkg.com/react-native-keyboard-controller/-/react-native-keyboard-controller-1.14.0.tgz#f6faaa12b3736a10f4eec4236ed5b0343508b9a1"
|
||||||
integrity sha512-2OpQcesiYsMilrTzgcTafSGexd9UryRQRuHudIcOn0YaqvvzNpnhVZMVuJMH93fJv/iaZYp3138rgUKOdHhtSw==
|
integrity sha512-JW9k2fehFXOpvLWh1YcgyubLodg/HPi6bR11sCZB/BOawf1tnbGnqk967B8XkxDOKHH6mg+z82quCvv8ALh1rg==
|
||||||
|
|
||||||
react-native-mmkv@^2.12.2:
|
react-native-mmkv@^2.12.2:
|
||||||
version "2.12.2"
|
version "2.12.2"
|
||||||
|
|||||||
Reference in New Issue
Block a user