Merge branch 'main' into static_asset_cdn
This commit is contained in:
@@ -24,6 +24,20 @@
|
||||
*
|
||||
* THIS NEEDS TO BE DUPLICATED IN `bskyweb/templates/base.html`
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'InterVariable';
|
||||
src: url(/static/media/InterVariable.c9f788f6e7ebaec75d7c.ttf) format('truetype');
|
||||
font-weight: 300 1000;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'InterVariableItalic';
|
||||
src: url(/static/media/InterVariable-Italic.55d6a3f35e9b605ba6f4.ttf) format('truetype');
|
||||
font-weight: 300 1000;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
html {
|
||||
background-color: white;
|
||||
scrollbar-gutter: stable both-edges;
|
||||
|
||||
@@ -96,6 +96,11 @@ jest.mock('expo-modules-core', () => ({
|
||||
getIsReducedMotionEnabled: () => false,
|
||||
}
|
||||
}
|
||||
if (moduleName === 'BottomSheet') {
|
||||
return {
|
||||
dismissAll: () => {},
|
||||
}
|
||||
}
|
||||
}),
|
||||
requireNativeViewManager: jest.fn().mockImplementation(moduleName => {
|
||||
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-splash-screen": "~0.27.4",
|
||||
"expo-status-bar": "~1.12.1",
|
||||
"expo-system-ui": "~3.0.4",
|
||||
"expo-task-manager": "~11.8.1",
|
||||
"expo-updates": "~0.25.14",
|
||||
"expo-web-browser": "~13.0.3",
|
||||
@@ -171,11 +170,11 @@
|
||||
"react-native-compressor": "^1.8.24",
|
||||
"react-native-date-picker": "^4.4.2",
|
||||
"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-image-crop-picker": "0.41.2",
|
||||
"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-pager-view": "6.2.3",
|
||||
"react-native-picker-select": "^9.1.3",
|
||||
|
||||
@@ -38,37 +38,65 @@ index b0d71dc..41b9a0e 100644
|
||||
|
||||
- (void)reactBlur
|
||||
diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h
|
||||
index e9b330f..1ecdf0a 100644
|
||||
index e9b330f..ec5f58c 100644
|
||||
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h
|
||||
+++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h
|
||||
@@ -16,4 +16,6 @@
|
||||
@@ -15,5 +15,8 @@
|
||||
@property (nonatomic, copy) NSString *title;
|
||||
@property (nonatomic, copy) RCTDirectEventBlock onRefresh;
|
||||
@property (nonatomic, weak) UIScrollView *scrollView;
|
||||
|
||||
+- (void)forwarderBeginRefreshing;
|
||||
+@property (nonatomic, copy) UIColor *customTintColor;
|
||||
+
|
||||
+- (void)forwarderBeginRefreshing;
|
||||
|
||||
@end
|
||||
diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
|
||||
index b09e653..f93cb46 100644
|
||||
index b09e653..288e60c 100644
|
||||
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
|
||||
+++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
|
||||
@@ -198,9 +198,53 @@ - (void)refreshControlValueChanged
|
||||
[self setCurrentRefreshingState:super.refreshing];
|
||||
_refreshingProgrammatically = NO;
|
||||
@@ -22,6 +22,7 @@ @implementation RCTRefreshControl {
|
||||
NSString *_title;
|
||||
UIColor *_titleColor;
|
||||
CGFloat _progressViewOffset;
|
||||
+ UIColor *_customTintColor;
|
||||
}
|
||||
|
||||
+ if (@available(iOS 17.4, *)) {
|
||||
+ if (_currentRefreshingState) {
|
||||
+ UIImpactFeedbackGenerator *feedbackGenerator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight];
|
||||
+ [feedbackGenerator prepare];
|
||||
+ [feedbackGenerator impactOccurred];
|
||||
+ }
|
||||
+ }
|
||||
- (instancetype)init
|
||||
@@ -56,6 +57,12 @@ - (void)layoutSubviews
|
||||
_isInitialRender = false;
|
||||
}
|
||||
|
||||
+- (void)didMoveToSuperview
|
||||
+{
|
||||
+ [super didMoveToSuperview];
|
||||
+ [self setTintColor:_customTintColor];
|
||||
+}
|
||||
+
|
||||
if (_onRefresh) {
|
||||
_onRefresh(nil);
|
||||
- (void)beginRefreshingProgrammatically
|
||||
{
|
||||
UInt64 beginRefreshingTimestamp = _currentRefreshingStateTimestamp;
|
||||
@@ -203,4 +210,58 @@ - (void)refreshControlValueChanged
|
||||
}
|
||||
}
|
||||
|
||||
+- (void)setCustomTintColor:(UIColor *)customTintColor
|
||||
+{
|
||||
+ _customTintColor = customTintColor;
|
||||
+ [self setTintColor:customTintColor];
|
||||
+}
|
||||
+
|
||||
+// Fix for https://github.com/facebook/react-native/issues/43388
|
||||
+// A bug in iOS 17.4 causes the haptic to not play when refreshing if the tintColor
|
||||
+// is set before the refresh control gets added to the scrollview. We'll call this
|
||||
+// function whenever the superview changes. We'll also call it if the value of customTintColor
|
||||
+// changes.
|
||||
+- (void)setTintColor:(UIColor *)tintColor
|
||||
+{
|
||||
+ if ([self.superview isKindOfClass:[UIScrollView class]] && self.tintColor != tintColor) {
|
||||
+ [super setTintColor:tintColor];
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+/*
|
||||
+ This method is used by Bluesky's ExpoScrollForwarder. This allows other React Native
|
||||
+ libraries to perform a refresh of a scrollview and access the refresh control's onRefresh
|
||||
@@ -106,6 +134,24 @@ index b09e653..f93cb46 100644
|
||||
+}
|
||||
+
|
||||
@end
|
||||
diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m
|
||||
index 40aaf9c..1c60164 100644
|
||||
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m
|
||||
+++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m
|
||||
@@ -22,11 +22,12 @@ - (UIView *)view
|
||||
|
||||
RCT_EXPORT_VIEW_PROPERTY(onRefresh, RCTDirectEventBlock)
|
||||
RCT_EXPORT_VIEW_PROPERTY(refreshing, BOOL)
|
||||
-RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor)
|
||||
RCT_EXPORT_VIEW_PROPERTY(title, NSString)
|
||||
RCT_EXPORT_VIEW_PROPERTY(titleColor, UIColor)
|
||||
RCT_EXPORT_VIEW_PROPERTY(progressViewOffset, CGFloat)
|
||||
|
||||
+RCT_REMAP_VIEW_PROPERTY(tintColor, customTintColor, UIColor)
|
||||
+
|
||||
RCT_EXPORT_METHOD(setNativeRefreshing : (nonnull NSNumber *)viewTag toRefreshing : (BOOL)refreshing)
|
||||
{
|
||||
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
|
||||
diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java
|
||||
index 5f5e1ab..aac00b6 100644
|
||||
--- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import 'react-native-url-polyfill/auto'
|
||||
import 'lib/sentry' // must be near top
|
||||
import 'view/icons'
|
||||
import '#/lib/sentry' // must be near top
|
||||
import '#/view/icons'
|
||||
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import {GestureHandlerRootView} from 'react-native-gesture-handler'
|
||||
|
||||
@@ -70,6 +70,8 @@ export function applyFonts(
|
||||
* IMPORTANT: This is unused. Expo statically extracts these fonts.
|
||||
*
|
||||
* All used fonts MUST be configured here. Unused fonts can be commented out.
|
||||
*
|
||||
* This is used for both web fonts and native fonts.
|
||||
*/
|
||||
export function DO_NOT_USE() {
|
||||
return useFonts({
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React from 'react'
|
||||
import {ColorSchemeName, useColorScheme} from 'react-native'
|
||||
import * as SystemUI from 'expo-system-ui'
|
||||
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {useThemePrefs} from 'state/shell'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useThemePrefs} from '#/state/shell'
|
||||
import {dark, dim, light} from '#/alf/themes'
|
||||
import {ThemeName} from '#/alf/types'
|
||||
|
||||
@@ -12,7 +11,6 @@ export function useColorModeTheme(): ThemeName {
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
updateDocument(theme)
|
||||
SystemUI.setBackgroundColorAsync(getBackgroundColor(theme))
|
||||
}, [theme])
|
||||
|
||||
return theme
|
||||
|
||||
@@ -87,6 +87,7 @@ export type ButtonProps = Pick<
|
||||
style?: StyleProp<ViewStyle>
|
||||
hoverStyle?: StyleProp<ViewStyle>
|
||||
children: NonTextElements | ((context: ButtonContext) => NonTextElements)
|
||||
PressableComponent?: React.ComponentType<PressableProps>
|
||||
}
|
||||
|
||||
export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean}
|
||||
@@ -114,6 +115,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
disabled = false,
|
||||
style,
|
||||
hoverStyle: hoverStyleProp,
|
||||
PressableComponent = Pressable,
|
||||
...rest
|
||||
},
|
||||
ref,
|
||||
@@ -449,10 +451,11 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
const flattenedBaseStyles = flatten([baseStyles, style])
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
<PressableComponent
|
||||
role="button"
|
||||
accessibilityHint={undefined} // optional
|
||||
{...rest}
|
||||
// @ts-ignore - this will always be a pressable
|
||||
ref={ref}
|
||||
aria-label={label}
|
||||
aria-pressed={state.pressed}
|
||||
@@ -500,7 +503,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
<Context.Provider value={context}>
|
||||
{typeof children === 'function' ? children(context) : children}
|
||||
</Context.Provider>
|
||||
</Pressable>
|
||||
</PressableComponent>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -6,9 +6,14 @@ import {
|
||||
DialogControlRefProps,
|
||||
DialogOuterProps,
|
||||
} from '#/components/Dialog/types'
|
||||
import {BottomSheetSnapPoint} from '../../../modules/bottom-sheet/src/BottomSheet.types'
|
||||
|
||||
export const Context = React.createContext<DialogContextProps>({
|
||||
close: () => {},
|
||||
isNativeDialog: false,
|
||||
nativeSnapPoint: BottomSheetSnapPoint.Hidden,
|
||||
disableDrag: false,
|
||||
setDisableDrag: () => {},
|
||||
})
|
||||
|
||||
export function useDialogContext() {
|
||||
|
||||
+173
-207
@@ -1,86 +1,48 @@
|
||||
import React, {useImperativeHandle} from 'react'
|
||||
import {
|
||||
Dimensions,
|
||||
Keyboard,
|
||||
NativeScrollEvent,
|
||||
NativeSyntheticEvent,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
StyleProp,
|
||||
TextInput,
|
||||
View,
|
||||
ViewStyle,
|
||||
} 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 BottomSheet, {
|
||||
BottomSheetBackdropProps,
|
||||
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 {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {isAndroid, isIOS} from '#/platform/detection'
|
||||
import {useA11y} from '#/state/a11y'
|
||||
import {useDialogStateControlContext} from '#/state/dialogs'
|
||||
import {atoms as a, flatten, useTheme} from '#/alf'
|
||||
import {Context} from '#/components/Dialog/context'
|
||||
import {List, ListMethods, ListProps} from '#/view/com/util/List'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Context, useDialogContext} from '#/components/Dialog/context'
|
||||
import {
|
||||
DialogControlProps,
|
||||
DialogInnerProps,
|
||||
DialogOuterProps,
|
||||
} from '#/components/Dialog/types'
|
||||
import {createInput} from '#/components/forms/TextField'
|
||||
import {FullWindowOverlay} from '#/components/FullWindowOverlay'
|
||||
import {Portal} from '#/components/Portal'
|
||||
import {Portal as DefaultPortal} 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 * from '#/components/Dialog/types'
|
||||
export * from '#/components/Dialog/utils'
|
||||
// @ts-ignore
|
||||
export const Input = createInput(BottomSheetTextInput)
|
||||
|
||||
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 const Input = createInput(TextInput)
|
||||
|
||||
export function Outer({
|
||||
children,
|
||||
@@ -88,24 +50,22 @@ export function Outer({
|
||||
onClose,
|
||||
nativeOptions,
|
||||
testID,
|
||||
Portal = DefaultPortal,
|
||||
}: React.PropsWithChildren<DialogOuterProps>) {
|
||||
const t = useTheme()
|
||||
const sheet = React.useRef<BottomSheet>(null)
|
||||
const sheetOptions = nativeOptions?.sheet || {}
|
||||
const hasSnapPoints = !!sheetOptions.snapPoints
|
||||
const insets = useSafeAreaInsets()
|
||||
const ref = React.useRef<BottomSheet>(null)
|
||||
const closeCallbacks = React.useRef<(() => void)[]>([])
|
||||
const {setDialogIsOpen} = useDialogStateControlContext()
|
||||
const {setDialogIsOpen, setFullyExpandedCount} =
|
||||
useDialogStateControlContext()
|
||||
|
||||
/*
|
||||
* Used to manage open/closed, but index is otherwise handled internally by `BottomSheet`
|
||||
*/
|
||||
const [openIndex, setOpenIndex] = React.useState(-1)
|
||||
const prevSnapPoint = React.useRef<BottomSheetSnapPoint>(
|
||||
BottomSheetSnapPoint.Hidden,
|
||||
)
|
||||
|
||||
/*
|
||||
* `openIndex` is the index of the snap point to open the bottom sheet to. If >0, the bottom sheet is open.
|
||||
*/
|
||||
const isOpen = openIndex > -1
|
||||
const [disableDrag, setDisableDrag] = React.useState(false)
|
||||
const [snapPoint, setSnapPoint] = React.useState<BottomSheetSnapPoint>(
|
||||
BottomSheetSnapPoint.Partial,
|
||||
)
|
||||
|
||||
const callQueuedCallbacks = React.useCallback(() => {
|
||||
for (const cb of closeCallbacks.current) {
|
||||
@@ -119,25 +79,19 @@ export function Outer({
|
||||
closeCallbacks.current = []
|
||||
}, [])
|
||||
|
||||
const open = React.useCallback<DialogControlProps['open']>(
|
||||
({index} = {}) => {
|
||||
// Run any leftover callbacks that might have been queued up before calling `.open()`
|
||||
callQueuedCallbacks()
|
||||
|
||||
setDialogIsOpen(control.id, true)
|
||||
// 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],
|
||||
)
|
||||
const open = React.useCallback<DialogControlProps['open']>(() => {
|
||||
// Run any leftover callbacks that might have been queued up before calling `.open()`
|
||||
callQueuedCallbacks()
|
||||
setDialogIsOpen(control.id, true)
|
||||
ref.current?.present()
|
||||
}, [setDialogIsOpen, control.id, callQueuedCallbacks])
|
||||
|
||||
// This is the function that we call when we want to dismiss the dialog.
|
||||
const close = React.useCallback<DialogControlProps['close']>(cb => {
|
||||
if (typeof cb === 'function') {
|
||||
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
|
||||
@@ -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
|
||||
// tells us that we need to toggle the accessibility overlay setting
|
||||
setDialogIsOpen(control.id, false)
|
||||
setOpenIndex(-1)
|
||||
|
||||
callQueuedCallbacks()
|
||||
onClose?.()
|
||||
}, [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(
|
||||
control.ref,
|
||||
() => ({
|
||||
@@ -161,159 +142,144 @@ export function Outer({
|
||||
[open, close],
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
setDialogIsOpen(control.id, false)
|
||||
}
|
||||
}, [control.id, setDialogIsOpen])
|
||||
|
||||
const context = React.useMemo(() => ({close}), [close])
|
||||
const context = React.useMemo(
|
||||
() => ({
|
||||
close,
|
||||
isNativeDialog: true,
|
||||
nativeSnapPoint: snapPoint,
|
||||
disableDrag,
|
||||
setDisableDrag,
|
||||
}),
|
||||
[close, snapPoint, disableDrag, setDisableDrag],
|
||||
)
|
||||
|
||||
return (
|
||||
isOpen && (
|
||||
<Portal>
|
||||
<FullWindowOverlay>
|
||||
<View
|
||||
// iOS
|
||||
accessibilityViewIsModal
|
||||
// Android
|
||||
importantForAccessibility="yes"
|
||||
style={[a.absolute, a.inset_0]}
|
||||
testID={testID}
|
||||
onTouchMove={() => Keyboard.dismiss()}>
|
||||
<BottomSheet
|
||||
enableDynamicSizing={!hasSnapPoints}
|
||||
enablePanDownToClose
|
||||
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>
|
||||
)
|
||||
<Portal>
|
||||
<Context.Provider value={context}>
|
||||
<BottomSheet
|
||||
ref={ref}
|
||||
cornerRadius={20}
|
||||
backgroundColor={t.atoms.bg.backgroundColor}
|
||||
{...nativeOptions}
|
||||
onSnapPointChange={onSnapPointChange}
|
||||
onStateChange={onStateChange}
|
||||
disableDrag={disableDrag}>
|
||||
<View testID={testID}>{children}</View>
|
||||
</BottomSheet>
|
||||
</Context.Provider>
|
||||
</Portal>
|
||||
)
|
||||
}
|
||||
|
||||
export function Inner({children, style}: DialogInnerProps) {
|
||||
const insets = useSafeAreaInsets()
|
||||
return (
|
||||
<BottomSheetView
|
||||
<View
|
||||
style={[
|
||||
a.py_xl,
|
||||
a.pt_2xl,
|
||||
a.px_xl,
|
||||
{
|
||||
paddingTop: 40,
|
||||
borderTopLeftRadius: 40,
|
||||
borderTopRightRadius: 40,
|
||||
paddingBottom: insets.bottom + a.pb_5xl.paddingBottom,
|
||||
paddingBottom: insets.bottom + insets.top,
|
||||
},
|
||||
flatten(style),
|
||||
style,
|
||||
]}>
|
||||
{children}
|
||||
</BottomSheetView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export const ScrollableInner = React.forwardRef<
|
||||
BottomSheetScrollViewMethods,
|
||||
DialogInnerProps
|
||||
>(function ScrollableInner({children, style}, ref) {
|
||||
const insets = useSafeAreaInsets()
|
||||
return (
|
||||
<BottomSheetScrollView
|
||||
keyboardShouldPersistTaps="handled"
|
||||
style={[
|
||||
a.flex_1, // main diff is this
|
||||
a.p_xl,
|
||||
a.h_full,
|
||||
{
|
||||
paddingTop: 40,
|
||||
borderTopLeftRadius: 40,
|
||||
borderTopRightRadius: 40,
|
||||
},
|
||||
style,
|
||||
]}
|
||||
contentContainerStyle={a.pb_4xl}
|
||||
ref={ref}>
|
||||
{children}
|
||||
<View style={{height: insets.bottom + a.pt_5xl.paddingTop}} />
|
||||
</BottomSheetScrollView>
|
||||
)
|
||||
})
|
||||
export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>(
|
||||
function ScrollableInner({children, style, ...props}, ref) {
|
||||
const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext()
|
||||
const insets = useSafeAreaInsets()
|
||||
const [keyboardHeight, setKeyboardHeight] = React.useState(0)
|
||||
useKeyboardHandler({
|
||||
onEnd: e => {
|
||||
'worklet'
|
||||
runOnJS(setKeyboardHeight)(e.height)
|
||||
},
|
||||
})
|
||||
|
||||
const basePading =
|
||||
(isIOS ? 30 : 50) + (isIOS ? keyboardHeight / 4 : keyboardHeight)
|
||||
const fullPaddingBase = insets.bottom + insets.top + basePading
|
||||
const fullPadding = isIOS ? fullPaddingBase : fullPaddingBase + 50
|
||||
|
||||
const paddingBottom =
|
||||
nativeSnapPoint === BottomSheetSnapPoint.Full ? fullPadding : basePading
|
||||
|
||||
const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
const {contentOffset} = e.nativeEvent
|
||||
if (contentOffset.y > 0 && !disableDrag) {
|
||||
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<
|
||||
BottomSheetFlatListMethods,
|
||||
BottomSheetFlatListProps<any> & {webInnerStyle?: StyleProp<ViewStyle>}
|
||||
>(function InnerFlatList({style, contentContainerStyle, ...props}, ref) {
|
||||
ListMethods,
|
||||
ListProps<any> & {webInnerStyle?: StyleProp<ViewStyle>}
|
||||
>(function InnerFlatList({style, ...props}, ref) {
|
||||
const insets = useSafeAreaInsets()
|
||||
|
||||
const {nativeSnapPoint} = useDialogContext()
|
||||
return (
|
||||
<BottomSheetFlatList
|
||||
<List
|
||||
keyboardShouldPersistTaps="handled"
|
||||
contentContainerStyle={[a.pb_4xl, flatten(contentContainerStyle)]}
|
||||
bounces={nativeSnapPoint === BottomSheetSnapPoint.Full}
|
||||
ListFooterComponent={
|
||||
<View style={{height: insets.bottom + a.pt_5xl.paddingTop}} />
|
||||
}
|
||||
ref={ref}
|
||||
{...props}
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.p_xl,
|
||||
a.pt_0,
|
||||
a.h_full,
|
||||
{
|
||||
marginTop: 40,
|
||||
},
|
||||
flatten(style),
|
||||
]}
|
||||
style={[style]}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
export function Handle() {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {screenReaderEnabled} = useA11y()
|
||||
const {close} = useDialogContext()
|
||||
|
||||
return (
|
||||
<View style={[a.absolute, a.w_full, a.align_center, a.z_10, {height: 40}]}>
|
||||
<View
|
||||
style={[
|
||||
a.rounded_sm,
|
||||
{
|
||||
top: a.pt_lg.paddingTop,
|
||||
width: 35,
|
||||
height: 4,
|
||||
alignSelf: 'center',
|
||||
backgroundColor: t.palette.contrast_900,
|
||||
opacity: 0.5,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<View style={[a.absolute, a.w_full, a.align_center, a.z_10, {height: 20}]}>
|
||||
<Pressable
|
||||
accessible={screenReaderEnabled}
|
||||
onPress={() => close()}
|
||||
accessibilityLabel={_(msg`Dismiss`)}
|
||||
accessibilityHint={_(msg`Double tap to close the dialog`)}>
|
||||
<View
|
||||
style={[
|
||||
a.rounded_sm,
|
||||
{
|
||||
top: 10,
|
||||
width: 35,
|
||||
height: 5,
|
||||
alignSelf: 'center',
|
||||
backgroundColor: t.palette.contrast_975,
|
||||
opacity: 0.5,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -103,6 +103,10 @@ export function Outer({
|
||||
const context = React.useMemo(
|
||||
() => ({
|
||||
close,
|
||||
isNativeDialog: false,
|
||||
nativeSnapPoint: 0,
|
||||
disableDrag: false,
|
||||
setDisableDrag: () => {},
|
||||
}),
|
||||
[close],
|
||||
)
|
||||
@@ -229,10 +233,6 @@ export const InnerFlatList = React.forwardRef<
|
||||
)
|
||||
})
|
||||
|
||||
export function Handle() {
|
||||
return null
|
||||
}
|
||||
|
||||
export function Close() {
|
||||
const {_} = useLingui()
|
||||
const {close} = React.useContext(Context)
|
||||
@@ -258,3 +258,7 @@ export function Close() {
|
||||
</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,
|
||||
ScrollViewProps,
|
||||
} from 'react-native'
|
||||
import {BottomSheetProps} from '@discord/bottom-sheet/src'
|
||||
|
||||
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>
|
||||
|
||||
@@ -37,6 +39,10 @@ export type DialogControlProps = DialogControlRefProps & {
|
||||
|
||||
export type DialogContextProps = {
|
||||
close: DialogControlProps['close']
|
||||
isNativeDialog: boolean
|
||||
nativeSnapPoint: BottomSheetSnapPoint
|
||||
disableDrag: boolean
|
||||
setDisableDrag: React.Dispatch<React.SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
export type DialogControlOpenOptions = {
|
||||
@@ -52,11 +58,10 @@ export type DialogControlOpenOptions = {
|
||||
export type DialogOuterProps = {
|
||||
control: DialogControlProps
|
||||
onClose?: () => void
|
||||
nativeOptions?: {
|
||||
sheet?: Omit<BottomSheetProps, 'children'>
|
||||
}
|
||||
nativeOptions?: Omit<BottomSheetViewProps, 'children'>
|
||||
webOptions?: {}
|
||||
testID?: string
|
||||
Portal?: PortalComponent
|
||||
}
|
||||
|
||||
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 {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
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 {logger} from '#/logger'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Text} from '#/components/Typography'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
|
||||
import {useLikedByQuery} from '#/state/queries/post-liked-by'
|
||||
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
|
||||
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 {Text} from '#/components/Typography'
|
||||
|
||||
interface LikesDialogProps {
|
||||
control: Dialog.DialogOuterProps['control']
|
||||
@@ -25,7 +24,6 @@ export function LikesDialog(props: LikesDialogProps) {
|
||||
return (
|
||||
<Dialog.Outer control={props.control}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<LikesDialogInner {...props} />
|
||||
</Dialog.Outer>
|
||||
)
|
||||
|
||||
@@ -103,17 +103,17 @@ export function useLink({
|
||||
linkRequiresWarning(href, displayText),
|
||||
)
|
||||
|
||||
if (requiresWarning) {
|
||||
if (isWeb) {
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
if (requiresWarning) {
|
||||
openModal({
|
||||
name: 'link-warning',
|
||||
text: displayText,
|
||||
href: href,
|
||||
})
|
||||
} else {
|
||||
e.preventDefault()
|
||||
|
||||
if (isExternal) {
|
||||
openLink(href)
|
||||
} else {
|
||||
|
||||
@@ -4,7 +4,7 @@ import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
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 {Button, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
@@ -82,19 +82,21 @@ export function Outer({
|
||||
style?: StyleProp<ViewStyle>
|
||||
}>) {
|
||||
const context = React.useContext(Context)
|
||||
const {_} = useLingui()
|
||||
|
||||
return (
|
||||
<Dialog.Outer control={context.control}>
|
||||
<Dialog.Outer
|
||||
control={context.control}
|
||||
nativeOptions={{preventExpansion: true}}>
|
||||
<Dialog.Handle />
|
||||
|
||||
{/* Re-wrap with context since Dialogs are portal-ed to root */}
|
||||
<Context.Provider value={context}>
|
||||
<Dialog.ScrollableInner label="Menu TODO">
|
||||
<Dialog.ScrollableInner label={_(msg`Menu`)} style={[a.pt_sm]}>
|
||||
<View style={[a.gap_lg]}>
|
||||
{children}
|
||||
{isNative && showCancel && <Cancel />}
|
||||
<View style={[{height: a.pb_lg.paddingBottom}]} />
|
||||
</View>
|
||||
<View style={{height: a.gap_lg.gap}} />
|
||||
</Dialog.ScrollableInner>
|
||||
</Context.Provider>
|
||||
</Dialog.Outer>
|
||||
@@ -116,15 +118,14 @@ export function Item({children, label, style, onPress, ...rest}: ItemProps) {
|
||||
{...rest}
|
||||
accessibilityHint=""
|
||||
accessibilityLabel={label}
|
||||
onPress={e => {
|
||||
onPress(e)
|
||||
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
onPress={async e => {
|
||||
await onPress(e)
|
||||
if (!e.defaultPrevented) {
|
||||
control?.close()
|
||||
}
|
||||
}}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
onPressIn={e => {
|
||||
onPressIn()
|
||||
rest.onPressIn?.(e)
|
||||
|
||||
@@ -5,12 +5,12 @@ import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {differenceInSeconds} from 'date-fns'
|
||||
|
||||
import {HITSLOP_10} from '#/lib/constants'
|
||||
import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
|
||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {HITSLOP_10} from 'lib/constants'
|
||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||
import {useSession} from 'state/session'
|
||||
import {useSession} from '#/state/session'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
|
||||
@@ -12,6 +12,8 @@ type ComponentMap = {
|
||||
[id: string]: Component
|
||||
}
|
||||
|
||||
export type PortalComponent = ({children}: {children?: React.ReactNode}) => null
|
||||
|
||||
export function createPortalGroup() {
|
||||
const Context = React.createContext<ContextType>({
|
||||
outlet: null,
|
||||
|
||||
@@ -4,8 +4,9 @@ import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
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 {PortalComponent} from '#/components/Portal'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export {
|
||||
@@ -25,9 +26,11 @@ export function Outer({
|
||||
children,
|
||||
control,
|
||||
testID,
|
||||
Portal,
|
||||
}: React.PropsWithChildren<{
|
||||
control: Dialog.DialogControlProps
|
||||
testID?: string
|
||||
Portal?: PortalComponent
|
||||
}>) {
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const titleId = React.useId()
|
||||
@@ -39,10 +42,9 @@ export function Outer({
|
||||
)
|
||||
|
||||
return (
|
||||
<Dialog.Outer control={control} testID={testID}>
|
||||
<Dialog.Outer control={control} testID={testID} Portal={Portal}>
|
||||
<Dialog.Handle />
|
||||
<Context.Provider value={context}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.ScrollableInner
|
||||
accessibilityLabelledBy={titleId}
|
||||
accessibilityDescribedBy={descriptionId}
|
||||
@@ -141,7 +143,7 @@ export function Action({
|
||||
* Note: The dialog will close automatically when the action is pressed, you
|
||||
* should NOT close the dialog as a side effect of this method.
|
||||
*/
|
||||
onPress: ButtonProps['onPress']
|
||||
onPress: (e: GestureResponderEvent) => void
|
||||
color?: ButtonColor
|
||||
/**
|
||||
* Optional i18n string. If undefined, it will default to "Confirm".
|
||||
@@ -181,6 +183,7 @@ export function Basic({
|
||||
onConfirm,
|
||||
confirmButtonColor,
|
||||
showCancel = true,
|
||||
Portal,
|
||||
}: React.PropsWithChildren<{
|
||||
control: Dialog.DialogOuterProps['control']
|
||||
title: string
|
||||
@@ -194,12 +197,13 @@ export function Basic({
|
||||
* Note: The dialog will close automatically when the action is pressed, you
|
||||
* should NOT close the dialog as a side effect of this method.
|
||||
*/
|
||||
onConfirm: ButtonProps['onPress']
|
||||
onConfirm: (e: GestureResponderEvent) => void
|
||||
confirmButtonColor?: ButtonColor
|
||||
showCancel?: boolean
|
||||
Portal?: PortalComponent
|
||||
}>) {
|
||||
return (
|
||||
<Outer control={control} testID="confirmModal">
|
||||
<Outer control={control} testID="confirmModal" Portal={Portal}>
|
||||
<TitleText>{title}</TitleText>
|
||||
<DescriptionText>{description}</DescriptionText>
|
||||
<Actions>
|
||||
|
||||
@@ -4,7 +4,6 @@ import {AppBskyLabelerDefs} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
export {useDialogControl as useReportDialogControl} from '#/components/Dialog'
|
||||
import {getLabelingServiceTitle} from '#/lib/moderation'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {Button, useButtonContext} from '#/components/Button'
|
||||
|
||||
@@ -6,6 +6,7 @@ import {useLingui} from '@lingui/react'
|
||||
|
||||
import {getLabelingServiceTitle} from '#/lib/moderation'
|
||||
import {ReportOption} from '#/lib/moderation/useReportOptions'
|
||||
import {isAndroid} from '#/platform/detection'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
@@ -225,6 +226,8 @@ export function SubmitView({
|
||||
{submitting && <ButtonIcon icon={Loader} />}
|
||||
</Button>
|
||||
</View>
|
||||
{/* Maybe fix this later -h */}
|
||||
{isAndroid ? <View style={{height: 300}} /> : null}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react'
|
||||
import {Pressable, View} from 'react-native'
|
||||
import {ScrollView} from 'react-native-gesture-handler'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
@@ -8,12 +9,10 @@ import {useMyLabelersQuery} from '#/state/queries/preferences'
|
||||
export {useDialogControl as useReportDialogControl} from '#/components/Dialog'
|
||||
|
||||
import {AppBskyLabelerDefs} from '@atproto/api'
|
||||
import {BottomSheetScrollViewMethods} from '@discord/bottom-sheet/src'
|
||||
|
||||
import {atoms as a} from '#/alf'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {useDelayedLoading} from '#/components/hooks/useDelayedLoading'
|
||||
import {useOnKeyboardDidShow} from '#/components/hooks/useOnKeyboard'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {SelectLabelerView} from './SelectLabelerView'
|
||||
@@ -25,7 +24,6 @@ export function ReportDialog(props: ReportDialogProps) {
|
||||
return (
|
||||
<Dialog.Outer control={props.control}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<ReportDialogInner {...props} />
|
||||
</Dialog.Outer>
|
||||
)
|
||||
@@ -40,10 +38,7 @@ function ReportDialogInner(props: ReportDialogProps) {
|
||||
} = useMyLabelersQuery()
|
||||
const isLoading = useDelayedLoading(500, isLabelerLoading)
|
||||
|
||||
const ref = React.useRef<BottomSheetScrollViewMethods>(null)
|
||||
useOnKeyboardDidShow(() => {
|
||||
ref.current?.scrollToEnd({animated: true})
|
||||
})
|
||||
const ref = React.useRef<ScrollView>(null)
|
||||
|
||||
return (
|
||||
<Dialog.ScrollableInner label={_(msg`Report dialog`)} ref={ref}>
|
||||
|
||||
@@ -149,7 +149,6 @@ export function QrCodeDialog({
|
||||
|
||||
return (
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Handle />
|
||||
<Dialog.ScrollableInner
|
||||
label={_(msg`Create a QR code for a starter pack`)}>
|
||||
<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 {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 {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 {isNative, isWeb} from 'platform/detection'
|
||||
import * as Toast from 'view/com/util/Toast'
|
||||
import {isNative, isWeb} from '#/platform/detection'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import {DialogControlProps} from '#/components/Dialog'
|
||||
@@ -32,6 +32,7 @@ interface Props {
|
||||
export function ShareDialog(props: Props) {
|
||||
return (
|
||||
<Dialog.Outer control={props.control}>
|
||||
<Dialog.Handle />
|
||||
<ShareDialogInner {...props} />
|
||||
</Dialog.Outer>
|
||||
)
|
||||
@@ -84,7 +85,6 @@ function ShareDialogInner({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog.Handle />
|
||||
<Dialog.ScrollableInner label={_(msg`Share link dialog`)}>
|
||||
{!imageLoaded || !link ? (
|
||||
<View style={[a.p_xl, a.align_center]}>
|
||||
|
||||
@@ -3,13 +3,13 @@ import type {ListRenderItemInfo} from 'react-native'
|
||||
import {View} from 'react-native'
|
||||
import {AppBskyActorDefs, ModerationOpts} from '@atproto/api'
|
||||
import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
|
||||
import {BottomSheetFlatListMethods} from '@discord/bottom-sheet'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {useSession} from 'state/session'
|
||||
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useSession} from '#/state/session'
|
||||
import {ListMethods} from '#/view/com/util/List'
|
||||
import {WizardAction, WizardState} from '#/screens/StarterPack/Wizard/State'
|
||||
import {atoms as a, native, useTheme, web} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
@@ -45,7 +45,7 @@ export function WizardEditListDialog({
|
||||
const {currentAccount} = useSession()
|
||||
const initialNumToRender = useInitialNumToRender()
|
||||
|
||||
const listRef = useRef<BottomSheetFlatListMethods>(null)
|
||||
const listRef = useRef<ListMethods>(null)
|
||||
|
||||
const getData = () => {
|
||||
if (state.currentStep === 'Feeds') return state.feeds
|
||||
@@ -76,10 +76,7 @@ export function WizardEditListDialog({
|
||||
)
|
||||
|
||||
return (
|
||||
<Dialog.Outer
|
||||
control={control}
|
||||
testID="newChatDialog"
|
||||
nativeOptions={{sheet: {snapPoints: ['95%']}}}>
|
||||
<Dialog.Outer control={control} testID="newChatDialog">
|
||||
<Dialog.Handle />
|
||||
<Dialog.InnerFlatList
|
||||
ref={listRef}
|
||||
@@ -89,6 +86,7 @@ export function WizardEditListDialog({
|
||||
ListHeaderComponent={
|
||||
<View
|
||||
style={[
|
||||
native(a.pt_4xl),
|
||||
a.flex_row,
|
||||
a.justify_between,
|
||||
a.border_b,
|
||||
@@ -103,13 +101,7 @@ export function WizardEditListDialog({
|
||||
height: 48,
|
||||
},
|
||||
]
|
||||
: [
|
||||
a.pb_sm,
|
||||
a.align_end,
|
||||
{
|
||||
height: 68,
|
||||
},
|
||||
],
|
||||
: [a.pb_sm, a.align_end],
|
||||
]}>
|
||||
<View style={{width: 60}} />
|
||||
<Text style={[a.font_bold, a.text_xl]}>
|
||||
@@ -143,8 +135,6 @@ export function WizardEditListDialog({
|
||||
paddingHorizontal: 0,
|
||||
marginTop: 0,
|
||||
paddingTop: 0,
|
||||
borderTopLeftRadius: 40,
|
||||
borderTopRightRadius: 40,
|
||||
}),
|
||||
]}
|
||||
webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]}
|
||||
|
||||
@@ -85,7 +85,6 @@ export function TagMenu({
|
||||
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.Inner label={_(msg`Tag menu: ${displayTag}`)}>
|
||||
{isPreferencesLoading ? (
|
||||
<View style={[a.w_full, a.align_center]}>
|
||||
|
||||
@@ -53,11 +53,14 @@ export function childIsString(
|
||||
)
|
||||
}
|
||||
|
||||
export function renderChildrenWithEmoji(children: StringChild) {
|
||||
export function renderChildrenWithEmoji(
|
||||
children: StringChild,
|
||||
props: Omit<TextProps, 'children'> = {},
|
||||
) {
|
||||
const normalized = Array.isArray(children) ? children : [children]
|
||||
|
||||
return (
|
||||
<UITextView>
|
||||
<UITextView {...props}>
|
||||
{normalized.map(child => {
|
||||
if (typeof child !== 'string') return child
|
||||
|
||||
@@ -68,10 +71,12 @@ export function renderChildrenWithEmoji(children: StringChild) {
|
||||
}
|
||||
|
||||
return child.split(EMOJI).map((stringPart, index) => (
|
||||
<UITextView key={index}>
|
||||
<UITextView key={index} {...props}>
|
||||
{stringPart}
|
||||
{emojis[index] ? (
|
||||
<UITextView style={{color: 'black', fontFamily: 'System'}}>
|
||||
<UITextView
|
||||
{...props}
|
||||
style={[props?.style, {color: 'black', fontFamily: 'System'}]}>
|
||||
{emojis[index]}
|
||||
</UITextView>
|
||||
) : null}
|
||||
@@ -163,15 +168,17 @@ export function Text({
|
||||
}
|
||||
}
|
||||
|
||||
const shared = {
|
||||
uiTextView: true,
|
||||
selectable,
|
||||
style: s,
|
||||
dataSet: Object.assign({tooltip: title}, dataSet || {}),
|
||||
...rest,
|
||||
}
|
||||
|
||||
return (
|
||||
<UITextView
|
||||
selectable={selectable}
|
||||
uiTextView
|
||||
style={s}
|
||||
{...rest}
|
||||
// @ts-ignore
|
||||
dataSet={Object.assign({tooltip: title}, dataSet || {})}>
|
||||
{isIOS && emoji ? renderChildrenWithEmoji(children) : children}
|
||||
<UITextView {...shared}>
|
||||
{isIOS && emoji ? renderChildrenWithEmoji(children, shared) : children}
|
||||
</UITextView>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ export function BirthDateSettingsDialog({
|
||||
return (
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.ScrollableInner label={_(msg`My Birthday`)}>
|
||||
<View style={[a.gap_sm, a.pb_lg]}>
|
||||
<Text style={[a.text_2xl, a.font_bold]}>
|
||||
|
||||
@@ -50,7 +50,6 @@ export function EmbedConsentDialog({
|
||||
return (
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.ScrollableInner
|
||||
label={_(msg`External Media`)}
|
||||
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,
|
||||
} from 'react'
|
||||
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 {useLingui} from '@lingui/react'
|
||||
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {
|
||||
@@ -19,7 +21,8 @@ import {
|
||||
} from '#/state/queries/tenor'
|
||||
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
|
||||
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 TextField from '#/components/forms/TextField'
|
||||
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 {Button, ButtonIcon, ButtonText} from '../Button'
|
||||
import {ListFooter, ListMaybePlaceholder} from '../Lists'
|
||||
import {GifPreview} from './GifSelect.shared'
|
||||
import {PortalComponent} from '../Portal'
|
||||
|
||||
export function GifSelectDialog({
|
||||
controlRef,
|
||||
onClose,
|
||||
onSelectGif: onSelectGifProp,
|
||||
Portal,
|
||||
}: {
|
||||
controlRef: React.RefObject<{open: () => void}>
|
||||
onClose: () => void
|
||||
onSelectGif: (gif: Gif) => void
|
||||
Portal?: PortalComponent
|
||||
}) {
|
||||
const control = Dialog.useDialogControl()
|
||||
|
||||
@@ -59,8 +64,13 @@ export function GifSelectDialog({
|
||||
return (
|
||||
<Dialog.Outer
|
||||
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 />
|
||||
<ErrorBoundary renderError={renderErrorBoundary}>
|
||||
<GifList control={control} onSelectGif={onSelectGif} />
|
||||
@@ -80,9 +90,10 @@ function GifList({
|
||||
const t = useTheme()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const textInputRef = useRef<TextInput>(null)
|
||||
const listRef = useRef<BottomSheetFlatListMethods>(null)
|
||||
const listRef = useRef<ListMethods>(null)
|
||||
const [undeferredSearch, setSearch] = useState('')
|
||||
const search = useThrottledValue(undeferredSearch, 500)
|
||||
const {height} = useWindowDimensions()
|
||||
|
||||
const isSearching = search.length > 0
|
||||
|
||||
@@ -95,7 +106,7 @@ function GifList({
|
||||
isFetchingNextPage,
|
||||
hasNextPage,
|
||||
error,
|
||||
isLoading,
|
||||
isPending,
|
||||
isError,
|
||||
refetch,
|
||||
} = isSearching ? searchQuery : trendingQuery
|
||||
@@ -132,6 +143,7 @@ function GifList({
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
native(a.pt_4xl),
|
||||
a.relative,
|
||||
a.mb_lg,
|
||||
a.flex_row,
|
||||
@@ -196,13 +208,14 @@ function GifList({
|
||||
data={flattenedData}
|
||||
renderItem={renderItem}
|
||||
numColumns={gtMobile ? 3 : 2}
|
||||
columnWrapperStyle={a.gap_sm}
|
||||
columnWrapperStyle={[a.gap_sm]}
|
||||
contentContainerStyle={[native([a.px_xl, {minHeight: height}])]}
|
||||
ListHeaderComponent={
|
||||
<>
|
||||
{listHeader}
|
||||
{!hasData && (
|
||||
<ListMaybePlaceholder
|
||||
isLoading={isLoading}
|
||||
isLoading={isPending}
|
||||
isError={isError}
|
||||
onRetry={refetch}
|
||||
onGoBack={onGoBack}
|
||||
@@ -273,3 +286,47 @@ function DialogError({details}: {details?: string}) {
|
||||
</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 {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {createPortalGroup} from '#/components/Portal'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
const ONE_DAY = 24 * 60 * 60 * 1000
|
||||
|
||||
const Portal = createPortalGroup()
|
||||
|
||||
export function MutedWordsDialog() {
|
||||
const {mutedWordsDialogControl: control} = useGlobalDialogsControlContext()
|
||||
return (
|
||||
@@ -105,307 +108,349 @@ function MutedWordsInner() {
|
||||
}, [_, field, targets, addMutedWord, setField, durations, excludeFollowing])
|
||||
|
||||
return (
|
||||
<Dialog.ScrollableInner label={_(msg`Manage your muted words and tags`)}>
|
||||
<View>
|
||||
<Text
|
||||
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]}>
|
||||
<Portal.Provider>
|
||||
<Dialog.ScrollableInner label={_(msg`Manage your muted words and tags`)}>
|
||||
<View>
|
||||
<Text
|
||||
style={[
|
||||
a.text_md,
|
||||
a.font_bold,
|
||||
a.pb_md,
|
||||
a.pb_sm,
|
||||
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>
|
||||
|
||||
{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>
|
||||
<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>
|
||||
) : 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>
|
||||
|
||||
<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
|
||||
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>
|
||||
|
||||
{isNative && <View style={{height: 20}} />}
|
||||
</View>
|
||||
<Dialog.Close />
|
||||
</Dialog.ScrollableInner>
|
||||
|
||||
<Dialog.Close />
|
||||
</Dialog.ScrollableInner>
|
||||
<Portal.Outlet />
|
||||
</Portal.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -437,6 +482,7 @@ function MutedWordRow({
|
||||
onConfirm={remove}
|
||||
confirmButtonCta={_(msg`Remove`)}
|
||||
confirmButtonColor="negative"
|
||||
Portal={Portal.Portal}
|
||||
/>
|
||||
|
||||
<View
|
||||
|
||||
@@ -37,6 +37,7 @@ import * as Toggle from '#/components/forms/Toggle'
|
||||
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {PortalComponent} from '#/components/Portal'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export type PostInteractionSettingsFormProps = {
|
||||
@@ -54,13 +55,15 @@ export type PostInteractionSettingsFormProps = {
|
||||
|
||||
export function PostInteractionSettingsControlledDialog({
|
||||
control,
|
||||
Portal,
|
||||
...rest
|
||||
}: PostInteractionSettingsFormProps & {
|
||||
control: Dialog.DialogControlProps
|
||||
Portal?: PortalComponent
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
return (
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Outer control={control} Portal={Portal}>
|
||||
<Dialog.Handle />
|
||||
<Dialog.ScrollableInner
|
||||
label={_(msg`Edit post interaction settings`)}
|
||||
@@ -231,7 +234,6 @@ export function PostInteractionSettingsForm({
|
||||
}: PostInteractionSettingsFormProps) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const control = Dialog.useDialogContext()
|
||||
const {data: lists} = useMyListsQuery('curate')
|
||||
const [quotesEnabled, setQuotesEnabled] = React.useState(
|
||||
!(
|
||||
@@ -437,7 +439,6 @@ export function PostInteractionSettingsForm({
|
||||
<Button
|
||||
label={_(msg`Save`)}
|
||||
onPress={onSave}
|
||||
onAccessibilityEscape={control.close}
|
||||
color="primary"
|
||||
size="large"
|
||||
variant="solid"
|
||||
|
||||
@@ -43,7 +43,6 @@ export function SwitchAccountDialog({
|
||||
return (
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.ScrollableInner label={_(msg`Switch Account`)}>
|
||||
<View style={[a.gap_lg]}>
|
||||
<Text style={[a.text_2xl, a.font_bold]}>
|
||||
|
||||
@@ -44,7 +44,6 @@ export function NeueTypography() {
|
||||
return (
|
||||
<Dialog.Outer control={control} onClose={onClose}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.ScrollableInner label={_(msg`Introducing new font settings`)}>
|
||||
<View style={[a.gap_xl]}>
|
||||
<View style={[a.gap_md]}>
|
||||
|
||||
@@ -136,7 +136,7 @@ let ConvoMenu = ({
|
||||
<Menu.Outer>
|
||||
<Menu.Item
|
||||
label={_(msg`Leave conversation`)}
|
||||
onPress={leaveConvoControl.open}>
|
||||
onPress={() => leaveConvoControl.open()}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Leave conversation</Trans>
|
||||
</Menu.ItemText>
|
||||
@@ -195,7 +195,7 @@ let ConvoMenu = ({
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
label={_(msg`Report conversation`)}
|
||||
onPress={reportControl.open}>
|
||||
onPress={() => reportControl.open()}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Report conversation</Trans>
|
||||
</Menu.ItemText>
|
||||
@@ -206,7 +206,7 @@ let ConvoMenu = ({
|
||||
<Menu.Group>
|
||||
<Menu.Item
|
||||
label={_(msg`Leave conversation`)}
|
||||
onPress={leaveConvoControl.open}>
|
||||
onPress={() => leaveConvoControl.open()}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Leave conversation</Trans>
|
||||
</Menu.ItemText>
|
||||
|
||||
@@ -7,11 +7,11 @@ import {useLingui} from '@lingui/react'
|
||||
|
||||
import {richTextToString} from '#/lib/strings/rich-text-helpers'
|
||||
import {getTranslatorLink} from '#/locale/helpers'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useConvoActive} from '#/state/messages/convo'
|
||||
import {useLanguagePrefs} from '#/state/preferences'
|
||||
import {useOpenLink} from '#/state/preferences/in-app-browser'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {useConvoActive} from 'state/messages/convo'
|
||||
import {useSession} from 'state/session'
|
||||
import {useSession} from '#/state/session'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {ReportDialog} from '#/components/dms/ReportDialog'
|
||||
@@ -120,7 +120,7 @@ export let MessageMenu = ({
|
||||
<Menu.Item
|
||||
testID="messageDropdownDeleteBtn"
|
||||
label={_(msg`Delete message for me`)}
|
||||
onPress={deleteControl.open}>
|
||||
onPress={() => deleteControl.open()}>
|
||||
<Menu.ItemText>{_(msg`Delete for me`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Trash} position="right" />
|
||||
</Menu.Item>
|
||||
@@ -128,7 +128,7 @@ export let MessageMenu = ({
|
||||
<Menu.Item
|
||||
testID="messageDropdownReportBtn"
|
||||
label={_(msg`Report message`)}
|
||||
onPress={reportControl.open}>
|
||||
onPress={() => reportControl.open()}>
|
||||
<Menu.ItemText>{_(msg`Report`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Warning} position="right" />
|
||||
</Menu.Item>
|
||||
|
||||
@@ -10,13 +10,11 @@ import {useLingui} from '@lingui/react'
|
||||
import {useMutation} from '@tanstack/react-query'
|
||||
|
||||
import {ReportOption} from '#/lib/moderation/useReportOptions'
|
||||
import {isAndroid} from '#/platform/detection'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {KeyboardControllerPadding} from '#/components/KeyboardControllerPadding'
|
||||
import {Button, ButtonIcon, ButtonText} from '../Button'
|
||||
import {Divider} from '../Divider'
|
||||
import {ChevronLeft_Stroke2_Corner0_Rounded as Chevron} from '../icons/Chevron'
|
||||
@@ -41,14 +39,11 @@ let ReportDialog = ({
|
||||
}): React.ReactNode => {
|
||||
const {_} = useLingui()
|
||||
return (
|
||||
<Dialog.Outer
|
||||
control={control}
|
||||
nativeOptions={isAndroid ? {sheet: {snapPoints: ['100%']}} : {}}>
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Handle />
|
||||
<Dialog.ScrollableInner label={_(msg`Report this message`)}>
|
||||
<DialogInner params={params} />
|
||||
<Dialog.Close />
|
||||
<KeyboardControllerPadding />
|
||||
</Dialog.ScrollableInner>
|
||||
</Dialog.Outer>
|
||||
)
|
||||
|
||||
@@ -2,9 +2,9 @@ import React, {useCallback} from 'react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {logger} from '#/logger'
|
||||
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 * as Toast from '#/view/com/util/Toast'
|
||||
import {useTheme} from '#/alf'
|
||||
@@ -55,10 +55,8 @@ export function NewChat({
|
||||
accessibilityHint=""
|
||||
/>
|
||||
|
||||
<Dialog.Outer
|
||||
control={control}
|
||||
testID="newChatDialog"
|
||||
nativeOptions={{sheet: {snapPoints: ['100%']}}}>
|
||||
<Dialog.Outer control={control} testID="newChatDialog">
|
||||
<Dialog.Handle />
|
||||
<SearchablePeopleList
|
||||
title={_(msg`Start a new chat`)}
|
||||
onSelectChat={onCreateChat}
|
||||
|
||||
@@ -5,10 +5,8 @@ import React, {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import type {TextInput as TextInputType} from 'react-native'
|
||||
import {View} from 'react-native'
|
||||
import {TextInput, View} from 'react-native'
|
||||
import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
|
||||
import {BottomSheetFlatListMethods} from '@discord/bottom-sheet'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
@@ -16,18 +14,17 @@ import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
|
||||
import {useListConvosQuery} from '#/state/queries/messages/list-converations'
|
||||
import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
|
||||
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 {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 {TextInput} from '#/components/dms/dialogs/TextInput'
|
||||
import {canBeMessaged} from '#/components/dms/util'
|
||||
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 {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||
import {Text} from '#/components/Typography'
|
||||
@@ -66,9 +63,9 @@ export function SearchablePeopleList({
|
||||
const {_} = useLingui()
|
||||
const moderationOpts = useModerationOpts()
|
||||
const control = Dialog.useDialogContext()
|
||||
const listRef = useRef<BottomSheetFlatListMethods>(null)
|
||||
const listRef = useRef<ListMethods>(null)
|
||||
const {currentAccount} = useSession()
|
||||
const inputRef = useRef<TextInputType>(null)
|
||||
const inputRef = useRef<TextInput>(null)
|
||||
|
||||
const [searchText, setSearchText] = useState('')
|
||||
|
||||
@@ -101,15 +98,15 @@ export function SearchablePeopleList({
|
||||
})
|
||||
}
|
||||
|
||||
_items = _items.sort(a => {
|
||||
_items = _items.sort(item => {
|
||||
// @ts-ignore
|
||||
return a.enabled ? -1 : 1
|
||||
return item.enabled ? -1 : 1
|
||||
})
|
||||
}
|
||||
} else {
|
||||
const placeholders: Item[] = Array(10)
|
||||
.fill(0)
|
||||
.map((_, i) => ({
|
||||
.map((__, i) => ({
|
||||
type: 'placeholder',
|
||||
key: i + '',
|
||||
}))
|
||||
@@ -155,9 +152,9 @@ export function SearchablePeopleList({
|
||||
}
|
||||
|
||||
// only sort follows
|
||||
followsItems = followsItems.sort(a => {
|
||||
followsItems = followsItems.sort(item => {
|
||||
// @ts-ignore
|
||||
return a.enabled ? -1 : 1
|
||||
return item.enabled ? -1 : 1
|
||||
})
|
||||
|
||||
// then append
|
||||
@@ -177,9 +174,9 @@ export function SearchablePeopleList({
|
||||
}
|
||||
}
|
||||
|
||||
_items = _items.sort(a => {
|
||||
_items = _items.sort(item => {
|
||||
// @ts-ignore
|
||||
return a.enabled ? -1 : 1
|
||||
return item.enabled ? -1 : 1
|
||||
})
|
||||
} else {
|
||||
_items.push(...placeholders)
|
||||
@@ -242,57 +239,46 @@ export function SearchablePeopleList({
|
||||
<View
|
||||
style={[
|
||||
a.relative,
|
||||
a.pt_md,
|
||||
web(a.pt_lg),
|
||||
native(a.pt_4xl),
|
||||
a.pb_xs,
|
||||
a.px_lg,
|
||||
a.border_b,
|
||||
t.atoms.border_contrast_low,
|
||||
t.atoms.bg,
|
||||
native([a.pt_lg]),
|
||||
]}>
|
||||
<View
|
||||
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>
|
||||
<View style={[a.relative, native(a.align_center), a.justify_center]}>
|
||||
<Text
|
||||
style={[
|
||||
a.z_10,
|
||||
a.text_lg,
|
||||
a.font_bold,
|
||||
a.font_heavy,
|
||||
a.leading_tight,
|
||||
t.atoms.text_contrast_high,
|
||||
]}>
|
||||
{title}
|
||||
</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 style={[native([a.pt_sm]), web([a.pt_xs])]}>
|
||||
<View style={[, web([a.pt_xs])]}>
|
||||
<SearchInput
|
||||
inputRef={inputRef}
|
||||
value={searchText}
|
||||
@@ -309,7 +295,6 @@ export function SearchablePeopleList({
|
||||
t.atoms.border_contrast_low,
|
||||
t.atoms.bg,
|
||||
t.atoms.text_contrast_high,
|
||||
t.palette.contrast_500,
|
||||
_,
|
||||
title,
|
||||
searchText,
|
||||
@@ -326,14 +311,7 @@ export function SearchablePeopleList({
|
||||
keyExtractor={(item: Item) => item.key}
|
||||
style={[
|
||||
web([a.py_0, {height: '100vh', maxHeight: 600}, a.px_0]),
|
||||
native({
|
||||
height: '100%',
|
||||
paddingHorizontal: 0,
|
||||
marginTop: 0,
|
||||
paddingTop: 0,
|
||||
borderTopLeftRadius: 40,
|
||||
borderTopRightRadius: 40,
|
||||
}),
|
||||
native({height: '100%'}),
|
||||
]}
|
||||
webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]}
|
||||
keyboardDismissMode="on-drag"
|
||||
@@ -396,7 +374,8 @@ function ProfileCard({
|
||||
<View style={[a.flex_1, a.gap_2xs]}>
|
||||
<Text
|
||||
style={[t.atoms.text, a.font_bold, a.leading_tight, a.self_start]}
|
||||
numberOfLines={1}>
|
||||
numberOfLines={1}
|
||||
emoji>
|
||||
{displayName}
|
||||
</Text>
|
||||
<Text
|
||||
@@ -474,7 +453,7 @@ function SearchInput({
|
||||
value: string
|
||||
onChangeText: (text: string) => void
|
||||
onEscape: () => void
|
||||
inputRef: React.RefObject<TextInputType>
|
||||
inputRef: React.RefObject<TextInput>
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
|
||||
@@ -2,9 +2,9 @@ import React, {useCallback} from 'react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {logger} from '#/logger'
|
||||
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 Dialog from '#/components/Dialog'
|
||||
import {SearchablePeopleList} from './SearchablePeopleList'
|
||||
@@ -17,10 +17,8 @@ export function SendViaChatDialog({
|
||||
onSelectChat: (chatId: string) => void
|
||||
}) {
|
||||
return (
|
||||
<Dialog.Outer
|
||||
control={control}
|
||||
testID="sendViaChatChatDialog"
|
||||
nativeOptions={{sheet: {snapPoints: ['100%']}}}>
|
||||
<Dialog.Outer control={control} testID="sendViaChatChatDialog">
|
||||
<Dialog.Handle />
|
||||
<SendViaChatDialogInner control={control} onSelectChat={onSelectChat} />
|
||||
</Dialog.Outer>
|
||||
)
|
||||
|
||||
@@ -2,8 +2,8 @@ import React from 'react'
|
||||
import {Pressable, View, ViewStyle} from 'react-native'
|
||||
import Animated, {LinearTransition} from 'react-native-reanimated'
|
||||
|
||||
import {HITSLOP_10} from '#/lib/constants'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {HITSLOP_10} from 'lib/constants'
|
||||
import {
|
||||
atoms as a,
|
||||
flatten,
|
||||
|
||||
@@ -32,7 +32,6 @@ export function LabelsOnMeDialog(props: LabelsOnMeDialogProps) {
|
||||
return (
|
||||
<Dialog.Outer control={props.control}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<LabelsOnMeDialogInner {...props} />
|
||||
</Dialog.Outer>
|
||||
)
|
||||
@@ -158,23 +157,25 @@ function Label({
|
||||
<Divider />
|
||||
|
||||
<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>
|
||||
Source:{' '}
|
||||
<InlineLinkText
|
||||
label={sourceName}
|
||||
to={makeProfileLink(
|
||||
labeler ? labeler.creator : {did: label.src, handle: ''},
|
||||
)}
|
||||
onPress={() => control.close()}>
|
||||
{sourceName}
|
||||
</InlineLinkText>
|
||||
</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</Text>
|
||||
) : (
|
||||
<View style={{flexDirection: 'row'}}>
|
||||
<Text style={[t.atoms.text_contrast_medium]}>
|
||||
<Trans>Source: </Trans>{' '}
|
||||
</Text>
|
||||
<InlineLinkText
|
||||
label={sourceName}
|
||||
to={makeProfileLink(
|
||||
labeler ? labeler.creator : {did: label.src, handle: ''},
|
||||
)}
|
||||
onPress={() => control.close()}>
|
||||
{sourceName}
|
||||
</InlineLinkText>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
@@ -236,24 +237,24 @@ function AppealForm({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text style={[a.text_2xl, a.font_bold, a.pb_xs, a.leading_tight]}>
|
||||
<Trans>Appeal "{strings.name}" label</Trans>
|
||||
</Text>
|
||||
<Text style={[a.text_md, a.leading_snug]}>
|
||||
<Trans>
|
||||
This appeal will be sent to{' '}
|
||||
<InlineLinkText
|
||||
label={sourceName}
|
||||
to={makeProfileLink(
|
||||
labeler ? labeler.creator : {did: label.src, handle: ''},
|
||||
)}
|
||||
onPress={() => control.close()}
|
||||
style={[a.text_md, a.leading_snug]}>
|
||||
{sourceName}
|
||||
</InlineLinkText>
|
||||
.
|
||||
</Trans>
|
||||
</Text>
|
||||
<View style={{flexWrap: 'wrap', flexDirection: 'row'}}>
|
||||
<Text style={[a.text_2xl, a.font_bold, a.pb_xs, a.leading_tight]}>
|
||||
<Trans>Appeal "{strings.name}" label</Trans>
|
||||
</Text>
|
||||
<Text style={[a.text_md, a.leading_snug]}>
|
||||
<Trans>This appeal will be sent to</Trans>{' '}
|
||||
</Text>
|
||||
<InlineLinkText
|
||||
label={sourceName}
|
||||
to={makeProfileLink(
|
||||
labeler ? labeler.creator : {did: label.src, handle: ''},
|
||||
)}
|
||||
onPress={() => control.close()}
|
||||
style={[a.text_md, a.leading_snug]}>
|
||||
{sourceName}
|
||||
</InlineLinkText>
|
||||
<Text style={[a.text_md, a.leading_snug]}>.</Text>
|
||||
</View>
|
||||
<View style={[a.my_md]}>
|
||||
<Dialog.Input
|
||||
label={_(msg`Text input field`)}
|
||||
|
||||
@@ -141,23 +141,24 @@ function ModerationDetailsDialogInner({
|
||||
{modcause?.type === 'label' && (
|
||||
<View style={[a.pt_lg]}>
|
||||
<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{' '}
|
||||
<InlineLinkText
|
||||
label={desc.source || _(msg`an unknown labeler`)}
|
||||
to={makeProfileLink({did: modcause.label.src, handle: ''})}
|
||||
onPress={() => control.close()}
|
||||
style={a.text_md}>
|
||||
{desc.source || _(msg`an unknown labeler`)}
|
||||
</InlineLinkText>
|
||||
.
|
||||
</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</Text>
|
||||
) : (
|
||||
<>
|
||||
<Text style={[t.atoms.text, a.text_md, a.leading_snug, a.mt_lg]}>
|
||||
<Trans>This label was applied by </Trans>
|
||||
</Text>
|
||||
<InlineLinkText
|
||||
label={desc.source || _(msg`an unknown labeler`)}
|
||||
to={makeProfileLink({did: modcause.label.src, handle: ''})}
|
||||
onPress={() => control.close()}
|
||||
style={a.text_md}>
|
||||
{desc.source || _(msg`an unknown labeler`)}
|
||||
</InlineLinkText>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
|
||||
+15
-17
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
AppBskyEmbedDefs,
|
||||
AppBskyEmbedExternal,
|
||||
AppBskyEmbedImages,
|
||||
AppBskyEmbedRecord,
|
||||
@@ -7,7 +6,6 @@ import {
|
||||
AppBskyEmbedVideo,
|
||||
AppBskyFeedPostgate,
|
||||
AtUri,
|
||||
BlobRef,
|
||||
BskyAgent,
|
||||
ComAtprotoLabelDefs,
|
||||
RichText,
|
||||
@@ -46,14 +44,7 @@ interface PostOpts {
|
||||
uri: string
|
||||
cid: string
|
||||
}
|
||||
video?: {
|
||||
blobRef: BlobRef
|
||||
altText: string
|
||||
captions: {lang: string; file: File}[]
|
||||
aspectRatio?: AppBskyEmbedDefs.AspectRatio
|
||||
}
|
||||
extLink?: ExternalEmbedDraft
|
||||
images?: ComposerImage[]
|
||||
labels?: string[]
|
||||
threadgate: ThreadgateAllowUISetting[]
|
||||
postgate: AppBskyFeedPostgate.Record
|
||||
@@ -230,13 +221,15 @@ async function resolveMedia(
|
||||
| AppBskyEmbedVideo.Main
|
||||
| undefined
|
||||
> {
|
||||
if (opts.images?.length) {
|
||||
const state = opts.composerState
|
||||
const media = state.embed.media
|
||||
if (media?.type === 'images') {
|
||||
logger.debug(`Uploading images`, {
|
||||
count: opts.images.length,
|
||||
count: media.images.length,
|
||||
})
|
||||
opts.onStateChange?.(`Uploading images...`)
|
||||
const images: AppBskyEmbedImages.Image[] = await Promise.all(
|
||||
opts.images.map(async (image, i) => {
|
||||
media.images.map(async (image, i) => {
|
||||
logger.debug(`Compressing image #${i}`)
|
||||
const {path, width, height, mime} = await compressImage(image)
|
||||
logger.debug(`Uploading image #${i}`)
|
||||
@@ -253,9 +246,10 @@ async function resolveMedia(
|
||||
images,
|
||||
}
|
||||
}
|
||||
if (opts.video) {
|
||||
if (media?.type === 'video' && media.video.status === 'done') {
|
||||
const video = media.video
|
||||
const captions = await Promise.all(
|
||||
opts.video.captions
|
||||
video.captions
|
||||
.filter(caption => caption.lang !== '')
|
||||
.map(async caption => {
|
||||
const {data} = await agent.uploadBlob(caption.file, {
|
||||
@@ -266,13 +260,17 @@ async function resolveMedia(
|
||||
)
|
||||
return {
|
||||
$type: 'app.bsky.embed.video',
|
||||
video: opts.video.blobRef,
|
||||
alt: opts.video.altText || undefined,
|
||||
video: video.pendingPublish.blobRef,
|
||||
alt: video.altText || undefined,
|
||||
captions: captions.length === 0 ? undefined : captions,
|
||||
aspectRatio: opts.video.aspectRatio,
|
||||
aspectRatio: {
|
||||
width: video.asset.width,
|
||||
height: video.asset.height,
|
||||
},
|
||||
}
|
||||
}
|
||||
if (opts.extLink) {
|
||||
// TODO: Read this from composer state as well.
|
||||
if (opts.extLink.embed) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import {nanoid} from 'nanoid/non-secure'
|
||||
import {AbortError} from '#/lib/async/cancelable'
|
||||
import {ServerError} from '#/lib/media/video/errors'
|
||||
import {CompressedVideo} from '#/lib/media/video/types'
|
||||
import {createVideoEndpointUrl, mimeToExt} from './util'
|
||||
import {getServiceAuthToken, getVideoUploadLimits} from './upload.shared'
|
||||
import {createVideoEndpointUrl, mimeToExt} from './util'
|
||||
|
||||
export async function uploadVideo({
|
||||
video,
|
||||
|
||||
@@ -7,8 +7,8 @@ import {nanoid} from 'nanoid/non-secure'
|
||||
import {AbortError} from '#/lib/async/cancelable'
|
||||
import {ServerError} from '#/lib/media/video/errors'
|
||||
import {CompressedVideo} from '#/lib/media/video/types'
|
||||
import {createVideoEndpointUrl, mimeToExt} from './util'
|
||||
import {getServiceAuthToken, getVideoUploadLimits} from './upload.shared'
|
||||
import {createVideoEndpointUrl, mimeToExt} from './util'
|
||||
|
||||
export async function uploadVideo({
|
||||
video,
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
|
||||
import {IconCircle} from '#/components/IconCircle'
|
||||
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
||||
import {CircleInfo_Stroke2_Corner0_Rounded} from '#/components/icons/CircleInfo'
|
||||
@@ -89,15 +90,18 @@ export function StepProfile() {
|
||||
requestNotificationsPermission('StartOnboarding')
|
||||
}, [gate, requestNotificationsPermission])
|
||||
|
||||
const sheetWrapper = useSheetWrapper()
|
||||
const openPicker = React.useCallback(
|
||||
async (opts?: ImagePickerOptions) => {
|
||||
const response = await launchImageLibraryAsync({
|
||||
exif: false,
|
||||
mediaTypes: MediaTypeOptions.Images,
|
||||
quality: 1,
|
||||
...opts,
|
||||
legacy: true,
|
||||
})
|
||||
const response = await sheetWrapper(
|
||||
launchImageLibraryAsync({
|
||||
exif: false,
|
||||
mediaTypes: MediaTypeOptions.Images,
|
||||
quality: 1,
|
||||
...opts,
|
||||
legacy: true,
|
||||
}),
|
||||
)
|
||||
|
||||
return (response.assets ?? [])
|
||||
.slice(0, 1)
|
||||
@@ -121,7 +125,7 @@ export function StepProfile() {
|
||||
size: getDataUriSize(image.uri),
|
||||
}))
|
||||
},
|
||||
[_, setError],
|
||||
[_, setError, sheetWrapper],
|
||||
)
|
||||
|
||||
const onContinue = React.useCallback(async () => {
|
||||
@@ -168,9 +172,11 @@ export function StepProfile() {
|
||||
|
||||
setError('')
|
||||
|
||||
const items = await openPicker({
|
||||
aspect: [1, 1],
|
||||
})
|
||||
const items = await sheetWrapper(
|
||||
openPicker({
|
||||
aspect: [1, 1],
|
||||
}),
|
||||
)
|
||||
let image = items[0]
|
||||
if (!image) return
|
||||
|
||||
@@ -196,7 +202,13 @@ export function StepProfile() {
|
||||
image,
|
||||
useCreatedAvatar: false,
|
||||
}))
|
||||
}, [requestPhotoAccessIfNeeded, setAvatar, openPicker, setError])
|
||||
}, [
|
||||
requestPhotoAccessIfNeeded,
|
||||
setAvatar,
|
||||
openPicker,
|
||||
setError,
|
||||
sheetWrapper,
|
||||
])
|
||||
|
||||
const onSecondaryPress = React.useCallback(() => {
|
||||
if (avatar.useCreatedAvatar) {
|
||||
@@ -286,7 +298,6 @@ export function StepProfile() {
|
||||
</View>
|
||||
|
||||
<Dialog.Outer control={creatorControl}>
|
||||
<Dialog.Handle />
|
||||
<Dialog.Inner
|
||||
label="Avatar creator"
|
||||
style={[
|
||||
|
||||
@@ -15,35 +15,35 @@ import {useNavigation} from '@react-navigation/native'
|
||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
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 {getStarterPackOgCard} from '#/lib/strings/starter-pack'
|
||||
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 {useStarterPackQuery} from '#/state/queries/starter-packs'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
||||
import {
|
||||
ProgressGuideAction,
|
||||
useProgressGuideControls,
|
||||
} from '#/state/shell/progress-guide'
|
||||
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 {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 {useSetActiveStarterPack} from '#/state/shell/starter-pack'
|
||||
import {PagerWithHeader} from '#/view/com/pager/PagerWithHeader'
|
||||
import {ProfileSubpageHeader} from '#/view/com/profile/ProfileSubpageHeader'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
|
||||
import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader'
|
||||
import {CenteredView} from 'view/com/util/Views'
|
||||
import {CenteredView} from '#/view/com/util/Views'
|
||||
import {bulkWriteFollows} from '#/screens/Onboarding/util'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
@@ -591,7 +591,7 @@ function OverflowMenu({
|
||||
|
||||
<Menu.Item
|
||||
label={_(msg`Report starter pack`)}
|
||||
onPress={reportDialogControl.open}>
|
||||
onPress={() => reportDialogControl.open()}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Report starter pack</Trans>
|
||||
</Menu.ItemText>
|
||||
|
||||
+47
-40
@@ -1,8 +1,9 @@
|
||||
import React from 'react'
|
||||
import {SharedValue, useSharedValue} from 'react-native-reanimated'
|
||||
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {DialogControlRefProps} from '#/components/Dialog'
|
||||
import {Provider as GlobalDialogsProvider} from '#/components/dialogs/Context'
|
||||
import {BottomSheet} from '../../../modules/bottom-sheet'
|
||||
|
||||
interface IDialogContext {
|
||||
/**
|
||||
@@ -16,25 +17,24 @@ interface IDialogContext {
|
||||
* `useId`.
|
||||
*/
|
||||
openDialogs: React.MutableRefObject<Set<string>>
|
||||
}
|
||||
|
||||
interface IDialogControlContext {
|
||||
closeAllDialogs(): boolean
|
||||
setDialogIsOpen(id: string, isOpen: boolean): void
|
||||
/**
|
||||
* The counterpart to `accessibilityViewIsModal` for Android. This property
|
||||
* applies to the parent of all non-modal views, and prevents TalkBack from
|
||||
* navigating within content beneath an open dialog.
|
||||
*
|
||||
* @see https://reactnative.dev/docs/accessibility#importantforaccessibility-android
|
||||
* The number of dialogs that are fully expanded. This is used to determine the backgground color of the status bar
|
||||
* on iOS.
|
||||
*/
|
||||
importantForAccessibility: SharedValue<'auto' | 'no-hide-descendants'>
|
||||
fullyExpandedCount: number
|
||||
setFullyExpandedCount: React.Dispatch<React.SetStateAction<number>>
|
||||
}
|
||||
|
||||
const DialogContext = React.createContext<IDialogContext>({} as IDialogContext)
|
||||
|
||||
const DialogControlContext = React.createContext<{
|
||||
closeAllDialogs(): boolean
|
||||
setDialogIsOpen(id: string, isOpen: boolean): void
|
||||
}>({
|
||||
closeAllDialogs: () => false,
|
||||
setDialogIsOpen: () => {},
|
||||
})
|
||||
const DialogControlContext = React.createContext<IDialogControlContext>(
|
||||
{} as IDialogControlContext,
|
||||
)
|
||||
|
||||
export function useDialogStateContext() {
|
||||
return React.useContext(DialogContext)
|
||||
@@ -45,48 +45,55 @@ export function useDialogStateControlContext() {
|
||||
}
|
||||
|
||||
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
const [fullyExpandedCount, setFullyExpandedCount] = React.useState(0)
|
||||
|
||||
const activeDialogs = React.useRef<
|
||||
Map<string, React.MutableRefObject<DialogControlRefProps>>
|
||||
>(new Map())
|
||||
const openDialogs = React.useRef<Set<string>>(new Set())
|
||||
const importantForAccessibility = useSharedValue<
|
||||
'auto' | 'no-hide-descendants'
|
||||
>('auto')
|
||||
|
||||
const closeAllDialogs = React.useCallback(() => {
|
||||
openDialogs.current.forEach(id => {
|
||||
const dialog = activeDialogs.current.get(id)
|
||||
if (dialog) dialog.current.close()
|
||||
})
|
||||
return openDialogs.current.size > 0
|
||||
if (isWeb) {
|
||||
openDialogs.current.forEach(id => {
|
||||
const dialog = activeDialogs.current.get(id)
|
||||
if (dialog) dialog.current.close()
|
||||
})
|
||||
|
||||
return openDialogs.current.size > 0
|
||||
} else {
|
||||
BottomSheet.dismissAll()
|
||||
return false
|
||||
}
|
||||
}, [])
|
||||
|
||||
const setDialogIsOpen = React.useCallback(
|
||||
(id: string, isOpen: boolean) => {
|
||||
if (isOpen) {
|
||||
openDialogs.current.add(id)
|
||||
importantForAccessibility.value = 'no-hide-descendants'
|
||||
} else {
|
||||
openDialogs.current.delete(id)
|
||||
if (openDialogs.current.size < 1) {
|
||||
importantForAccessibility.value = 'auto'
|
||||
}
|
||||
}
|
||||
},
|
||||
[importantForAccessibility],
|
||||
)
|
||||
const setDialogIsOpen = React.useCallback((id: string, isOpen: boolean) => {
|
||||
if (isOpen) {
|
||||
openDialogs.current.add(id)
|
||||
} else {
|
||||
openDialogs.current.delete(id)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const context = React.useMemo<IDialogContext>(
|
||||
() => ({
|
||||
activeDialogs,
|
||||
openDialogs,
|
||||
importantForAccessibility,
|
||||
}),
|
||||
[importantForAccessibility, activeDialogs, openDialogs],
|
||||
[activeDialogs, openDialogs],
|
||||
)
|
||||
const controls = React.useMemo(
|
||||
() => ({closeAllDialogs, setDialogIsOpen}),
|
||||
[closeAllDialogs, setDialogIsOpen],
|
||||
() => ({
|
||||
closeAllDialogs,
|
||||
setDialogIsOpen,
|
||||
fullyExpandedCount,
|
||||
setFullyExpandedCount,
|
||||
}),
|
||||
[
|
||||
closeAllDialogs,
|
||||
setDialogIsOpen,
|
||||
fullyExpandedCount,
|
||||
setFullyExpandedCount,
|
||||
],
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,14 +2,14 @@ import React from 'react'
|
||||
import {Linking} from 'react-native'
|
||||
import * as WebBrowser from 'expo-web-browser'
|
||||
|
||||
import {isNative} from '#/platform/detection'
|
||||
import * as persisted from '#/state/persisted'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {
|
||||
createBskyAppAbsoluteUrl,
|
||||
isBskyRSSUrl,
|
||||
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'
|
||||
|
||||
type StateContext = persisted.Schema['useInAppBrowser']
|
||||
@@ -62,7 +62,7 @@ export function useOpenLink() {
|
||||
const pal = usePalette('default')
|
||||
|
||||
const openLink = React.useCallback(
|
||||
(url: string, override?: boolean) => {
|
||||
async (url: string, override?: boolean) => {
|
||||
if (isBskyRSSUrl(url) && isRelativeUrl(url)) {
|
||||
url = createBskyAppAbsoluteUrl(url)
|
||||
}
|
||||
@@ -75,7 +75,7 @@ export function useOpenLink() {
|
||||
})
|
||||
return
|
||||
} else if (override ?? enabled) {
|
||||
WebBrowser.openBrowserAsync(url, {
|
||||
await WebBrowser.openBrowserAsync(url, {
|
||||
presentationStyle:
|
||||
WebBrowser.WebBrowserPresentationStyle.FULL_SCREEN,
|
||||
toolbarColor: pal.colors.backgroundLight,
|
||||
|
||||
@@ -6,21 +6,6 @@
|
||||
* may need to touch all three. Ask Eric if you aren't sure.
|
||||
*/
|
||||
|
||||
@font-face {
|
||||
font-family: 'InterVariable';
|
||||
src: url(/assets/fonts/inter/InterVariable.ttf) format('truetype');
|
||||
font-weight: 300 1000;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'InterVariableItalic';
|
||||
src: url(/assets/fonts/inter/InterVariable-Italic.ttf) format('truetype');
|
||||
font-weight: 300 1000;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/**
|
||||
* BEGIN STYLES
|
||||
*
|
||||
|
||||
@@ -66,12 +66,8 @@ export function ServerInputDialog({
|
||||
])
|
||||
|
||||
return (
|
||||
<Dialog.Outer
|
||||
control={control}
|
||||
nativeOptions={{sheet: {snapPoints: ['100%']}}}
|
||||
onClose={onClose}>
|
||||
<Dialog.Outer control={control} onClose={onClose}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.ScrollableInner
|
||||
accessibilityDescribedBy="dialog-description"
|
||||
accessibilityLabelledBy="dialog-title">
|
||||
|
||||
+300
-268
@@ -114,11 +114,14 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
||||
import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||
import {createPortalGroup} from '#/components/Portal'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {Text as NewText} from '#/components/Typography'
|
||||
import {composerReducer, createComposerState} from './state/composer'
|
||||
import {NO_VIDEO, NoVideoState, processVideo, VideoState} from './state/video'
|
||||
|
||||
const Portal = createPortalGroup()
|
||||
|
||||
const MAX_IMAGES = 4
|
||||
|
||||
type CancelRef = {
|
||||
@@ -184,13 +187,10 @@ export const ComposePost = ({
|
||||
initQuote,
|
||||
)
|
||||
|
||||
const [videoAltText, setVideoAltText] = useState('')
|
||||
const [captions, setCaptions] = useState<{lang: string; file: File}[]>([])
|
||||
|
||||
// TODO: Move more state here.
|
||||
const [composerState, dispatch] = useReducer(
|
||||
composerReducer,
|
||||
{initImageUris},
|
||||
{initImageUris, initQuoteUri: initQuote?.uri},
|
||||
createComposerState,
|
||||
)
|
||||
|
||||
@@ -337,6 +337,7 @@ export const ComposePost = ({
|
||||
|
||||
const onNewLink = useCallback(
|
||||
(uri: string) => {
|
||||
dispatch({type: 'embed_add_uri', uri})
|
||||
if (extLink != null) return
|
||||
setExtLink({uri, isLoading: true})
|
||||
},
|
||||
@@ -421,10 +422,9 @@ export const ComposePost = ({
|
||||
try {
|
||||
postUri = (
|
||||
await apilib.post(agent, {
|
||||
composerState, // TODO: not used yet.
|
||||
composerState, // TODO: move more state here.
|
||||
rawText: richtext.text,
|
||||
replyTo: replyTo?.uri,
|
||||
images,
|
||||
quote,
|
||||
extLink,
|
||||
labels,
|
||||
@@ -432,18 +432,6 @@ export const ComposePost = ({
|
||||
postgate,
|
||||
onStateChange: setProcessingState,
|
||||
langs: toPostLanguages(langPrefs.postLanguage),
|
||||
video:
|
||||
videoState.status === 'done'
|
||||
? {
|
||||
blobRef: videoState.pendingPublish.blobRef,
|
||||
altText: videoAltText,
|
||||
captions: captions,
|
||||
aspectRatio: {
|
||||
width: videoState.asset.width,
|
||||
height: videoState.asset.height,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
).uri
|
||||
try {
|
||||
@@ -521,7 +509,6 @@ export const ComposePost = ({
|
||||
[
|
||||
_,
|
||||
agent,
|
||||
captions,
|
||||
composerState,
|
||||
extLink,
|
||||
images,
|
||||
@@ -540,9 +527,7 @@ export const ComposePost = ({
|
||||
setExtLink,
|
||||
setLangPrefs,
|
||||
threadgateAllowUISettings,
|
||||
videoAltText,
|
||||
videoState.asset,
|
||||
videoState.pendingPublish,
|
||||
videoState.status,
|
||||
],
|
||||
)
|
||||
@@ -582,6 +567,7 @@ export const ComposePost = ({
|
||||
|
||||
const onSelectGif = useCallback(
|
||||
(gif: Gif) => {
|
||||
dispatch({type: 'embed_add_gif', gif})
|
||||
setExtLink({
|
||||
uri: `${gif.media_formats.gif.url}?hh=${gif.media_formats.gif.dims[1]}&ww=${gif.media_formats.gif.dims[0]}`,
|
||||
isLoading: true,
|
||||
@@ -600,6 +586,7 @@ export const ComposePost = ({
|
||||
|
||||
const handleChangeGifAltText = useCallback(
|
||||
(altText: string) => {
|
||||
dispatch({type: 'embed_update_gif', alt: altText})
|
||||
setExtLink(ext =>
|
||||
ext && ext.meta
|
||||
? {
|
||||
@@ -629,268 +616,313 @@ export const ComposePost = ({
|
||||
const keyboardVerticalOffset = useKeyboardVerticalOffset()
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
testID="composePostView"
|
||||
behavior={isIOS ? 'padding' : 'height'}
|
||||
keyboardVerticalOffset={keyboardVerticalOffset}
|
||||
style={a.flex_1}>
|
||||
<View style={[a.flex_1, viewStyles]} aria-modal accessibilityViewIsModal>
|
||||
<Animated.View
|
||||
style={topBarAnimatedStyle}
|
||||
layout={native(LinearTransition)}>
|
||||
<View style={styles.topbarInner}>
|
||||
<Button
|
||||
label={_(msg`Cancel`)}
|
||||
variant="ghost"
|
||||
color="primary"
|
||||
shape="default"
|
||||
size="small"
|
||||
style={[
|
||||
a.rounded_full,
|
||||
a.py_sm,
|
||||
{paddingLeft: 7, paddingRight: 7},
|
||||
]}
|
||||
onPress={onPressCancel}
|
||||
accessibilityHint={_(
|
||||
msg`Closes post composer and discards post draft`,
|
||||
)}>
|
||||
<ButtonText style={[a.text_md]}>
|
||||
<Trans>Cancel</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
<View style={a.flex_1} />
|
||||
{isProcessing ? (
|
||||
<>
|
||||
<Text style={pal.textLight}>{processingState}</Text>
|
||||
<View style={styles.postBtn}>
|
||||
<ActivityIndicator />
|
||||
</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>
|
||||
<Portal.Provider>
|
||||
<KeyboardAvoidingView
|
||||
testID="composePostView"
|
||||
behavior={isIOS ? 'padding' : 'height'}
|
||||
keyboardVerticalOffset={keyboardVerticalOffset}
|
||||
style={a.flex_1}>
|
||||
<View
|
||||
style={[a.flex_1, viewStyles]}
|
||||
aria-modal
|
||||
accessibilityViewIsModal>
|
||||
<Animated.View
|
||||
style={topBarAnimatedStyle}
|
||||
layout={native(LinearTransition)}>
|
||||
<View style={styles.topbarInner}>
|
||||
<Button
|
||||
label={_(msg`Cancel`)}
|
||||
variant="ghost"
|
||||
color="primary"
|
||||
shape="default"
|
||||
size="small"
|
||||
style={[
|
||||
a.rounded_full,
|
||||
a.py_sm,
|
||||
{paddingLeft: 7, paddingRight: 7},
|
||||
]}
|
||||
onPress={onPressCancel}
|
||||
accessibilityHint={_(
|
||||
msg`Closes post composer and discards post draft`,
|
||||
)}>
|
||||
<ButtonText style={[a.text_md]}>
|
||||
<Trans>Cancel</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
<View style={a.flex_1} />
|
||||
{isProcessing ? (
|
||||
<>
|
||||
<Text style={pal.textLight}>{processingState}</Text>
|
||||
<View style={styles.postBtn}>
|
||||
<ActivityIndicator />
|
||||
</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>
|
||||
<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}
|
||||
|
||||
{isAltTextRequiredAndMissing && (
|
||||
<View style={[styles.reminderLine, pal.viewLight]}>
|
||||
<View style={styles.errorIcon}>
|
||||
<FontAwesomeIcon
|
||||
icon="exclamation"
|
||||
style={{color: colors.red4}}
|
||||
size={10}
|
||||
<View
|
||||
style={[
|
||||
styles.textInputLayout,
|
||||
isNative && styles.textInputLayoutMobile,
|
||||
]}>
|
||||
<UserAvatar
|
||||
avatar={currentProfile?.avatar}
|
||||
size={50}
|
||||
type={currentProfile?.associated?.labeler ? 'labeler' : 'user'}
|
||||
/>
|
||||
<TextInput
|
||||
ref={textInput}
|
||||
richtext={richtext}
|
||||
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>
|
||||
|
||||
<Gallery
|
||||
images={images}
|
||||
dispatch={dispatch}
|
||||
Portal={Portal.Portal}
|
||||
/>
|
||||
{images.length === 0 && extLink && (
|
||||
<View style={a.relative}>
|
||||
<ExternalEmbed
|
||||
link={extLink}
|
||||
gif={extGif}
|
||||
onRemove={() => {
|
||||
if (extGif) {
|
||||
dispatch({type: 'embed_remove_gif'})
|
||||
} else {
|
||||
dispatch({type: 'embed_remove_link'})
|
||||
}
|
||||
setExtLink(undefined)
|
||||
setExtGif(undefined)
|
||||
}}
|
||||
/>
|
||||
<GifAltText
|
||||
link={extLink}
|
||||
gif={extGif}
|
||||
onSubmit={handleChangeGifAltText}
|
||||
Portal={Portal.Portal}
|
||||
/>
|
||||
</View>
|
||||
<Text style={[pal.text, a.flex_1]}>
|
||||
<Trans>One or more images is missing alt text.</Trans>
|
||||
</Text>
|
||||
)}
|
||||
<LayoutAnimationConfig skipExiting>
|
||||
{hasVideo && (
|
||||
<Animated.View
|
||||
style={[a.w_full, a.mt_lg]}
|
||||
entering={native(ZoomIn)}
|
||||
exiting={native(ZoomOut)}>
|
||||
{videoState.asset &&
|
||||
(videoState.status === 'compressing' ? (
|
||||
<VideoTranscodeProgress
|
||||
asset={videoState.asset}
|
||||
progress={videoState.progress}
|
||||
clear={clearVideo}
|
||||
/>
|
||||
) : videoState.video ? (
|
||||
<VideoPreview
|
||||
asset={videoState.asset}
|
||||
video={videoState.video}
|
||||
setDimensions={updateVideoDimensions}
|
||||
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,
|
||||
},
|
||||
})
|
||||
}}
|
||||
Portal={Portal.Portal}
|
||||
/>
|
||||
</Animated.View>
|
||||
)}
|
||||
</LayoutAnimationConfig>
|
||||
<View style={!hasVideo ? [a.mt_md] : []}>
|
||||
{quote ? (
|
||||
<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}
|
||||
</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}
|
||||
</Animated.ScrollView>
|
||||
<SuggestedLanguage text={richtext.text} />
|
||||
|
||||
{replyTo ? null : (
|
||||
<ThreadgateBtn
|
||||
postgate={postgate}
|
||||
onChangePostgate={setPostgate}
|
||||
threadgateAllowUISettings={threadgateAllowUISettings}
|
||||
onChangeThreadgateAllowUISettings={
|
||||
onChangeThreadgateAllowUISettings
|
||||
}
|
||||
style={bottomBarAnimatedStyle}
|
||||
Portal={Portal.Portal}
|
||||
/>
|
||||
)}
|
||||
<View
|
||||
style={[
|
||||
styles.textInputLayout,
|
||||
isNative && styles.textInputLayoutMobile,
|
||||
t.atoms.bg,
|
||||
t.atoms.border_contrast_medium,
|
||||
styles.bottomBar,
|
||||
]}>
|
||||
<UserAvatar
|
||||
avatar={currentProfile?.avatar}
|
||||
size={50}
|
||||
type={currentProfile?.associated?.labeler ? 'labeler' : 'user'}
|
||||
/>
|
||||
<TextInput
|
||||
ref={textInput}
|
||||
richtext={richtext}
|
||||
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>
|
||||
|
||||
<Gallery images={images} dispatch={dispatch} />
|
||||
{images.length === 0 && extLink && (
|
||||
<View style={a.relative}>
|
||||
<ExternalEmbed
|
||||
link={extLink}
|
||||
gif={extGif}
|
||||
onRemove={() => {
|
||||
setExtLink(undefined)
|
||||
setExtGif(undefined)
|
||||
}}
|
||||
/>
|
||||
<GifAltText
|
||||
link={extLink}
|
||||
gif={extGif}
|
||||
onSubmit={handleChangeGifAltText}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
<LayoutAnimationConfig skipExiting>
|
||||
{hasVideo && (
|
||||
<Animated.View
|
||||
style={[a.w_full, a.mt_lg]}
|
||||
entering={native(ZoomIn)}
|
||||
exiting={native(ZoomOut)}>
|
||||
{videoState.asset &&
|
||||
(videoState.status === 'compressing' ? (
|
||||
<VideoTranscodeProgress
|
||||
asset={videoState.asset}
|
||||
progress={videoState.progress}
|
||||
clear={clearVideo}
|
||||
/>
|
||||
) : videoState.video ? (
|
||||
<VideoPreview
|
||||
asset={videoState.asset}
|
||||
video={videoState.video}
|
||||
setDimensions={updateVideoDimensions}
|
||||
clear={clearVideo}
|
||||
/>
|
||||
) : null)}
|
||||
<SubtitleDialogBtn
|
||||
defaultAltText={videoAltText}
|
||||
saveAltText={setVideoAltText}
|
||||
captions={captions}
|
||||
setCaptions={setCaptions}
|
||||
{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}
|
||||
/>
|
||||
</Animated.View>
|
||||
<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>
|
||||
)}
|
||||
</LayoutAnimationConfig>
|
||||
<View style={!hasVideo ? [a.mt_md] : []}>
|
||||
{quote ? (
|
||||
<View style={[s.mt5, s.mb2, isWeb && s.mb10]}>
|
||||
<View style={{pointerEvents: 'none'}}>
|
||||
<QuoteEmbed quote={quote} />
|
||||
</View>
|
||||
{quote.uri !== initQuote?.uri && (
|
||||
<QuoteX onRemove={() => setQuote(undefined)} />
|
||||
)}
|
||||
</View>
|
||||
) : null}
|
||||
<View style={a.flex_1} />
|
||||
<SelectLangBtn />
|
||||
<CharProgress count={graphemeLength} />
|
||||
</View>
|
||||
</Animated.ScrollView>
|
||||
<SuggestedLanguage text={richtext.text} />
|
||||
|
||||
{replyTo ? null : (
|
||||
<ThreadgateBtn
|
||||
postgate={postgate}
|
||||
onChangePostgate={setPostgate}
|
||||
threadgateAllowUISettings={threadgateAllowUISettings}
|
||||
onChangeThreadgateAllowUISettings={
|
||||
onChangeThreadgateAllowUISettings
|
||||
}
|
||||
style={bottomBarAnimatedStyle}
|
||||
/>
|
||||
)}
|
||||
<View
|
||||
style={[
|
||||
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}
|
||||
/>
|
||||
{!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>
|
||||
<Prompt.Basic
|
||||
control={discardPromptControl}
|
||||
title={_(msg`Discard draft?`)}
|
||||
description={_(msg`Are you sure you'd like to discard this draft?`)}
|
||||
onConfirm={onClose}
|
||||
confirmButtonCta={_(msg`Discard`)}
|
||||
confirmButtonColor="negative"
|
||||
/>
|
||||
</KeyboardAvoidingView>
|
||||
<Prompt.Basic
|
||||
control={discardPromptControl}
|
||||
title={_(msg`Discard draft?`)}
|
||||
description={_(msg`Are you sure you'd like to discard this draft?`)}
|
||||
onConfirm={onClose}
|
||||
confirmButtonCta={_(msg`Discard`)}
|
||||
confirmButtonColor="negative"
|
||||
Portal={Portal.Portal}
|
||||
/>
|
||||
</KeyboardAvoidingView>
|
||||
<Portal.Outlet />
|
||||
</Portal.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import * as Dialog from '#/components/Dialog'
|
||||
import * as TextField from '#/components/forms/TextField'
|
||||
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||
import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||
import {PortalComponent} from '#/components/Portal'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {GifEmbed} from '../util/post-embeds/GifEmbed'
|
||||
import {AltTextReminder} from './photos/Gallery'
|
||||
@@ -28,10 +29,12 @@ export function GifAltText({
|
||||
link: linkProp,
|
||||
gif,
|
||||
onSubmit,
|
||||
Portal,
|
||||
}: {
|
||||
link: ExternalEmbedDraft
|
||||
gif?: Gif
|
||||
onSubmit: (alt: string) => void
|
||||
Portal: PortalComponent
|
||||
}) {
|
||||
const control = Dialog.useDialogControl()
|
||||
const {_} = useLingui()
|
||||
@@ -96,9 +99,7 @@ export function GifAltText({
|
||||
|
||||
<AltTextReminder />
|
||||
|
||||
<Dialog.Outer
|
||||
control={control}
|
||||
nativeOptions={isAndroid ? {sheet: {snapPoints: ['100%']}} : {}}>
|
||||
<Dialog.Outer control={control} Portal={Portal}>
|
||||
<Dialog.Handle />
|
||||
<AltTextInner
|
||||
onSubmit={onPressSubmit}
|
||||
@@ -185,6 +186,8 @@ function AltTextInner({
|
||||
</View>
|
||||
</View>
|
||||
<Dialog.Close />
|
||||
{/* Maybe fix this later -h */}
|
||||
{isAndroid ? <View style={{height: 300}} /> : null}
|
||||
</Dialog.ScrollableInner>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import {EditImageDialogProps} from './EditImageDialog'
|
||||
export const EditImageDialog = (props: EditImageDialogProps) => {
|
||||
return (
|
||||
<Dialog.Outer control={props.control}>
|
||||
<Dialog.Handle />
|
||||
<EditImageInner key={props.image.source.id} {...props} />
|
||||
</Dialog.Outer>
|
||||
)
|
||||
|
||||
@@ -21,6 +21,7 @@ import {ComposerImage, cropImage} from '#/state/gallery'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import {useTheme} from '#/alf'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {PortalComponent} from '#/components/Portal'
|
||||
import {ComposerAction} from '../state/composer'
|
||||
import {EditImageDialog} from './EditImageDialog'
|
||||
import {ImageAltTextDialog} from './ImageAltTextDialog'
|
||||
@@ -30,6 +31,7 @@ const IMAGE_GAP = 8
|
||||
interface GalleryProps {
|
||||
images: ComposerImage[]
|
||||
dispatch: (action: ComposerAction) => void
|
||||
Portal: PortalComponent
|
||||
}
|
||||
|
||||
export let Gallery = (props: GalleryProps): React.ReactNode => {
|
||||
@@ -57,7 +59,12 @@ interface GalleryInnerProps extends GalleryProps {
|
||||
containerInfo: Dimensions
|
||||
}
|
||||
|
||||
const GalleryInner = ({images, containerInfo, dispatch}: GalleryInnerProps) => {
|
||||
const GalleryInner = ({
|
||||
images,
|
||||
containerInfo,
|
||||
dispatch,
|
||||
Portal,
|
||||
}: GalleryInnerProps) => {
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
|
||||
const {altTextControlStyle, imageControlsStyle, imageStyle} =
|
||||
@@ -111,6 +118,7 @@ const GalleryInner = ({images, containerInfo, dispatch}: GalleryInnerProps) => {
|
||||
onRemove={() => {
|
||||
dispatch({type: 'embed_remove_image', image})
|
||||
}}
|
||||
Portal={Portal}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
@@ -127,6 +135,7 @@ type GalleryItemProps = {
|
||||
imageStyle?: ViewStyle
|
||||
onChange: (next: ComposerImage) => void
|
||||
onRemove: () => void
|
||||
Portal: PortalComponent
|
||||
}
|
||||
|
||||
const GalleryItem = ({
|
||||
@@ -136,6 +145,7 @@ const GalleryItem = ({
|
||||
imageStyle,
|
||||
onChange,
|
||||
onRemove,
|
||||
Portal,
|
||||
}: GalleryItemProps): React.ReactNode => {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
@@ -230,6 +240,7 @@ const GalleryItem = ({
|
||||
control={altTextControl}
|
||||
image={image}
|
||||
onChange={onChange}
|
||||
Portal={Portal}
|
||||
/>
|
||||
|
||||
<EditImageDialog
|
||||
|
||||
@@ -5,25 +5,26 @@ import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {MAX_ALT_TEXT} from '#/lib/constants'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {isAndroid, isWeb} from '#/platform/detection'
|
||||
import {ComposerImage} from '#/state/gallery'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import * as TextField from '#/components/forms/TextField'
|
||||
import {PortalComponent} from '#/components/Portal'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
type Props = {
|
||||
control: Dialog.DialogOuterProps['control']
|
||||
image: ComposerImage
|
||||
onChange: (next: ComposerImage) => void
|
||||
Portal: PortalComponent
|
||||
}
|
||||
|
||||
export const ImageAltTextDialog = (props: Props): React.ReactNode => {
|
||||
return (
|
||||
<Dialog.Outer control={props.control}>
|
||||
<Dialog.Outer control={props.control} Portal={props.Portal}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<ImageAltTextInner {...props} />
|
||||
</Dialog.Outer>
|
||||
)
|
||||
@@ -116,6 +117,8 @@ const ImageAltTextInner = ({
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
{/* Maybe fix this later -h */}
|
||||
{isAndroid ? <View style={{height: 300}} /> : null}
|
||||
</Dialog.ScrollableInner>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,14 +9,16 @@ import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import {GifSelectDialog} from '#/components/dialogs/GifSelect'
|
||||
import {GifSquare_Stroke2_Corner0_Rounded as GifIcon} from '#/components/icons/Gif'
|
||||
import {PortalComponent} from '#/components/Portal'
|
||||
|
||||
type Props = {
|
||||
onClose: () => void
|
||||
onSelectGif: (gif: Gif) => void
|
||||
disabled?: boolean
|
||||
Portal?: PortalComponent
|
||||
}
|
||||
|
||||
export function SelectGifBtn({onClose, onSelectGif, disabled}: Props) {
|
||||
export function SelectGifBtn({onClose, onSelectGif, disabled, Portal}: Props) {
|
||||
const {_} = useLingui()
|
||||
const ref = useRef<{open: () => void}>(null)
|
||||
const t = useTheme()
|
||||
@@ -46,6 +48,7 @@ export function SelectGifBtn({onClose, onSelectGif, disabled}: Props) {
|
||||
controlRef={ref}
|
||||
onClose={onClose}
|
||||
onSelectGif={onSelectGif}
|
||||
Portal={Portal}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -9,6 +9,7 @@ import {isNative} from '#/platform/detection'
|
||||
import {ComposerImage, createComposerImage} from '#/state/gallery'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
|
||||
import {Image_Stroke2_Corner0_Rounded as Image} from '#/components/icons/Image'
|
||||
|
||||
type Props = {
|
||||
@@ -21,23 +22,26 @@ export function SelectPhotoBtn({size, disabled, onAdd}: Props) {
|
||||
const {_} = useLingui()
|
||||
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
|
||||
const t = useTheme()
|
||||
const sheetWrapper = useSheetWrapper()
|
||||
|
||||
const onPressSelectPhotos = useCallback(async () => {
|
||||
if (isNative && !(await requestPhotoAccessIfNeeded())) {
|
||||
return
|
||||
}
|
||||
|
||||
const images = await openPicker({
|
||||
selectionLimit: 4 - size,
|
||||
allowsMultipleSelection: true,
|
||||
})
|
||||
const images = await sheetWrapper(
|
||||
openPicker({
|
||||
selectionLimit: 4 - size,
|
||||
allowsMultipleSelection: true,
|
||||
}),
|
||||
)
|
||||
|
||||
const results = await Promise.all(
|
||||
images.map(img => createComposerImage(img)),
|
||||
)
|
||||
|
||||
onAdd(results)
|
||||
}, [requestPhotoAccessIfNeeded, size, onAdd])
|
||||
}, [requestPhotoAccessIfNeeded, size, onAdd, sheetWrapper])
|
||||
|
||||
return (
|
||||
<Button
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import {ImagePickerAsset} from 'expo-image-picker'
|
||||
|
||||
import {isBskyPostUrl} from '#/lib/strings/url-helpers'
|
||||
import {ComposerImage, createInitialImages} from '#/state/gallery'
|
||||
import {Gif} from '#/state/queries/tenor'
|
||||
import {ComposerOpts} from '#/state/shell/composer'
|
||||
import {createVideoState, VideoAction, videoReducer, VideoState} from './video'
|
||||
|
||||
type PostRecord = {
|
||||
uri: string
|
||||
}
|
||||
|
||||
type ImagesMedia = {
|
||||
type: 'images'
|
||||
images: ComposerImage[]
|
||||
labels: string[]
|
||||
}
|
||||
|
||||
type VideoMedia = {
|
||||
@@ -19,16 +16,30 @@ type VideoMedia = {
|
||||
video: VideoState
|
||||
}
|
||||
|
||||
type ComposerEmbed = {
|
||||
// TODO: Other record types.
|
||||
record: PostRecord | undefined
|
||||
// TODO: Other media types.
|
||||
media: ImagesMedia | VideoMedia | undefined
|
||||
type GifMedia = {
|
||||
type: 'gif'
|
||||
gif: Gif
|
||||
alt: string
|
||||
}
|
||||
|
||||
type Link = {
|
||||
type: 'link'
|
||||
uri: string
|
||||
}
|
||||
|
||||
// This structure doesn't exactly correspond to the data model.
|
||||
// Instead, it maps to how the UI is organized, and how we present a post.
|
||||
type EmbedDraft = {
|
||||
// We'll always submit quote and actual media (images, video, gifs) chosen by the user.
|
||||
quote: Link | undefined
|
||||
media: ImagesMedia | VideoMedia | GifMedia | undefined
|
||||
// This field may end up ignored if we have more important things to display than a link card:
|
||||
link: Link | undefined
|
||||
}
|
||||
|
||||
export type ComposerState = {
|
||||
// TODO: Other draft data.
|
||||
embed: ComposerEmbed
|
||||
embed: EmbedDraft
|
||||
}
|
||||
|
||||
export type ComposerAction =
|
||||
@@ -42,6 +53,12 @@ export type ComposerAction =
|
||||
}
|
||||
| {type: 'embed_remove_video'}
|
||||
| {type: 'embed_update_video'; videoAction: VideoAction}
|
||||
| {type: 'embed_add_uri'; uri: string}
|
||||
| {type: 'embed_remove_quote'}
|
||||
| {type: 'embed_remove_link'}
|
||||
| {type: 'embed_add_gif'; gif: Gif}
|
||||
| {type: 'embed_update_gif'; alt: string}
|
||||
| {type: 'embed_remove_gif'}
|
||||
|
||||
const MAX_IMAGES = 4
|
||||
|
||||
@@ -60,7 +77,6 @@ export function composerReducer(
|
||||
nextMedia = {
|
||||
type: 'images',
|
||||
images: action.images.slice(0, MAX_IMAGES),
|
||||
labels: [],
|
||||
}
|
||||
} else if (prevMedia.type === 'images') {
|
||||
nextMedia = {
|
||||
@@ -171,6 +187,102 @@ export function composerReducer(
|
||||
},
|
||||
}
|
||||
}
|
||||
case 'embed_add_uri': {
|
||||
const prevQuote = state.embed.quote
|
||||
const prevLink = state.embed.link
|
||||
let nextQuote = prevQuote
|
||||
let nextLink = prevLink
|
||||
if (isBskyPostUrl(action.uri)) {
|
||||
if (!prevQuote) {
|
||||
nextQuote = {
|
||||
type: 'link',
|
||||
uri: action.uri,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!prevLink) {
|
||||
nextLink = {
|
||||
type: 'link',
|
||||
uri: action.uri,
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
embed: {
|
||||
...state.embed,
|
||||
quote: nextQuote,
|
||||
link: nextLink,
|
||||
},
|
||||
}
|
||||
}
|
||||
case 'embed_remove_link': {
|
||||
return {
|
||||
...state,
|
||||
embed: {
|
||||
...state.embed,
|
||||
link: undefined,
|
||||
},
|
||||
}
|
||||
}
|
||||
case 'embed_remove_quote': {
|
||||
return {
|
||||
...state,
|
||||
embed: {
|
||||
...state.embed,
|
||||
quote: undefined,
|
||||
},
|
||||
}
|
||||
}
|
||||
case 'embed_add_gif': {
|
||||
const prevMedia = state.embed.media
|
||||
let nextMedia = prevMedia
|
||||
if (!prevMedia) {
|
||||
nextMedia = {
|
||||
type: 'gif',
|
||||
gif: action.gif,
|
||||
alt: '',
|
||||
}
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
embed: {
|
||||
...state.embed,
|
||||
media: nextMedia,
|
||||
},
|
||||
}
|
||||
}
|
||||
case 'embed_update_gif': {
|
||||
const prevMedia = state.embed.media
|
||||
let nextMedia = prevMedia
|
||||
if (prevMedia?.type === 'gif') {
|
||||
nextMedia = {
|
||||
...prevMedia,
|
||||
alt: action.alt,
|
||||
}
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
embed: {
|
||||
...state.embed,
|
||||
media: nextMedia,
|
||||
},
|
||||
}
|
||||
}
|
||||
case 'embed_remove_gif': {
|
||||
const prevMedia = state.embed.media
|
||||
let nextMedia = prevMedia
|
||||
if (prevMedia?.type === 'gif') {
|
||||
nextMedia = undefined
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
embed: {
|
||||
...state.embed,
|
||||
media: nextMedia,
|
||||
},
|
||||
}
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
@@ -178,22 +290,31 @@ export function composerReducer(
|
||||
|
||||
export function createComposerState({
|
||||
initImageUris,
|
||||
initQuoteUri,
|
||||
}: {
|
||||
initImageUris: ComposerOpts['imageUris']
|
||||
initQuoteUri: string | undefined
|
||||
}): ComposerState {
|
||||
let media: ImagesMedia | undefined
|
||||
if (initImageUris?.length) {
|
||||
media = {
|
||||
type: 'images',
|
||||
images: createInitialImages(initImageUris),
|
||||
labels: [],
|
||||
}
|
||||
}
|
||||
// TODO: initial video.
|
||||
let quote: Link | undefined
|
||||
if (initQuoteUri) {
|
||||
quote = {
|
||||
type: 'link',
|
||||
uri: initQuoteUri,
|
||||
}
|
||||
}
|
||||
// TODO: Other initial content.
|
||||
return {
|
||||
embed: {
|
||||
record: undefined,
|
||||
quote,
|
||||
media,
|
||||
link: undefined,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ import {JobStatus} from '@atproto/api/dist/client/types/app/bsky/video/defs'
|
||||
import {I18n} from '@lingui/core'
|
||||
import {msg} from '@lingui/macro'
|
||||
|
||||
import {createVideoAgent} from '#/lib/media/video/util'
|
||||
import {uploadVideo} from '#/lib/media/video/upload'
|
||||
import {AbortError} from '#/lib/async/cancelable'
|
||||
import {compressVideo} from '#/lib/media/video/compress'
|
||||
import {
|
||||
@@ -14,8 +12,12 @@ import {
|
||||
VideoTooLargeError,
|
||||
} from '#/lib/media/video/errors'
|
||||
import {CompressedVideo} from '#/lib/media/video/types'
|
||||
import {uploadVideo} from '#/lib/media/video/upload'
|
||||
import {createVideoAgent} from '#/lib/media/video/util'
|
||||
import {logger} from '#/logger'
|
||||
|
||||
type CaptionsTrack = {lang: string; file: File}
|
||||
|
||||
export type VideoAction =
|
||||
| {
|
||||
type: 'compressing_to_uploading'
|
||||
@@ -40,6 +42,16 @@ export type VideoAction =
|
||||
height: number
|
||||
signal: AbortSignal
|
||||
}
|
||||
| {
|
||||
type: 'update_alt_text'
|
||||
altText: string
|
||||
signal: AbortSignal
|
||||
}
|
||||
| {
|
||||
type: 'update_captions'
|
||||
updater: (prev: CaptionsTrack[]) => CaptionsTrack[]
|
||||
signal: AbortSignal
|
||||
}
|
||||
| {
|
||||
type: 'update_job_status'
|
||||
jobStatus: AppBskyVideoDefs.JobStatus
|
||||
@@ -57,6 +69,8 @@ export const NO_VIDEO = Object.freeze({
|
||||
video: undefined,
|
||||
jobId: undefined,
|
||||
pendingPublish: undefined,
|
||||
altText: '',
|
||||
captions: [],
|
||||
})
|
||||
|
||||
export type NoVideoState = typeof NO_VIDEO
|
||||
@@ -70,6 +84,8 @@ type ErrorState = {
|
||||
jobId: string | null
|
||||
error: string
|
||||
pendingPublish?: undefined
|
||||
altText: string
|
||||
captions: CaptionsTrack[]
|
||||
}
|
||||
|
||||
type CompressingState = {
|
||||
@@ -80,6 +96,8 @@ type CompressingState = {
|
||||
video?: undefined
|
||||
jobId?: undefined
|
||||
pendingPublish?: undefined
|
||||
altText: string
|
||||
captions: CaptionsTrack[]
|
||||
}
|
||||
|
||||
type UploadingState = {
|
||||
@@ -90,6 +108,8 @@ type UploadingState = {
|
||||
video: CompressedVideo
|
||||
jobId?: undefined
|
||||
pendingPublish?: undefined
|
||||
altText: string
|
||||
captions: CaptionsTrack[]
|
||||
}
|
||||
|
||||
type ProcessingState = {
|
||||
@@ -101,6 +121,8 @@ type ProcessingState = {
|
||||
jobId: string
|
||||
jobStatus: AppBskyVideoDefs.JobStatus | null
|
||||
pendingPublish?: undefined
|
||||
altText: string
|
||||
captions: CaptionsTrack[]
|
||||
}
|
||||
|
||||
type DoneState = {
|
||||
@@ -111,6 +133,8 @@ type DoneState = {
|
||||
video: CompressedVideo
|
||||
jobId?: undefined
|
||||
pendingPublish: {blobRef: BlobRef; mutableProcessed: boolean}
|
||||
altText: string
|
||||
captions: CaptionsTrack[]
|
||||
}
|
||||
|
||||
export type VideoState =
|
||||
@@ -129,6 +153,8 @@ export function createVideoState(
|
||||
progress: 0,
|
||||
abortController,
|
||||
asset,
|
||||
altText: '',
|
||||
captions: [],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,6 +175,8 @@ export function videoReducer(
|
||||
asset: state.asset ?? null,
|
||||
video: state.video ?? null,
|
||||
jobId: state.jobId ?? null,
|
||||
altText: state.altText,
|
||||
captions: state.captions,
|
||||
}
|
||||
} else if (action.type === 'update_progress') {
|
||||
if (state.status === 'compressing' || state.status === 'uploading') {
|
||||
@@ -164,6 +192,16 @@ export function videoReducer(
|
||||
asset: {...state.asset, width: action.width, height: action.height},
|
||||
}
|
||||
}
|
||||
} else if (action.type === 'update_alt_text') {
|
||||
return {
|
||||
...state,
|
||||
altText: action.altText,
|
||||
}
|
||||
} else if (action.type === 'update_captions') {
|
||||
return {
|
||||
...state,
|
||||
captions: action.updater(state.captions),
|
||||
}
|
||||
} else if (action.type === 'compressing_to_uploading') {
|
||||
if (state.status === 'compressing') {
|
||||
return {
|
||||
@@ -172,6 +210,8 @@ export function videoReducer(
|
||||
abortController: state.abortController,
|
||||
asset: state.asset,
|
||||
video: action.video,
|
||||
altText: state.altText,
|
||||
captions: state.captions,
|
||||
}
|
||||
}
|
||||
return state
|
||||
@@ -185,6 +225,8 @@ export function videoReducer(
|
||||
video: state.video,
|
||||
jobId: action.jobId,
|
||||
jobStatus: null,
|
||||
altText: state.altText,
|
||||
captions: state.captions,
|
||||
}
|
||||
}
|
||||
} else if (action.type === 'update_job_status') {
|
||||
@@ -210,6 +252,8 @@ export function videoReducer(
|
||||
blobRef: action.blobRef,
|
||||
mutableProcessed: false,
|
||||
},
|
||||
altText: state.altText,
|
||||
captions: state.captions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import * as Dialog from '#/components/Dialog'
|
||||
import {PostInteractionSettingsControlledDialog} from '#/components/dialogs/PostInteractionSettingsDialog'
|
||||
import {Earth_Stroke2_Corner0_Rounded as Earth} from '#/components/icons/Globe'
|
||||
import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
|
||||
import {PortalComponent} from '#/components/Portal'
|
||||
|
||||
export function ThreadgateBtn({
|
||||
postgate,
|
||||
@@ -20,6 +21,7 @@ export function ThreadgateBtn({
|
||||
threadgateAllowUISettings,
|
||||
onChangeThreadgateAllowUISettings,
|
||||
style,
|
||||
Portal,
|
||||
}: {
|
||||
postgate: AppBskyFeedPostgate.Record
|
||||
onChangePostgate: (v: AppBskyFeedPostgate.Record) => void
|
||||
@@ -28,6 +30,8 @@ export function ThreadgateBtn({
|
||||
onChangeThreadgateAllowUISettings: (v: ThreadgateAllowUISetting[]) => void
|
||||
|
||||
style?: StyleProp<AnimatedStyle<ViewStyle>>
|
||||
|
||||
Portal: PortalComponent
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
@@ -77,6 +81,7 @@ export function ThreadgateBtn({
|
||||
onChangePostgate={onChangePostgate}
|
||||
threadgateAllowUISettings={threadgateAllowUISettings}
|
||||
onChangeThreadgateAllowUISettings={onChangeThreadgateAllowUISettings}
|
||||
Portal={Portal}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ import {useLingui} from '@lingui/react'
|
||||
import {MAX_ALT_TEXT} from '#/lib/constants'
|
||||
import {useEnforceMaxGraphemeCount} from '#/lib/strings/helpers'
|
||||
import {LANGUAGES} from '#/locale/languages'
|
||||
import {isAndroid, isWeb} from '#/platform/detection'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useLanguagePrefs} from '#/state/preferences'
|
||||
import {atoms as a, useTheme, web} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
@@ -17,18 +17,20 @@ import {CC_Stroke2_Corner0_Rounded as CCIcon} from '#/components/icons/CC'
|
||||
import {PageText_Stroke2_Corner0_Rounded as PageTextIcon} from '#/components/icons/PageText'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
|
||||
import {PortalComponent} from '#/components/Portal'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {SubtitleFilePicker} from './SubtitleFilePicker'
|
||||
|
||||
const MAX_NUM_CAPTIONS = 1
|
||||
|
||||
type CaptionsTrack = {lang: string; file: File}
|
||||
|
||||
interface Props {
|
||||
defaultAltText: string
|
||||
captions: {lang: string; file: File}[]
|
||||
captions: CaptionsTrack[]
|
||||
saveAltText: (altText: string) => void
|
||||
setCaptions: React.Dispatch<
|
||||
React.SetStateAction<{lang: string; file: File}[]>
|
||||
>
|
||||
setCaptions: (updater: (prev: CaptionsTrack[]) => CaptionsTrack[]) => void
|
||||
Portal: PortalComponent
|
||||
}
|
||||
|
||||
export function SubtitleDialogBtn(props: Props) {
|
||||
@@ -56,9 +58,7 @@ export function SubtitleDialogBtn(props: Props) {
|
||||
{isWeb ? <Trans>Captions & alt text</Trans> : <Trans>Alt text</Trans>}
|
||||
</ButtonText>
|
||||
</Button>
|
||||
<Dialog.Outer
|
||||
control={control}
|
||||
nativeOptions={isAndroid ? {sheet: {snapPoints: ['60%']}} : {}}>
|
||||
<Dialog.Outer control={control} Portal={props.Portal}>
|
||||
<Dialog.Handle />
|
||||
<SubtitleDialogInner {...props} />
|
||||
</Dialog.Outer>
|
||||
@@ -198,9 +198,7 @@ function SubtitleFileRow({
|
||||
language: string
|
||||
file: File
|
||||
otherLanguages: {code2: string; code3: string; name: string}[]
|
||||
setCaptions: React.Dispatch<
|
||||
React.SetStateAction<{lang: string; file: File}[]>
|
||||
>
|
||||
setCaptions: (updater: (prev: CaptionsTrack[]) => CaptionsTrack[]) => void
|
||||
style: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
|
||||
@@ -48,7 +48,7 @@ export function PostThreadComposePrompt({
|
||||
accessibilityHint={_(msg`Opens composer`)}
|
||||
style={[
|
||||
gtMobile ? a.py_xs : {paddingTop: 8, paddingBottom: 11},
|
||||
gtMobile ? {paddingLeft: 6, paddingRight: 6} : a.px_sm,
|
||||
a.px_sm,
|
||||
a.border_t,
|
||||
t.atoms.border_contrast_low,
|
||||
t.atoms.bg,
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import React from 'react'
|
||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
import {AppBskyActorDefs} from '@atproto/api'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {s} from '#/lib/styles'
|
||||
import {logger} from '#/logger'
|
||||
import {Shadow, useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {
|
||||
@@ -16,8 +11,11 @@ import {
|
||||
useProfileQuery,
|
||||
} from '#/state/queries/profile'
|
||||
import {useRequireAuth} from '#/state/session'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a, useBreakpoints} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||
|
||||
export function PostThreadFollowBtn({did}: {did: string}) {
|
||||
const {data: profile, isLoading} = useProfileQuery({did})
|
||||
@@ -36,9 +34,7 @@ function PostThreadFollowBtnLoaded({
|
||||
}) {
|
||||
const navigation = useNavigation()
|
||||
const {_} = useLingui()
|
||||
const pal = usePalette('default')
|
||||
const palInverted = usePalette('inverted')
|
||||
const {isTabletOrDesktop} = useWebMediaQueries()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const profile: Shadow<AppBskyActorDefs.ProfileViewBasic> =
|
||||
useProfileShadow(profileUnshadowed)
|
||||
const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
|
||||
@@ -113,51 +109,32 @@ function PostThreadFollowBtnLoaded({
|
||||
if (!showFollowBtn) return null
|
||||
|
||||
return (
|
||||
<View style={{width: isTabletOrDesktop ? 130 : 120}}>
|
||||
<View style={styles.btnOuter}>
|
||||
<TouchableOpacity
|
||||
testID="followBtn"
|
||||
onPress={onPress}
|
||||
style={[styles.btn, !isFollowing ? palInverted.view : pal.viewLight]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Follow ${profile.handle}`)}
|
||||
accessibilityHint={_(
|
||||
msg`Shows posts from ${profile.handle} in your feed`,
|
||||
)}>
|
||||
{isTabletOrDesktop && (
|
||||
<FontAwesomeIcon
|
||||
icon={!isFollowing ? 'plus' : 'check'}
|
||||
style={[!isFollowing ? palInverted.text : pal.text, s.mr5]}
|
||||
/>
|
||||
)}
|
||||
<Text
|
||||
type="button"
|
||||
style={[!isFollowing ? palInverted.text : pal.text, s.bold]}
|
||||
numberOfLines={1}>
|
||||
{!isFollowing ? (
|
||||
isFollowedBy ? (
|
||||
<Trans>Follow Back</Trans>
|
||||
) : (
|
||||
<Trans>Follow</Trans>
|
||||
)
|
||||
) : (
|
||||
<Trans>Following</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
<Button
|
||||
testID="followBtn"
|
||||
label={_(msg`Follow ${profile.handle}`)}
|
||||
onPress={onPress}
|
||||
size="small"
|
||||
variant="solid"
|
||||
color="secondary_inverted"
|
||||
style={[a.rounded_full]}>
|
||||
{gtMobile && (
|
||||
<ButtonIcon
|
||||
icon={isFollowing ? Check : Plus}
|
||||
position="left"
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
<ButtonText>
|
||||
{!isFollowing ? (
|
||||
isFollowedBy ? (
|
||||
<Trans>Follow Back</Trans>
|
||||
) : (
|
||||
<Trans>Follow</Trans>
|
||||
)
|
||||
) : (
|
||||
<Trans>Following</Trans>
|
||||
)}
|
||||
</ButtonText>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
btnOuter: {
|
||||
marginLeft: 'auto',
|
||||
},
|
||||
btn: {
|
||||
flexDirection: 'row',
|
||||
borderRadius: 50,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 14,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -14,14 +14,12 @@ import {useLingui} from '@lingui/react'
|
||||
|
||||
import {MAX_POST_LINES} from '#/lib/constants'
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {makeProfileLink} from '#/lib/routes/links'
|
||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {countLines} from '#/lib/strings/helpers'
|
||||
import {niceDate} from '#/lib/strings/time'
|
||||
import {s} from '#/lib/styles'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow'
|
||||
import {useLanguagePrefs} from '#/state/preferences'
|
||||
import {useOpenLink} from '#/state/preferences/in-app-browser'
|
||||
@@ -30,9 +28,10 @@ import {useSession} from '#/state/session'
|
||||
import {useComposerControls} from '#/state/shell/composer'
|
||||
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
|
||||
import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {AppModerationCause} from '#/components/Pills'
|
||||
import {RichText} from '#/components/RichText'
|
||||
import {Text as NewText} from '#/components/Typography'
|
||||
import {ContentHider} from '../../../components/moderation/ContentHider'
|
||||
import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe'
|
||||
import {PostAlerts} from '../../../components/moderation/PostAlerts'
|
||||
@@ -180,6 +179,7 @@ let PostThreadItemLoaded = ({
|
||||
hideTopBorder?: boolean
|
||||
threadgateRecord?: AppBskyFeedThreadgate.Record
|
||||
}): React.ReactNode => {
|
||||
const t = useTheme()
|
||||
const pal = usePalette('default')
|
||||
const {_, i18n} = useLingui()
|
||||
const langPrefs = useLanguagePrefs()
|
||||
@@ -268,8 +268,14 @@ let PostThreadItemLoaded = ({
|
||||
return (
|
||||
<>
|
||||
{rootUri !== post.uri && (
|
||||
<View style={{paddingLeft: 16, flexDirection: 'row', height: 16}}>
|
||||
<View style={{width: 38}}>
|
||||
<View
|
||||
style={[
|
||||
a.pl_lg,
|
||||
a.flex_row,
|
||||
a.pb_xs,
|
||||
{height: a.pt_lg.paddingTop},
|
||||
]}>
|
||||
<View style={{width: 42}}>
|
||||
<View
|
||||
style={[
|
||||
styles.replyLine,
|
||||
@@ -286,88 +292,74 @@ let PostThreadItemLoaded = ({
|
||||
<View
|
||||
testID={`postThreadItem-by-${post.author.handle}`}
|
||||
style={[
|
||||
styles.outer,
|
||||
styles.outerHighlighted,
|
||||
pal.border,
|
||||
pal.view,
|
||||
rootUri === post.uri && styles.outerHighlightedRoot,
|
||||
hideTopBorder && styles.noTopBorder,
|
||||
]}
|
||||
accessible={false}>
|
||||
<View style={[styles.layout]}>
|
||||
<View style={[styles.layoutAvi, {paddingBottom: 8}]}>
|
||||
<PreviewableUserAvatar
|
||||
size={42}
|
||||
profile={post.author}
|
||||
moderation={moderation.ui('avatar')}
|
||||
type={post.author.associated?.labeler ? 'labeler' : 'user'}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.layoutContent}>
|
||||
<View
|
||||
style={[styles.meta, styles.metaExpandedLine1, {zIndex: 1}]}>
|
||||
<Link style={s.flex1} href={authorHref} title={authorTitle}>
|
||||
<Text
|
||||
emoji
|
||||
type="xl-bold"
|
||||
style={[pal.text, a.self_start]}
|
||||
numberOfLines={1}
|
||||
lineHeight={1.2}>
|
||||
{sanitizeDisplayName(
|
||||
post.author.displayName ||
|
||||
sanitizeHandle(post.author.handle),
|
||||
moderation.ui('displayName'),
|
||||
)}
|
||||
</Text>
|
||||
</Link>
|
||||
</View>
|
||||
<View style={styles.meta}>
|
||||
<Link style={s.flex1} href={authorHref} title={authorTitle}>
|
||||
<Text
|
||||
emoji
|
||||
type="md"
|
||||
style={[pal.textLight]}
|
||||
numberOfLines={1}>
|
||||
{sanitizeHandle(post.author.handle, '@')}
|
||||
</Text>
|
||||
</Link>
|
||||
</View>
|
||||
a.px_lg,
|
||||
t.atoms.border_contrast_low,
|
||||
// root post styles
|
||||
rootUri === post.uri && [a.pt_lg],
|
||||
]}>
|
||||
<View style={[a.flex_row, a.gap_md, a.pb_md]}>
|
||||
<PreviewableUserAvatar
|
||||
size={42}
|
||||
profile={post.author}
|
||||
moderation={moderation.ui('avatar')}
|
||||
type={post.author.associated?.labeler ? 'labeler' : 'user'}
|
||||
/>
|
||||
<View style={[a.flex_1]}>
|
||||
<Link style={s.flex1} href={authorHref} title={authorTitle}>
|
||||
<NewText
|
||||
emoji
|
||||
style={[a.text_lg, a.font_bold, a.leading_snug, a.self_start]}
|
||||
numberOfLines={1}>
|
||||
{sanitizeDisplayName(
|
||||
post.author.displayName ||
|
||||
sanitizeHandle(post.author.handle),
|
||||
moderation.ui('displayName'),
|
||||
)}
|
||||
</NewText>
|
||||
</Link>
|
||||
<Link style={s.flex1} href={authorHref} title={authorTitle}>
|
||||
<NewText
|
||||
emoji
|
||||
style={[
|
||||
a.text_md,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}
|
||||
numberOfLines={1}>
|
||||
{sanitizeHandle(post.author.handle, '@')}
|
||||
</NewText>
|
||||
</Link>
|
||||
</View>
|
||||
{currentAccount?.did !== post.author.did && (
|
||||
<PostThreadFollowBtn did={post.author.did} />
|
||||
<View>
|
||||
<PostThreadFollowBtn did={post.author.did} />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<View style={[s.pl10, s.pr10, s.pb10]}>
|
||||
<LabelsOnMyPost post={post} />
|
||||
<View style={[a.pb_sm]}>
|
||||
<LabelsOnMyPost post={post} style={[a.pb_sm]} />
|
||||
<ContentHider
|
||||
modui={moderation.ui('contentView')}
|
||||
ignoreMute
|
||||
style={styles.contentHider}
|
||||
childContainerStyle={styles.contentHiderChild}>
|
||||
childContainerStyle={[a.pt_sm]}>
|
||||
<PostAlerts
|
||||
modui={moderation.ui('contentView')}
|
||||
size="lg"
|
||||
includeMute
|
||||
style={[a.pt_2xs, a.pb_sm]}
|
||||
style={[a.pb_sm]}
|
||||
additionalCauses={additionalPostAlerts}
|
||||
/>
|
||||
{richText?.text ? (
|
||||
<View
|
||||
style={[
|
||||
styles.postTextContainer,
|
||||
styles.postTextLargeContainer,
|
||||
]}>
|
||||
<RichText
|
||||
enableTags
|
||||
selectable
|
||||
value={richText}
|
||||
style={[a.flex_1, a.text_xl]}
|
||||
authorHandle={post.author.handle}
|
||||
/>
|
||||
</View>
|
||||
<RichText
|
||||
enableTags
|
||||
selectable
|
||||
value={richText}
|
||||
style={[a.flex_1, a.text_xl]}
|
||||
authorHandle={post.author.handle}
|
||||
/>
|
||||
) : undefined}
|
||||
{post.embed && (
|
||||
<View style={[a.pb_sm]}>
|
||||
<View style={[a.py_xs]}>
|
||||
<PostEmbeds
|
||||
embed={post.embed}
|
||||
moderation={moderation}
|
||||
@@ -386,68 +378,73 @@ let PostThreadItemLoaded = ({
|
||||
post.likeCount !== 0 ||
|
||||
post.quoteCount !== 0 ? (
|
||||
// Show this section unless we're *sure* it has no engagement.
|
||||
<View style={[styles.expandedInfo, pal.border]}>
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.gap_lg,
|
||||
a.border_t,
|
||||
a.border_b,
|
||||
a.mt_md,
|
||||
a.py_md,
|
||||
t.atoms.border_contrast_low,
|
||||
]}>
|
||||
{post.repostCount != null && post.repostCount !== 0 ? (
|
||||
<Link
|
||||
style={styles.expandedInfoItem}
|
||||
href={repostsHref}
|
||||
title={repostsTitle}>
|
||||
<Text
|
||||
<Link href={repostsHref} title={repostsTitle}>
|
||||
<NewText
|
||||
testID="repostCount-expanded"
|
||||
type="lg"
|
||||
style={pal.textLight}>
|
||||
<Text type="xl-bold" style={pal.text}>
|
||||
style={[a.text_md, t.atoms.text_contrast_medium]}>
|
||||
<NewText style={[a.text_md, a.font_bold, t.atoms.text]}>
|
||||
{formatCount(i18n, post.repostCount)}
|
||||
</Text>{' '}
|
||||
</NewText>{' '}
|
||||
<Plural
|
||||
value={post.repostCount}
|
||||
one="repost"
|
||||
other="reposts"
|
||||
/>
|
||||
</Text>
|
||||
</NewText>
|
||||
</Link>
|
||||
) : null}
|
||||
{post.quoteCount != null &&
|
||||
post.quoteCount !== 0 &&
|
||||
!post.viewer?.embeddingDisabled ? (
|
||||
<Link
|
||||
style={styles.expandedInfoItem}
|
||||
href={quotesHref}
|
||||
title={quotesTitle}>
|
||||
<Text
|
||||
<Link href={quotesHref} title={quotesTitle}>
|
||||
<NewText
|
||||
testID="quoteCount-expanded"
|
||||
type="lg"
|
||||
style={pal.textLight}>
|
||||
<Text type="xl-bold" style={pal.text}>
|
||||
style={[a.text_md, t.atoms.text_contrast_medium]}>
|
||||
<NewText style={[a.text_md, a.font_bold, t.atoms.text]}>
|
||||
{formatCount(i18n, post.quoteCount)}
|
||||
</Text>{' '}
|
||||
</NewText>{' '}
|
||||
<Plural
|
||||
value={post.quoteCount}
|
||||
one="quote"
|
||||
other="quotes"
|
||||
/>
|
||||
</Text>
|
||||
</NewText>
|
||||
</Link>
|
||||
) : null}
|
||||
{post.likeCount != null && post.likeCount !== 0 ? (
|
||||
<Link
|
||||
style={styles.expandedInfoItem}
|
||||
href={likesHref}
|
||||
title={likesTitle}>
|
||||
<Text
|
||||
<Link href={likesHref} title={likesTitle}>
|
||||
<NewText
|
||||
testID="likeCount-expanded"
|
||||
type="lg"
|
||||
style={pal.textLight}>
|
||||
<Text type="xl-bold" style={pal.text}>
|
||||
style={[a.text_md, t.atoms.text_contrast_medium]}>
|
||||
<NewText style={[a.text_md, a.font_bold, t.atoms.text]}>
|
||||
{formatCount(i18n, post.likeCount)}
|
||||
</Text>{' '}
|
||||
</NewText>{' '}
|
||||
<Plural value={post.likeCount} one="like" other="likes" />
|
||||
</Text>
|
||||
</NewText>
|
||||
</Link>
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
<View style={[s.pl10, s.pr10]}>
|
||||
<View
|
||||
style={[
|
||||
a.pt_sm,
|
||||
a.pb_2xs,
|
||||
{
|
||||
marginLeft: -5,
|
||||
},
|
||||
]}>
|
||||
<PostCtrls
|
||||
big
|
||||
post={post}
|
||||
@@ -481,9 +478,8 @@ let PostThreadItemLoaded = ({
|
||||
testID={`postThreadItem-by-${post.author.handle}`}
|
||||
href={postHref}
|
||||
disabled={overrideBlur}
|
||||
style={[pal.view]}
|
||||
modui={moderation.ui('contentList')}
|
||||
iconSize={isThreadedChild ? 26 : 38}
|
||||
iconSize={isThreadedChild ? 24 : 42}
|
||||
iconStyles={
|
||||
isThreadedChild ? {marginRight: 4} : {marginLeft: 2, marginRight: 2}
|
||||
}
|
||||
@@ -496,7 +492,7 @@ let PostThreadItemLoaded = ({
|
||||
paddingLeft: 8,
|
||||
height: isThreadedChildAdjacentTop ? 8 : 16,
|
||||
}}>
|
||||
<View style={{width: 38}}>
|
||||
<View style={{width: 42}}>
|
||||
{!isThreadedChild && showParentReplyLine && (
|
||||
<View
|
||||
style={[
|
||||
@@ -514,7 +510,9 @@ let PostThreadItemLoaded = ({
|
||||
|
||||
<View
|
||||
style={[
|
||||
styles.layout,
|
||||
a.flex_row,
|
||||
a.px_sm,
|
||||
a.gap_md,
|
||||
{
|
||||
paddingBottom:
|
||||
showChildReplyLine && !isThreadedChild
|
||||
@@ -526,9 +524,9 @@ let PostThreadItemLoaded = ({
|
||||
]}>
|
||||
{/* If we are in threaded mode, the avatar is rendered in PostMeta */}
|
||||
{!isThreadedChild && (
|
||||
<View style={styles.layoutAvi}>
|
||||
<View>
|
||||
<PreviewableUserAvatar
|
||||
size={38}
|
||||
size={42}
|
||||
profile={post.author}
|
||||
moderation={moderation.ui('avatar')}
|
||||
type={post.author.associated?.labeler ? 'labeler' : 'user'}
|
||||
@@ -549,12 +547,7 @@ let PostThreadItemLoaded = ({
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View
|
||||
style={
|
||||
isThreadedChild
|
||||
? styles.layoutContentThreaded
|
||||
: styles.layoutContent
|
||||
}>
|
||||
<View style={[a.flex_1]}>
|
||||
<PostMeta
|
||||
author={post.author}
|
||||
moderation={moderation}
|
||||
@@ -563,20 +556,16 @@ let PostThreadItemLoaded = ({
|
||||
showAvatar={isThreadedChild}
|
||||
avatarModeration={moderation.ui('avatar')}
|
||||
avatarSize={24}
|
||||
style={
|
||||
isThreadedChild && {
|
||||
paddingBottom: isWeb ? 5 : 4,
|
||||
}
|
||||
}
|
||||
style={[a.pb_xs]}
|
||||
/>
|
||||
<LabelsOnMyPost post={post} />
|
||||
<LabelsOnMyPost post={post} style={[a.pb_xs]} />
|
||||
<PostAlerts
|
||||
modui={moderation.ui('contentList')}
|
||||
style={[a.pt_2xs, a.pb_2xs]}
|
||||
style={[a.pb_2xs]}
|
||||
additionalCauses={additionalPostAlerts}
|
||||
/>
|
||||
{richText?.text ? (
|
||||
<View style={styles.postTextContainer}>
|
||||
<View style={[a.pb_2xs, a.pr_sm]}>
|
||||
<RichText
|
||||
enableTags
|
||||
value={richText}
|
||||
@@ -659,29 +648,31 @@ function PostOuterWrapper({
|
||||
hasPrecedingItem: boolean
|
||||
hideTopBorder?: boolean
|
||||
}>) {
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const pal = usePalette('default')
|
||||
const t = useTheme()
|
||||
if (treeView && depth > 0) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
pal.border,
|
||||
a.flex_row,
|
||||
a.px_sm,
|
||||
t.atoms.border_contrast_low,
|
||||
styles.cursor,
|
||||
{
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: isMobile ? 10 : 6,
|
||||
borderTopWidth: depth === 1 ? StyleSheet.hairlineWidth : 0,
|
||||
borderTopWidth: depth === 1 ? a.border_t.borderTopWidth : 0,
|
||||
},
|
||||
]}>
|
||||
{Array.from(Array(depth - 1)).map((_, n: number) => (
|
||||
<View
|
||||
key={`${post.uri}-padding-${n}`}
|
||||
style={{
|
||||
borderLeftWidth: 2,
|
||||
borderLeftColor: pal.colors.border,
|
||||
marginLeft: isMobile ? 6 : 12,
|
||||
paddingLeft: isMobile ? 6 : 8,
|
||||
}}
|
||||
style={[
|
||||
a.ml_sm,
|
||||
t.atoms.border_contrast_low,
|
||||
{
|
||||
borderLeftWidth: 2,
|
||||
paddingLeft: a.pl_sm.paddingLeft - 2, // minus border
|
||||
},
|
||||
]}
|
||||
/>
|
||||
))}
|
||||
<View style={{flex: 1}}>{children}</View>
|
||||
@@ -691,8 +682,9 @@ function PostOuterWrapper({
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.outer,
|
||||
pal.border,
|
||||
a.border_t,
|
||||
a.px_sm,
|
||||
t.atoms.border_contrast_low,
|
||||
showParentReplyLine && hasPrecedingItem && styles.noTopBorder,
|
||||
hideTopBorder && styles.noTopBorder,
|
||||
styles.cursor,
|
||||
@@ -713,6 +705,7 @@ function ExpandedPostDetails({
|
||||
needsTranslation: boolean
|
||||
translatorUrl: string
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const pal = usePalette('default')
|
||||
const {_, i18n} = useLingui()
|
||||
const openLink = useOpenLink()
|
||||
@@ -723,31 +716,25 @@ function ExpandedPostDetails({
|
||||
}, [openLink, translatorUrl])
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.flex_wrap,
|
||||
a.gap_xs,
|
||||
s.mt2,
|
||||
s.mb10,
|
||||
]}>
|
||||
<Text style={[a.text_sm, pal.textLight]}>
|
||||
<View style={[a.flex_row, a.align_center, a.flex_wrap, a.gap_sm, a.pt_md]}>
|
||||
<NewText style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
||||
{niceDate(i18n, post.indexedAt)}
|
||||
</Text>
|
||||
</NewText>
|
||||
{isRootPost && (
|
||||
<WhoCanReply post={post} isThreadAuthor={isThreadAuthor} />
|
||||
)}
|
||||
{needsTranslation && (
|
||||
<>
|
||||
<Text style={[a.text_sm, pal.textLight]}>·</Text>
|
||||
<NewText style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
||||
·
|
||||
</NewText>
|
||||
|
||||
<Text
|
||||
<NewText
|
||||
style={[a.text_sm, pal.link]}
|
||||
title={_(msg`Translate`)}
|
||||
onPress={onTranslatePress}>
|
||||
<Trans>Translate</Trans>
|
||||
</Text>
|
||||
</NewText>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
@@ -773,31 +760,9 @@ const styles = StyleSheet.create({
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
paddingLeft: 8,
|
||||
},
|
||||
outerHighlighted: {
|
||||
borderTopWidth: 0,
|
||||
paddingTop: 4,
|
||||
paddingLeft: 8,
|
||||
paddingRight: 8,
|
||||
},
|
||||
outerHighlightedRoot: {
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
paddingTop: 16,
|
||||
},
|
||||
noTopBorder: {
|
||||
borderTopWidth: 0,
|
||||
},
|
||||
layout: {
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: 8,
|
||||
},
|
||||
layoutAvi: {},
|
||||
layoutContent: {
|
||||
flex: 1,
|
||||
marginLeft: 10,
|
||||
},
|
||||
layoutContentThreaded: {
|
||||
flex: 1,
|
||||
},
|
||||
meta: {
|
||||
flexDirection: 'row',
|
||||
paddingVertical: 2,
|
||||
@@ -805,42 +770,6 @@ const styles = StyleSheet.create({
|
||||
metaExpandedLine1: {
|
||||
paddingVertical: 0,
|
||||
},
|
||||
alert: {
|
||||
marginBottom: 6,
|
||||
},
|
||||
postTextContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
paddingBottom: 4,
|
||||
paddingRight: 10,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
postTextLargeContainer: {
|
||||
paddingHorizontal: 0,
|
||||
paddingRight: 0,
|
||||
paddingBottom: 10,
|
||||
},
|
||||
translateLink: {
|
||||
marginBottom: 6,
|
||||
},
|
||||
contentHider: {
|
||||
marginBottom: 6,
|
||||
},
|
||||
contentHiderChild: {
|
||||
marginTop: 6,
|
||||
},
|
||||
expandedInfo: {
|
||||
flexDirection: 'row',
|
||||
padding: 10,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
marginTop: 5,
|
||||
marginBottom: 10,
|
||||
},
|
||||
expandedInfoItem: {
|
||||
marginRight: 10,
|
||||
},
|
||||
loadMore: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -20,6 +20,7 @@ import {isAndroid, isNative, isWeb} from '#/platform/detection'
|
||||
import {precacheProfile} from '#/state/queries/profile'
|
||||
import {HighPriorityImage} from '#/view/com/util/images/Image'
|
||||
import {tokens, useTheme} from '#/alf'
|
||||
import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
|
||||
import {
|
||||
Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled,
|
||||
Camera_Stroke2_Corner0_Rounded as Camera,
|
||||
@@ -271,6 +272,7 @@ let EditableUserAvatar = ({
|
||||
const {_} = useLingui()
|
||||
const {requestCameraAccessIfNeeded} = useCameraPermission()
|
||||
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
|
||||
const sheetWrapper = useSheetWrapper()
|
||||
|
||||
const aviStyle = useMemo(() => {
|
||||
if (type === 'algo' || type === 'list') {
|
||||
@@ -306,9 +308,11 @@ let EditableUserAvatar = ({
|
||||
return
|
||||
}
|
||||
|
||||
const items = await openPicker({
|
||||
aspect: [1, 1],
|
||||
})
|
||||
const items = await sheetWrapper(
|
||||
openPicker({
|
||||
aspect: [1, 1],
|
||||
}),
|
||||
)
|
||||
const item = items[0]
|
||||
if (!item) {
|
||||
return
|
||||
@@ -332,7 +336,7 @@ let EditableUserAvatar = ({
|
||||
logger.error('Failed to crop banner', {error: e})
|
||||
}
|
||||
}
|
||||
}, [onSelectNewAvatar, requestPhotoAccessIfNeeded])
|
||||
}, [onSelectNewAvatar, requestPhotoAccessIfNeeded, sheetWrapper])
|
||||
|
||||
const onRemoveAvatar = React.useCallback(() => {
|
||||
onSelectNewAvatar(null)
|
||||
|
||||
@@ -17,6 +17,7 @@ import {logger} from '#/logger'
|
||||
import {isAndroid, isNative} from '#/platform/detection'
|
||||
import {EventStopper} from '#/view/com/util/EventStopper'
|
||||
import {tokens, useTheme as useAlfTheme} from '#/alf'
|
||||
import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
|
||||
import {
|
||||
Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled,
|
||||
Camera_Stroke2_Corner0_Rounded as Camera,
|
||||
@@ -43,6 +44,7 @@ export function UserBanner({
|
||||
const {_} = useLingui()
|
||||
const {requestCameraAccessIfNeeded} = useCameraPermission()
|
||||
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
|
||||
const sheetWrapper = useSheetWrapper()
|
||||
|
||||
const onOpenCamera = React.useCallback(async () => {
|
||||
if (!(await requestCameraAccessIfNeeded())) {
|
||||
@@ -60,7 +62,7 @@ export function UserBanner({
|
||||
if (!(await requestPhotoAccessIfNeeded())) {
|
||||
return
|
||||
}
|
||||
const items = await openPicker()
|
||||
const items = await sheetWrapper(openPicker())
|
||||
if (!items[0]) {
|
||||
return
|
||||
}
|
||||
@@ -80,7 +82,7 @@ export function UserBanner({
|
||||
logger.error('Failed to crop banner', {error: e})
|
||||
}
|
||||
}
|
||||
}, [onSelectNewBanner, requestPhotoAccessIfNeeded])
|
||||
}, [onSelectNewBanner, requestPhotoAccessIfNeeded, sheetWrapper])
|
||||
|
||||
const onRemoveBanner = React.useCallback(() => {
|
||||
onSelectNewBanner?.(null)
|
||||
|
||||
@@ -100,7 +100,7 @@ export function ViewHeader({
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
<View style={styles.titleContainer} pointerEvents="none">
|
||||
<Text type="title" style={[pal.text, styles.title]}>
|
||||
<Text emoji type="title" style={[pal.text, styles.title]}>
|
||||
{title}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
@@ -240,8 +240,8 @@ let PostDropdownBtn = ({
|
||||
Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
|
||||
}, [_, richText])
|
||||
|
||||
const onPressTranslate = React.useCallback(() => {
|
||||
openLink(translatorUrl)
|
||||
const onPressTranslate = React.useCallback(async () => {
|
||||
await openLink(translatorUrl)
|
||||
}, [openLink, translatorUrl])
|
||||
|
||||
const onHidePost = React.useCallback(() => {
|
||||
@@ -439,7 +439,7 @@ let PostDropdownBtn = ({
|
||||
<Menu.Item
|
||||
testID="postDropdownSendViaDMBtn"
|
||||
label={_(msg`Send via direct message`)}
|
||||
onPress={sendViaChatControl.open}>
|
||||
onPress={() => sendViaChatControl.open()}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Send via direct message</Trans>
|
||||
</Menu.ItemText>
|
||||
@@ -467,7 +467,7 @@ let PostDropdownBtn = ({
|
||||
<Menu.Item
|
||||
testID="postDropdownEmbedBtn"
|
||||
label={_(msg`Embed post`)}
|
||||
onPress={embedPostControl.open}>
|
||||
onPress={() => embedPostControl.open()}>
|
||||
<Menu.ItemText>{_(msg`Embed post`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={CodeBrackets} position="right" />
|
||||
</Menu.Item>
|
||||
@@ -542,7 +542,7 @@ let PostDropdownBtn = ({
|
||||
? _(msg`Hide reply for me`)
|
||||
: _(msg`Hide post for me`)
|
||||
}
|
||||
onPress={hidePromptControl.open}>
|
||||
onPress={() => hidePromptControl.open()}>
|
||||
<Menu.ItemText>
|
||||
{isReply
|
||||
? _(msg`Hide reply for me`)
|
||||
@@ -630,7 +630,9 @@ let PostDropdownBtn = ({
|
||||
<Menu.Item
|
||||
testID="postDropdownEditPostInteractions"
|
||||
label={_(msg`Edit interaction settings`)}
|
||||
onPress={postInteractionSettingsDialogControl.open}
|
||||
onPress={() =>
|
||||
postInteractionSettingsDialogControl.open()
|
||||
}
|
||||
{...(isAuthor
|
||||
? Platform.select({
|
||||
web: {
|
||||
@@ -649,7 +651,7 @@ let PostDropdownBtn = ({
|
||||
<Menu.Item
|
||||
testID="postDropdownDeleteBtn"
|
||||
label={_(msg`Delete post`)}
|
||||
onPress={deletePromptControl.open}>
|
||||
onPress={() => deletePromptControl.open()}>
|
||||
<Menu.ItemText>{_(msg`Delete post`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Trash} position="right" />
|
||||
</Menu.Item>
|
||||
|
||||
@@ -86,7 +86,9 @@ let RepostButton = ({
|
||||
</Text>
|
||||
) : undefined}
|
||||
</Button>
|
||||
<Dialog.Outer control={dialogControl}>
|
||||
<Dialog.Outer
|
||||
control={dialogControl}
|
||||
nativeOptions={{preventExpansion: true}}>
|
||||
<Dialog.Handle />
|
||||
<Dialog.Inner label={_(msg`Repost or quote post`)}>
|
||||
<View style={a.gap_xl}>
|
||||
@@ -155,7 +157,6 @@ let RepostButton = ({
|
||||
</View>
|
||||
<Button
|
||||
label={_(msg`Cancel quote post`)}
|
||||
onAccessibilityEscape={close}
|
||||
onPress={close}
|
||||
size="large"
|
||||
variant="solid"
|
||||
|
||||
@@ -77,13 +77,16 @@ export function Text({
|
||||
flattened.fontSize = flattened.fontSize * fonts.scaleMultiplier
|
||||
}
|
||||
|
||||
const shared = {
|
||||
uiTextView: true,
|
||||
selectable,
|
||||
style: flattened,
|
||||
...props,
|
||||
}
|
||||
|
||||
return (
|
||||
<UITextView
|
||||
style={flattened}
|
||||
selectable={selectable}
|
||||
uiTextView
|
||||
{...props}>
|
||||
{isIOS && emoji ? renderChildrenWithEmoji(children) : children}
|
||||
<UITextView {...shared}>
|
||||
{isIOS && emoji ? renderChildrenWithEmoji(children, shared) : children}
|
||||
</UITextView>
|
||||
)
|
||||
}
|
||||
@@ -104,14 +107,16 @@ export function Text({
|
||||
flattened.fontSize = flattened.fontSize * fonts.scaleMultiplier
|
||||
}
|
||||
|
||||
const shared = {
|
||||
selectable,
|
||||
style: flattened,
|
||||
dataSet: Object.assign({tooltip: title}, dataSet || {}),
|
||||
...props,
|
||||
}
|
||||
|
||||
return (
|
||||
<RNText
|
||||
style={flattened}
|
||||
// @ts-ignore web only -esb
|
||||
dataSet={Object.assign({tooltip: title}, dataSet || {})}
|
||||
selectable={selectable}
|
||||
{...props}>
|
||||
{isIOS && emoji ? renderChildrenWithEmoji(children) : children}
|
||||
<RNText {...shared}>
|
||||
{isIOS && emoji ? renderChildrenWithEmoji(children, shared) : children}
|
||||
</RNText>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -79,7 +79,6 @@ export function DisableEmail2FADialog({
|
||||
return (
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.ScrollableInner
|
||||
accessibilityDescribedBy="dialog-description"
|
||||
accessibilityLabelledBy="dialog-title">
|
||||
|
||||
@@ -53,7 +53,6 @@ export function ExportCarDialog({
|
||||
return (
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.ScrollableInner
|
||||
accessibilityDescribedBy="dialog-description"
|
||||
accessibilityLabelledBy="dialog-title">
|
||||
|
||||
@@ -2,8 +2,8 @@ import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
import {useDialogStateControlContext} from '#/state/dialogs'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
@@ -179,19 +179,13 @@ export function Dialogs() {
|
||||
</Prompt.Outer>
|
||||
|
||||
<Dialog.Outer control={basic}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.Inner label="test">
|
||||
<H3 nativeID="dialog-title">Dialog</H3>
|
||||
<P nativeID="dialog-description">A basic dialog</P>
|
||||
</Dialog.Inner>
|
||||
</Dialog.Outer>
|
||||
|
||||
<Dialog.Outer
|
||||
control={scrollable}
|
||||
nativeOptions={{sheet: {snapPoints: ['100%']}}}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.Outer control={scrollable}>
|
||||
<Dialog.ScrollableInner
|
||||
accessibilityDescribedBy="dialog-description"
|
||||
accessibilityLabelledBy="dialog-title">
|
||||
@@ -230,8 +224,6 @@ export function Dialogs() {
|
||||
</Dialog.Outer>
|
||||
|
||||
<Dialog.Outer control={testDialog}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.ScrollableInner
|
||||
accessibilityDescribedBy="dialog-description"
|
||||
accessibilityLabelledBy="dialog-title">
|
||||
@@ -356,8 +348,6 @@ export function Dialogs() {
|
||||
|
||||
{shouldRenderUnmountTest && (
|
||||
<Dialog.Outer control={unmountTestDialog}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.Inner label="test">
|
||||
<H3 nativeID="dialog-title">Unmount Test Dialog</H3>
|
||||
<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 {StatusBar} from 'expo-status-bar'
|
||||
import * as SystemUI from 'expo-system-ui'
|
||||
|
||||
import {useDialogStateControlContext} from '#/state/dialogs'
|
||||
import {useComposerState} from '#/state/shell/composer'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {getBackgroundColor, useThemeName} from '#/alf/util/useColorModeTheme'
|
||||
import {ComposePost, useComposerCancelRef} from '../com/composer/Composer'
|
||||
|
||||
export function Composer({}: {winHeight: number}) {
|
||||
const {setFullyExpandedCount} = useDialogStateControlContext()
|
||||
const t = useTheme()
|
||||
const state = useComposerState()
|
||||
const ref = useComposerCancelRef()
|
||||
|
||||
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 (
|
||||
<Modal
|
||||
@@ -24,56 +33,18 @@ export function Composer({}: {winHeight: number}) {
|
||||
animationType="slide"
|
||||
onRequestClose={() => ref.current?.onPressCancel()}>
|
||||
<View style={[t.atoms.bg, a.flex_1]}>
|
||||
<Providers open={open}>
|
||||
<ComposePost
|
||||
cancelRef={ref}
|
||||
replyTo={state?.replyTo}
|
||||
onPost={state?.onPost}
|
||||
quote={state?.quote}
|
||||
quoteCount={state?.quoteCount}
|
||||
mention={state?.mention}
|
||||
text={state?.text}
|
||||
imageUris={state?.imageUris}
|
||||
videoUri={state?.videoUri}
|
||||
/>
|
||||
</Providers>
|
||||
<ComposePost
|
||||
cancelRef={ref}
|
||||
replyTo={state?.replyTo}
|
||||
onPost={state?.onPost}
|
||||
quote={state?.quote}
|
||||
quoteCount={state?.quoteCount}
|
||||
mention={state?.mention}
|
||||
text={state?.text}
|
||||
imageUris={state?.imageUris}
|
||||
videoUri={state?.videoUri}
|
||||
/>
|
||||
</View>
|
||||
</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 {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 {
|
||||
useIsDrawerOpen,
|
||||
@@ -20,17 +28,9 @@ import {
|
||||
useSetDrawerOpen,
|
||||
} from '#/state/shell'
|
||||
import {useCloseAnyActiveElement} from '#/state/util'
|
||||
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} 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 {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 {SigninDialog} from '#/components/dialogs/Signin'
|
||||
import {Outlet as PortalOutlet} from '#/components/Portal'
|
||||
@@ -61,7 +61,6 @@ function ShellInner() {
|
||||
const canGoBack = useNavigationState(state => !isStateAtTabRoot(state))
|
||||
const {hasSession} = useSession()
|
||||
const closeAnyActiveElement = useCloseAnyActiveElement()
|
||||
const {importantForAccessibility} = useDialogStateContext()
|
||||
|
||||
useNotificationsRegistration()
|
||||
useNotificationsHandler()
|
||||
@@ -101,9 +100,7 @@ function ShellInner() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Animated.View
|
||||
style={containerPadding}
|
||||
importantForAccessibility={importantForAccessibility}>
|
||||
<Animated.View style={containerPadding}>
|
||||
<ErrorBoundary>
|
||||
<Drawer
|
||||
renderDrawerContent={renderDrawerContent}
|
||||
@@ -127,6 +124,7 @@ function ShellInner() {
|
||||
}
|
||||
|
||||
export const Shell: React.FC = function ShellImpl() {
|
||||
const {fullyExpandedCount} = useDialogStateControlContext()
|
||||
const pal = usePalette('default')
|
||||
const theme = useTheme()
|
||||
React.useEffect(() => {
|
||||
@@ -140,7 +138,14 @@ export const Shell: React.FC = function ShellImpl() {
|
||||
}, [theme])
|
||||
return (
|
||||
<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>
|
||||
<ShellInner />
|
||||
</RoutesContainer>
|
||||
|
||||
@@ -29,6 +29,20 @@
|
||||
*
|
||||
* THIS NEEDS TO BE DUPLICATED IN `bskyweb/templates/base.html`
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'InterVariable';
|
||||
src: url(/static/media/InterVariable.c9f788f6e7ebaec75d7c.ttf) format('truetype');
|
||||
font-weight: 300 1000;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'InterVariableItalic';
|
||||
src: url(/static/media/InterVariable-Italic.55d6a3f35e9b605ba6f4.ttf) format('truetype');
|
||||
font-weight: 300 1000;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
html {
|
||||
background-color: white;
|
||||
scrollbar-gutter: stable both-edges;
|
||||
|
||||
@@ -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"
|
||||
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:
|
||||
version "11.8.1"
|
||||
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:
|
||||
use-latest-callback "^0.1.9"
|
||||
|
||||
react-native-gesture-handler@~2.16.2:
|
||||
version "2.16.2"
|
||||
resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.16.2.tgz#032bd2a07334292d7f6cff1dc9d1ec928f72e26d"
|
||||
integrity sha512-vGFlrDKlmyI+BT+FemqVxmvO7nqxU33cgXVsn6IKAFishvlG3oV2Ds67D5nPkHMea8T+s1IcuMm0bF8ntZtAyg==
|
||||
react-native-gesture-handler@2.20.0:
|
||||
version "2.20.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.20.0.tgz#2d9ec4e9bd22619ebe36269dda3ecb1173928276"
|
||||
integrity sha512-rFKqgHRfxQ7uSAivk8vxCiW4SB3G0U7jnv7kZD4Y90K5kp6YrU8Q3tWhxe3Rx55BIvSd3mBe9ZWbWVJ0FsSHPA==
|
||||
dependencies:
|
||||
"@egjs/hammerjs" "^2.0.17"
|
||||
hoist-non-react-statics "^3.3.0"
|
||||
invariant "^2.2.4"
|
||||
lodash "^4.17.21"
|
||||
prop-types "^15.7.2"
|
||||
|
||||
react-native-get-random-values@^1.6.0:
|
||||
@@ -18094,10 +18085,10 @@ react-native-ios-context-menu@^1.15.3:
|
||||
dependencies:
|
||||
"@dominicstop/ts-event-emitter" "^1.1.0"
|
||||
|
||||
react-native-keyboard-controller@^1.12.1:
|
||||
version "1.12.1"
|
||||
resolved "https://registry.yarnpkg.com/react-native-keyboard-controller/-/react-native-keyboard-controller-1.12.1.tgz#6de22ed4d060528a0dd25621eeaa7f71772ce35f"
|
||||
integrity sha512-2OpQcesiYsMilrTzgcTafSGexd9UryRQRuHudIcOn0YaqvvzNpnhVZMVuJMH93fJv/iaZYp3138rgUKOdHhtSw==
|
||||
react-native-keyboard-controller@^1.14.0:
|
||||
version "1.14.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native-keyboard-controller/-/react-native-keyboard-controller-1.14.0.tgz#f6faaa12b3736a10f4eec4236ed5b0343508b9a1"
|
||||
integrity sha512-JW9k2fehFXOpvLWh1YcgyubLodg/HPi6bR11sCZB/BOawf1tnbGnqk967B8XkxDOKHH6mg+z82quCvv8ALh1rg==
|
||||
|
||||
react-native-mmkv@^2.12.2:
|
||||
version "2.12.2"
|
||||
|
||||
Reference in New Issue
Block a user