my fork of the bluesky client

[Sheets] [Pt. 1] Root PR (#5557)

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
Co-authored-by: Eric Bailey <git@esb.lol>
Co-authored-by: dan <dan.abramov@gmail.com>
Co-authored-by: Hailey <me@haileyok.com>

Changed files
+2459 -1527
jest
modules
src
+5
jest/jestSetup.js
··· 96 getIsReducedMotionEnabled: () => false, 97 } 98 } 99 }), 100 requireNativeViewManager: jest.fn().mockImplementation(moduleName => { 101 return () => null
··· 96 getIsReducedMotionEnabled: () => false, 97 } 98 } 99 + if (moduleName === 'BottomSheet') { 100 + return { 101 + dismissAll: () => {}, 102 + } 103 + } 104 }), 105 requireNativeViewManager: jest.fn().mockImplementation(moduleName => { 106 return () => null
+49
modules/bottom-sheet/android/build.gradle
···
··· 1 + apply plugin: 'com.android.library' 2 + 3 + group = 'expo.modules.bottomsheet' 4 + version = '0.1.0' 5 + 6 + def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle") 7 + apply from: expoModulesCorePlugin 8 + applyKotlinExpoModulesCorePlugin() 9 + useCoreDependencies() 10 + useExpoPublishing() 11 + 12 + // If you want to use the managed Android SDK versions from expo-modules-core, set this to true. 13 + // The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code. 14 + // Most of the time, you may like to manage the Android SDK versions yourself. 15 + def useManagedAndroidSdkVersions = false 16 + if (useManagedAndroidSdkVersions) { 17 + useDefaultAndroidSdkVersions() 18 + } else { 19 + buildscript { 20 + // Simple helper that allows the root project to override versions declared by this library. 21 + ext.safeExtGet = { prop, fallback -> 22 + rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback 23 + } 24 + } 25 + project.android { 26 + compileSdkVersion safeExtGet("compileSdkVersion", 34) 27 + defaultConfig { 28 + minSdkVersion safeExtGet("minSdkVersion", 21) 29 + targetSdkVersion safeExtGet("targetSdkVersion", 34) 30 + } 31 + } 32 + } 33 + 34 + android { 35 + namespace "expo.modules.bottomsheet" 36 + defaultConfig { 37 + versionCode 1 38 + versionName "0.1.0" 39 + } 40 + lintOptions { 41 + abortOnError false 42 + } 43 + } 44 + 45 + dependencies { 46 + implementation project(':expo-modules-core') 47 + implementation 'com.google.android.material:material:1.12.0' 48 + implementation "com.facebook.react:react-native:+" 49 + }
+2
modules/bottom-sheet/android/src/main/AndroidManifest.xml
···
··· 1 + <manifest> 2 + </manifest>
+53
modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetModule.kt
···
··· 1 + package expo.modules.bottomsheet 2 + 3 + import expo.modules.kotlin.modules.Module 4 + import expo.modules.kotlin.modules.ModuleDefinition 5 + 6 + class BottomSheetModule : Module() { 7 + override fun definition() = 8 + ModuleDefinition { 9 + Name("BottomSheet") 10 + 11 + AsyncFunction("dismissAll") { 12 + SheetManager.dismissAll() 13 + } 14 + 15 + View(BottomSheetView::class) { 16 + Events( 17 + arrayOf( 18 + "onAttemptDismiss", 19 + "onSnapPointChange", 20 + "onStateChange", 21 + ), 22 + ) 23 + 24 + AsyncFunction("dismiss") { view: BottomSheetView -> 25 + view.dismiss() 26 + } 27 + 28 + AsyncFunction("updateLayout") { view: BottomSheetView -> 29 + view.updateLayout() 30 + } 31 + 32 + Prop("disableDrag") { view: BottomSheetView, prop: Boolean -> 33 + view.disableDrag = prop 34 + } 35 + 36 + Prop("minHeight") { view: BottomSheetView, prop: Float -> 37 + view.minHeight = prop 38 + } 39 + 40 + Prop("maxHeight") { view: BottomSheetView, prop: Float -> 41 + view.maxHeight = prop 42 + } 43 + 44 + Prop("preventDismiss") { view: BottomSheetView, prop: Boolean -> 45 + view.preventDismiss = prop 46 + } 47 + 48 + Prop("preventExpansion") { view: BottomSheetView, prop: Boolean -> 49 + view.preventExpansion = prop 50 + } 51 + } 52 + } 53 + }
+339
modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetView.kt
···
··· 1 + package expo.modules.bottomsheet 2 + 3 + import android.content.Context 4 + import android.view.View 5 + import android.view.ViewGroup 6 + import android.view.ViewStructure 7 + import android.view.accessibility.AccessibilityEvent 8 + import android.widget.FrameLayout 9 + import androidx.core.view.allViews 10 + import com.facebook.react.bridge.LifecycleEventListener 11 + import com.facebook.react.bridge.ReactContext 12 + import com.facebook.react.bridge.UiThreadUtil 13 + import com.facebook.react.uimanager.UIManagerHelper 14 + import com.facebook.react.uimanager.events.EventDispatcher 15 + import com.google.android.material.bottomsheet.BottomSheetBehavior 16 + import com.google.android.material.bottomsheet.BottomSheetDialog 17 + import expo.modules.kotlin.AppContext 18 + import expo.modules.kotlin.viewevent.EventDispatcher 19 + import expo.modules.kotlin.views.ExpoView 20 + import java.util.ArrayList 21 + 22 + class BottomSheetView( 23 + context: Context, 24 + appContext: AppContext, 25 + ) : ExpoView(context, appContext), 26 + LifecycleEventListener { 27 + private var innerView: View? = null 28 + private var dialog: BottomSheetDialog? = null 29 + 30 + private lateinit var dialogRootViewGroup: DialogRootViewGroup 31 + private var eventDispatcher: EventDispatcher? = null 32 + 33 + private val screenHeight = 34 + context.resources.displayMetrics.heightPixels 35 + .toFloat() 36 + 37 + private val onAttemptDismiss by EventDispatcher() 38 + private val onSnapPointChange by EventDispatcher() 39 + private val onStateChange by EventDispatcher() 40 + 41 + // Props 42 + var disableDrag = false 43 + set (value) { 44 + field = value 45 + this.setDraggable(!value) 46 + } 47 + 48 + var preventDismiss = false 49 + set(value) { 50 + field = value 51 + this.dialog?.setCancelable(!value) 52 + } 53 + var preventExpansion = false 54 + 55 + var minHeight = 0f 56 + set(value) { 57 + field = 58 + if (value < 0) { 59 + 0f 60 + } else { 61 + value 62 + } 63 + } 64 + 65 + var maxHeight = this.screenHeight 66 + set(value) { 67 + field = 68 + if (value > this.screenHeight) { 69 + this.screenHeight.toFloat() 70 + } else { 71 + value 72 + } 73 + } 74 + 75 + private var isOpen: Boolean = false 76 + set(value) { 77 + field = value 78 + onStateChange( 79 + mapOf( 80 + "state" to if (value) "open" else "closed", 81 + ), 82 + ) 83 + } 84 + 85 + private var isOpening: Boolean = false 86 + set(value) { 87 + field = value 88 + if (value) { 89 + onStateChange( 90 + mapOf( 91 + "state" to "opening", 92 + ), 93 + ) 94 + } 95 + } 96 + 97 + private var isClosing: Boolean = false 98 + set(value) { 99 + field = value 100 + if (value) { 101 + onStateChange( 102 + mapOf( 103 + "state" to "closing", 104 + ), 105 + ) 106 + } 107 + } 108 + 109 + private var selectedSnapPoint = 0 110 + set(value) { 111 + if (field == value) return 112 + 113 + field = value 114 + onSnapPointChange( 115 + mapOf( 116 + "snapPoint" to value, 117 + ), 118 + ) 119 + } 120 + 121 + // Lifecycle 122 + 123 + init { 124 + (appContext.reactContext as? ReactContext)?.let { 125 + it.addLifecycleEventListener(this) 126 + this.eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(it, this.id) 127 + 128 + this.dialogRootViewGroup = DialogRootViewGroup(context) 129 + this.dialogRootViewGroup.eventDispatcher = this.eventDispatcher 130 + } 131 + SheetManager.add(this) 132 + } 133 + 134 + override fun onLayout( 135 + changed: Boolean, 136 + l: Int, 137 + t: Int, 138 + r: Int, 139 + b: Int, 140 + ) { 141 + this.present() 142 + } 143 + 144 + private fun destroy() { 145 + this.isClosing = false 146 + this.isOpen = false 147 + this.dialog = null 148 + this.innerView = null 149 + SheetManager.remove(this) 150 + } 151 + 152 + // Presentation 153 + 154 + private fun present() { 155 + if (this.isOpen || this.isOpening || this.isClosing) return 156 + 157 + val contentHeight = this.getContentHeight() 158 + 159 + val dialog = BottomSheetDialog(context) 160 + dialog.setContentView(dialogRootViewGroup) 161 + dialog.setCancelable(!preventDismiss) 162 + dialog.setOnShowListener { 163 + val bottomSheet = dialog.findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet) 164 + bottomSheet?.let { 165 + // Let the outside view handle the background color on its own, the default for this is 166 + // white and we don't want that. 167 + it.setBackgroundColor(0) 168 + 169 + val behavior = BottomSheetBehavior.from(it) 170 + 171 + behavior.isFitToContents = true 172 + behavior.halfExpandedRatio = this.clampRatio(this.getTargetHeight() / this.screenHeight) 173 + if (contentHeight > this.screenHeight) { 174 + behavior.state = BottomSheetBehavior.STATE_EXPANDED 175 + this.selectedSnapPoint = 2 176 + } else { 177 + behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED 178 + this.selectedSnapPoint = 1 179 + } 180 + behavior.skipCollapsed = true 181 + behavior.isDraggable = true 182 + behavior.isHideable = true 183 + 184 + behavior.addBottomSheetCallback( 185 + object : BottomSheetBehavior.BottomSheetCallback() { 186 + override fun onStateChanged( 187 + bottomSheet: View, 188 + newState: Int, 189 + ) { 190 + when (newState) { 191 + BottomSheetBehavior.STATE_EXPANDED -> { 192 + selectedSnapPoint = 2 193 + } 194 + BottomSheetBehavior.STATE_COLLAPSED -> { 195 + selectedSnapPoint = 1 196 + } 197 + BottomSheetBehavior.STATE_HALF_EXPANDED -> { 198 + selectedSnapPoint = 1 199 + } 200 + BottomSheetBehavior.STATE_HIDDEN -> { 201 + selectedSnapPoint = 0 202 + } 203 + } 204 + } 205 + 206 + override fun onSlide( 207 + bottomSheet: View, 208 + slideOffset: Float, 209 + ) { } 210 + }, 211 + ) 212 + } 213 + } 214 + dialog.setOnDismissListener { 215 + this.isClosing = true 216 + this.destroy() 217 + } 218 + 219 + this.isOpening = true 220 + dialog.show() 221 + this.dialog = dialog 222 + } 223 + 224 + fun updateLayout() { 225 + val dialog = this.dialog ?: return 226 + val contentHeight = this.getContentHeight() 227 + 228 + val bottomSheet = dialog.findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet) 229 + bottomSheet?.let { 230 + val behavior = BottomSheetBehavior.from(it) 231 + 232 + behavior.halfExpandedRatio = this.clampRatio(this.getTargetHeight() / this.screenHeight) 233 + 234 + if (contentHeight > this.screenHeight && behavior.state != BottomSheetBehavior.STATE_EXPANDED) { 235 + behavior.state = BottomSheetBehavior.STATE_EXPANDED 236 + } else if (contentHeight < this.screenHeight && behavior.state != BottomSheetBehavior.STATE_HALF_EXPANDED) { 237 + behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED 238 + } 239 + } 240 + } 241 + 242 + fun dismiss() { 243 + this.dialog?.dismiss() 244 + } 245 + 246 + // Util 247 + 248 + private fun getContentHeight(): Float { 249 + val innerView = this.innerView ?: return 0f 250 + var index = 0 251 + innerView.allViews.forEach { 252 + if (index == 1) { 253 + return it.height.toFloat() 254 + } 255 + index++ 256 + } 257 + return 0f 258 + } 259 + 260 + private fun getTargetHeight(): Float { 261 + val contentHeight = this.getContentHeight() 262 + val height = 263 + if (contentHeight > maxHeight) { 264 + maxHeight 265 + } else if (contentHeight < minHeight) { 266 + minHeight 267 + } else { 268 + contentHeight 269 + } 270 + return height 271 + } 272 + 273 + private fun clampRatio(ratio: Float): Float { 274 + if (ratio < 0.01) { 275 + return 0.01f 276 + } else if (ratio > 0.99) { 277 + return 0.99f 278 + } 279 + return ratio 280 + } 281 + 282 + private fun setDraggable(draggable: Boolean) { 283 + val dialog = this.dialog ?: return 284 + val bottomSheet = dialog.findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet) 285 + bottomSheet?.let { 286 + val behavior = BottomSheetBehavior.from(it) 287 + behavior.isDraggable = draggable 288 + } 289 + } 290 + 291 + override fun onHostResume() { } 292 + 293 + override fun onHostPause() { } 294 + 295 + override fun onHostDestroy() { 296 + (appContext.reactContext as? ReactContext)?.let { 297 + it.removeLifecycleEventListener(this) 298 + this.destroy() 299 + } 300 + } 301 + 302 + // View overrides to pass to DialogRootViewGroup instead 303 + 304 + override fun dispatchProvideStructure(structure: ViewStructure?) { 305 + dialogRootViewGroup.dispatchProvideStructure(structure) 306 + } 307 + 308 + override fun setId(id: Int) { 309 + super.setId(id) 310 + dialogRootViewGroup.id = id 311 + } 312 + 313 + override fun addView( 314 + child: View?, 315 + index: Int, 316 + ) { 317 + this.innerView = child 318 + (child as ViewGroup).let { 319 + dialogRootViewGroup.addView(child, index) 320 + } 321 + } 322 + 323 + override fun removeView(view: View?) { 324 + UiThreadUtil.assertOnUiThread() 325 + if (view != null) { 326 + dialogRootViewGroup.removeView(view) 327 + } 328 + } 329 + 330 + override fun removeViewAt(index: Int) { 331 + UiThreadUtil.assertOnUiThread() 332 + val child = getChildAt(index) 333 + dialogRootViewGroup.removeView(child) 334 + } 335 + 336 + override fun addChildrenForAccessibility(outChildren: ArrayList<View>?) { } 337 + 338 + override fun dispatchPopulateAccessibilityEvent(event: AccessibilityEvent?): Boolean = false 339 + }
+171
modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/DialogRootViewGroup.kt
···
··· 1 + package expo.modules.bottomsheet 2 + 3 + import android.annotation.SuppressLint 4 + import android.content.Context 5 + import android.view.MotionEvent 6 + import android.view.View 7 + import com.facebook.react.bridge.GuardedRunnable 8 + import com.facebook.react.config.ReactFeatureFlags 9 + import com.facebook.react.uimanager.JSPointerDispatcher 10 + import com.facebook.react.uimanager.JSTouchDispatcher 11 + import com.facebook.react.uimanager.RootView 12 + import com.facebook.react.uimanager.ThemedReactContext 13 + import com.facebook.react.uimanager.UIManagerModule 14 + import com.facebook.react.uimanager.events.EventDispatcher 15 + import com.facebook.react.views.view.ReactViewGroup 16 + 17 + // SEE https://github.com/facebook/react-native/blob/309cdea337101cfe2212cfb6abebf1e783e43282/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.kt#L378 18 + 19 + /** 20 + * DialogRootViewGroup is the ViewGroup which contains all the children of a Modal. It gets all 21 + * child information forwarded from [ReactModalHostView] and uses that to create children. It is 22 + * also responsible for acting as a RootView and handling touch events. It does this the same way 23 + * as ReactRootView. 24 + * 25 + * To get layout to work properly, we need to layout all the elements within the Modal as if they 26 + * can fill the entire window. To do that, we need to explicitly set the styleWidth and 27 + * styleHeight on the LayoutShadowNode to be the window size. This is done through the 28 + * UIManagerModule, and will then cause the children to layout as if they can fill the window. 29 + */ 30 + class DialogRootViewGroup( 31 + private val context: Context?, 32 + ) : ReactViewGroup(context), 33 + RootView { 34 + private var hasAdjustedSize = false 35 + private var viewWidth = 0 36 + private var viewHeight = 0 37 + 38 + private val jSTouchDispatcher = JSTouchDispatcher(this) 39 + private var jSPointerDispatcher: JSPointerDispatcher? = null 40 + private var sizeChangeListener: OnSizeChangeListener? = null 41 + 42 + var eventDispatcher: EventDispatcher? = null 43 + 44 + interface OnSizeChangeListener { 45 + fun onSizeChange( 46 + width: Int, 47 + height: Int, 48 + ) 49 + } 50 + 51 + init { 52 + if (ReactFeatureFlags.dispatchPointerEvents) { 53 + jSPointerDispatcher = JSPointerDispatcher(this) 54 + } 55 + } 56 + 57 + override fun onSizeChanged( 58 + w: Int, 59 + h: Int, 60 + oldw: Int, 61 + oldh: Int, 62 + ) { 63 + super.onSizeChanged(w, h, oldw, oldh) 64 + 65 + viewWidth = w 66 + viewHeight = h 67 + updateFirstChildView() 68 + 69 + sizeChangeListener?.onSizeChange(w, h) 70 + } 71 + 72 + fun setOnSizeChangeListener(listener: OnSizeChangeListener) { 73 + sizeChangeListener = listener 74 + } 75 + 76 + private fun updateFirstChildView() { 77 + if (childCount > 0) { 78 + hasAdjustedSize = false 79 + val viewTag = getChildAt(0).id 80 + reactContext.runOnNativeModulesQueueThread( 81 + object : GuardedRunnable(reactContext) { 82 + override fun runGuarded() { 83 + val uiManager: UIManagerModule = 84 + reactContext 85 + .reactApplicationContext 86 + .getNativeModule(UIManagerModule::class.java) ?: return 87 + 88 + uiManager.updateNodeSize(viewTag, viewWidth, viewHeight) 89 + } 90 + }, 91 + ) 92 + } else { 93 + hasAdjustedSize = true 94 + } 95 + } 96 + 97 + override fun addView( 98 + child: View, 99 + index: Int, 100 + params: LayoutParams, 101 + ) { 102 + super.addView(child, index, params) 103 + if (hasAdjustedSize) { 104 + updateFirstChildView() 105 + } 106 + } 107 + 108 + override fun handleException(t: Throwable) { 109 + reactContext.reactApplicationContext.handleException(RuntimeException(t)) 110 + } 111 + 112 + private val reactContext: ThemedReactContext 113 + get() = context as ThemedReactContext 114 + 115 + override fun onInterceptTouchEvent(event: MotionEvent): Boolean { 116 + eventDispatcher?.let { jSTouchDispatcher.handleTouchEvent(event, it) } 117 + jSPointerDispatcher?.handleMotionEvent(event, eventDispatcher, true) 118 + return super.onInterceptTouchEvent(event) 119 + } 120 + 121 + @SuppressLint("ClickableViewAccessibility") 122 + override fun onTouchEvent(event: MotionEvent): Boolean { 123 + eventDispatcher?.let { jSTouchDispatcher.handleTouchEvent(event, it) } 124 + jSPointerDispatcher?.handleMotionEvent(event, eventDispatcher, false) 125 + super.onTouchEvent(event) 126 + 127 + // In case when there is no children interested in handling touch event, we return true from 128 + // the root view in order to receive subsequent events related to that gesture 129 + return true 130 + } 131 + 132 + override fun onInterceptHoverEvent(event: MotionEvent): Boolean { 133 + jSPointerDispatcher?.handleMotionEvent(event, eventDispatcher, true) 134 + return super.onHoverEvent(event) 135 + } 136 + 137 + override fun onHoverEvent(event: MotionEvent): Boolean { 138 + jSPointerDispatcher?.handleMotionEvent(event, eventDispatcher, false) 139 + return super.onHoverEvent(event) 140 + } 141 + 142 + @Deprecated("Deprecated in Java") 143 + override fun onChildStartedNativeGesture(ev: MotionEvent?) { 144 + eventDispatcher?.let { 145 + if (ev != null) { 146 + jSTouchDispatcher.onChildStartedNativeGesture(ev, it) 147 + } 148 + } 149 + } 150 + 151 + override fun onChildStartedNativeGesture( 152 + childView: View, 153 + ev: MotionEvent, 154 + ) { 155 + eventDispatcher?.let { jSTouchDispatcher.onChildStartedNativeGesture(ev, it) } 156 + jSPointerDispatcher?.onChildStartedNativeGesture(childView, ev, eventDispatcher) 157 + } 158 + 159 + override fun onChildEndedNativeGesture( 160 + childView: View, 161 + ev: MotionEvent, 162 + ) { 163 + eventDispatcher?.let { jSTouchDispatcher.onChildEndedNativeGesture(ev, it) } 164 + jSPointerDispatcher?.onChildEndedNativeGesture() 165 + } 166 + 167 + override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) { 168 + // No-op - override in order to still receive events to onInterceptTouchEvent 169 + // even when some other view disallow that 170 + } 171 + }
+28
modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/SheetManager.kt
···
··· 1 + package expo.modules.bottomsheet 2 + 3 + import java.lang.ref.WeakReference 4 + 5 + class SheetManager { 6 + companion object { 7 + private val sheets = mutableSetOf<WeakReference<BottomSheetView>>() 8 + 9 + fun add(view: BottomSheetView) { 10 + sheets.add(WeakReference(view)) 11 + } 12 + 13 + fun remove(view: BottomSheetView) { 14 + sheets.forEach { 15 + if (it.get() == view) { 16 + sheets.remove(it) 17 + return 18 + } 19 + } 20 + } 21 + 22 + fun dismissAll() { 23 + sheets.forEach { 24 + it.get()?.dismiss() 25 + } 26 + } 27 + } 28 + }
+9
modules/bottom-sheet/expo-module.config.json
···
··· 1 + { 2 + "platforms": ["ios", "android"], 3 + "ios": { 4 + "modules": ["BottomSheetModule"] 5 + }, 6 + "android": { 7 + "modules": ["expo.modules.bottomsheet.BottomSheetModule"] 8 + } 9 + }
+13
modules/bottom-sheet/index.ts
···
··· 1 + import {BottomSheet} from './src/BottomSheet' 2 + import { 3 + BottomSheetSnapPoint, 4 + BottomSheetState, 5 + BottomSheetViewProps, 6 + } from './src/BottomSheet.types' 7 + 8 + export { 9 + BottomSheet, 10 + BottomSheetSnapPoint, 11 + type BottomSheetState, 12 + type BottomSheetViewProps, 13 + }
+21
modules/bottom-sheet/ios/BottomSheet.podspec
···
··· 1 + Pod::Spec.new do |s| 2 + s.name = 'BottomSheet' 3 + s.version = '1.0.0' 4 + s.summary = 'A bottom sheet for use in Bluesky' 5 + s.description = 'A bottom sheet for use in Bluesky' 6 + s.author = '' 7 + s.homepage = 'https://github.com/bluesky-social/social-app' 8 + s.platforms = { :ios => '15.0', :tvos => '15.0' } 9 + s.source = { git: '' } 10 + s.static_framework = true 11 + 12 + s.dependency 'ExpoModulesCore' 13 + 14 + # Swift/Objective-C compatibility 15 + s.pod_target_xcconfig = { 16 + 'DEFINES_MODULE' => 'YES', 17 + 'SWIFT_COMPILATION_MODE' => 'wholemodule' 18 + } 19 + 20 + s.source_files = "**/*.{h,m,swift}" 21 + end
+47
modules/bottom-sheet/ios/BottomSheetModule.swift
···
··· 1 + import ExpoModulesCore 2 + 3 + public class BottomSheetModule: Module { 4 + public func definition() -> ModuleDefinition { 5 + Name("BottomSheet") 6 + 7 + AsyncFunction("dismissAll") { 8 + SheetManager.shared.dismissAll() 9 + } 10 + 11 + View(SheetView.self) { 12 + Events([ 13 + "onAttemptDismiss", 14 + "onSnapPointChange", 15 + "onStateChange" 16 + ]) 17 + 18 + AsyncFunction("dismiss") { (view: SheetView) in 19 + view.dismiss() 20 + } 21 + 22 + AsyncFunction("updateLayout") { (view: SheetView) in 23 + view.updateLayout() 24 + } 25 + 26 + Prop("cornerRadius") { (view: SheetView, prop: Float) in 27 + view.cornerRadius = CGFloat(prop) 28 + } 29 + 30 + Prop("minHeight") { (view: SheetView, prop: Double) in 31 + view.minHeight = prop 32 + } 33 + 34 + Prop("maxHeight") { (view: SheetView, prop: Double) in 35 + view.maxHeight = prop 36 + } 37 + 38 + Prop("preventDismiss") { (view: SheetView, prop: Bool) in 39 + view.preventDismiss = prop 40 + } 41 + 42 + Prop("preventExpansion") { (view: SheetView, prop: Bool) in 43 + view.preventExpansion = prop 44 + } 45 + } 46 + } 47 + }
+28
modules/bottom-sheet/ios/SheetManager.swift
···
··· 1 + // 2 + // SheetManager.swift 3 + // Pods 4 + // 5 + // Created by Hailey on 10/1/24. 6 + // 7 + 8 + import ExpoModulesCore 9 + 10 + class SheetManager { 11 + static let shared = SheetManager() 12 + 13 + private var sheetViews = NSHashTable<SheetView>(options: .weakMemory) 14 + 15 + func add(_ view: SheetView) { 16 + sheetViews.add(view) 17 + } 18 + 19 + func remove(_ view: SheetView) { 20 + sheetViews.remove(view) 21 + } 22 + 23 + func dismissAll() { 24 + sheetViews.allObjects.forEach { sheetView in 25 + sheetView.dismiss() 26 + } 27 + } 28 + }
+189
modules/bottom-sheet/ios/SheetView.swift
···
··· 1 + import ExpoModulesCore 2 + import UIKit 3 + 4 + class SheetView: ExpoView, UISheetPresentationControllerDelegate { 5 + // Views 6 + private var sheetVc: SheetViewController? 7 + private var innerView: UIView? 8 + private var touchHandler: RCTTouchHandler? 9 + 10 + // Events 11 + private let onAttemptDismiss = EventDispatcher() 12 + private let onSnapPointChange = EventDispatcher() 13 + private let onStateChange = EventDispatcher() 14 + 15 + // Open event firing 16 + private var isOpen: Bool = false { 17 + didSet { 18 + onStateChange([ 19 + "state": isOpen ? "open" : "closed" 20 + ]) 21 + } 22 + } 23 + 24 + // React view props 25 + var preventDismiss = false 26 + var preventExpansion = false 27 + var cornerRadius: CGFloat? 28 + var minHeight = 0.0 29 + var maxHeight: CGFloat! { 30 + didSet { 31 + let screenHeight = Util.getScreenHeight() ?? 0 32 + if maxHeight > screenHeight { 33 + maxHeight = screenHeight 34 + } 35 + } 36 + } 37 + 38 + private var isOpening = false { 39 + didSet { 40 + if isOpening { 41 + onStateChange([ 42 + "state": "opening" 43 + ]) 44 + } 45 + } 46 + } 47 + private var isClosing = false { 48 + didSet { 49 + if isClosing { 50 + onStateChange([ 51 + "state": "closing" 52 + ]) 53 + } 54 + } 55 + } 56 + private var selectedDetentIdentifier: UISheetPresentationController.Detent.Identifier? { 57 + didSet { 58 + if selectedDetentIdentifier == .large { 59 + onSnapPointChange([ 60 + "snapPoint": 2 61 + ]) 62 + } else { 63 + onSnapPointChange([ 64 + "snapPoint": 1 65 + ]) 66 + } 67 + } 68 + } 69 + 70 + // MARK: - Lifecycle 71 + 72 + required init (appContext: AppContext? = nil) { 73 + super.init(appContext: appContext) 74 + self.maxHeight = Util.getScreenHeight() 75 + self.touchHandler = RCTTouchHandler(bridge: appContext?.reactBridge) 76 + SheetManager.shared.add(self) 77 + } 78 + 79 + deinit { 80 + self.destroy() 81 + } 82 + 83 + // We don't want this view to actually get added to the tree, so we'll simply store it for adding 84 + // to the SheetViewController 85 + override func insertReactSubview(_ subview: UIView!, at atIndex: Int) { 86 + self.touchHandler?.attach(to: subview) 87 + self.innerView = subview 88 + } 89 + 90 + // We'll grab the content height from here so we know the initial detent to set 91 + override func layoutSubviews() { 92 + super.layoutSubviews() 93 + 94 + guard let innerView = self.innerView else { 95 + return 96 + } 97 + 98 + if innerView.subviews.count != 1 { 99 + return 100 + } 101 + 102 + self.present() 103 + } 104 + 105 + private func destroy() { 106 + self.isClosing = false 107 + self.isOpen = false 108 + self.sheetVc = nil 109 + self.touchHandler?.detach(from: self.innerView) 110 + self.touchHandler = nil 111 + self.innerView = nil 112 + SheetManager.shared.remove(self) 113 + } 114 + 115 + // MARK: - Presentation 116 + 117 + func present() { 118 + guard !self.isOpen, 119 + !self.isOpening, 120 + !self.isClosing, 121 + let innerView = self.innerView, 122 + let contentHeight = innerView.subviews.first?.frame.height, 123 + let rvc = self.reactViewController() else { 124 + return 125 + } 126 + 127 + let sheetVc = SheetViewController() 128 + sheetVc.setDetents(contentHeight: self.clampHeight(contentHeight), preventExpansion: self.preventExpansion) 129 + if let sheet = sheetVc.sheetPresentationController { 130 + sheet.delegate = self 131 + sheet.preferredCornerRadius = self.cornerRadius 132 + self.selectedDetentIdentifier = sheet.selectedDetentIdentifier 133 + } 134 + sheetVc.view.addSubview(innerView) 135 + 136 + self.sheetVc = sheetVc 137 + self.isOpening = true 138 + 139 + rvc.present(sheetVc, animated: true) { [weak self] in 140 + self?.isOpening = false 141 + self?.isOpen = true 142 + } 143 + } 144 + 145 + func updateLayout() { 146 + if let contentHeight = self.innerView?.subviews.first?.frame.size.height { 147 + self.sheetVc?.updateDetents(contentHeight: self.clampHeight(contentHeight), 148 + preventExpansion: self.preventExpansion) 149 + self.selectedDetentIdentifier = self.sheetVc?.getCurrentDetentIdentifier() 150 + } 151 + } 152 + 153 + func dismiss() { 154 + self.isClosing = true 155 + self.sheetVc?.dismiss(animated: true) { [weak self] in 156 + self?.destroy() 157 + } 158 + } 159 + 160 + // MARK: - Utils 161 + 162 + private func clampHeight(_ height: CGFloat) -> CGFloat { 163 + if height < self.minHeight { 164 + return self.minHeight 165 + } else if height > self.maxHeight { 166 + return self.maxHeight 167 + } 168 + return height 169 + } 170 + 171 + // MARK: - UISheetPresentationControllerDelegate 172 + 173 + func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool { 174 + self.onAttemptDismiss() 175 + return !self.preventDismiss 176 + } 177 + 178 + func presentationControllerWillDismiss(_ presentationController: UIPresentationController) { 179 + self.isClosing = true 180 + } 181 + 182 + func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { 183 + self.destroy() 184 + } 185 + 186 + func sheetPresentationControllerDidChangeSelectedDetentIdentifier(_ sheetPresentationController: UISheetPresentationController) { 187 + self.selectedDetentIdentifier = sheetPresentationController.selectedDetentIdentifier 188 + } 189 + }
+76
modules/bottom-sheet/ios/SheetViewController.swift
···
··· 1 + // 2 + // SheetViewController.swift 3 + // Pods 4 + // 5 + // Created by Hailey on 9/30/24. 6 + // 7 + 8 + import Foundation 9 + import UIKit 10 + 11 + class SheetViewController: UIViewController { 12 + init() { 13 + super.init(nibName: nil, bundle: nil) 14 + 15 + self.modalPresentationStyle = .formSheet 16 + self.isModalInPresentation = false 17 + 18 + if let sheet = self.sheetPresentationController { 19 + sheet.prefersGrabberVisible = false 20 + } 21 + } 22 + 23 + func setDetents(contentHeight: CGFloat, preventExpansion: Bool) { 24 + guard let sheet = self.sheetPresentationController, 25 + let screenHeight = Util.getScreenHeight() 26 + else { 27 + return 28 + } 29 + 30 + if contentHeight > screenHeight - 100 { 31 + sheet.detents = [ 32 + .large() 33 + ] 34 + sheet.selectedDetentIdentifier = .large 35 + } else { 36 + if #available(iOS 16.0, *) { 37 + sheet.detents = [ 38 + .custom { _ in 39 + return contentHeight 40 + } 41 + ] 42 + } else { 43 + sheet.detents = [ 44 + .medium() 45 + ] 46 + } 47 + 48 + if !preventExpansion { 49 + sheet.detents.append(.large()) 50 + } 51 + sheet.selectedDetentIdentifier = .medium 52 + } 53 + } 54 + 55 + func updateDetents(contentHeight: CGFloat, preventExpansion: Bool) { 56 + if let sheet = self.sheetPresentationController { 57 + sheet.animateChanges { 58 + self.setDetents(contentHeight: contentHeight, preventExpansion: preventExpansion) 59 + if #available(iOS 16.0, *) { 60 + sheet.invalidateDetents() 61 + } 62 + } 63 + } 64 + } 65 + 66 + func getCurrentDetentIdentifier() -> UISheetPresentationController.Detent.Identifier? { 67 + guard let sheet = self.sheetPresentationController else { 68 + return nil 69 + } 70 + return sheet.selectedDetentIdentifier 71 + } 72 + 73 + required init?(coder: NSCoder) { 74 + fatalError("init(coder:) has not been implemented") 75 + } 76 + }
+18
modules/bottom-sheet/ios/Util.swift
···
··· 1 + // 2 + // Util.swift 3 + // Pods 4 + // 5 + // Created by Hailey on 10/2/24. 6 + // 7 + 8 + class Util { 9 + static func getScreenHeight() -> CGFloat? { 10 + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, 11 + let window = windowScene.windows.first { 12 + let safeAreaInsets = window.safeAreaInsets 13 + let fullScreenHeight = UIScreen.main.bounds.height 14 + return fullScreenHeight - (safeAreaInsets.top + safeAreaInsets.bottom) 15 + } 16 + return nil 17 + } 18 + }
+100
modules/bottom-sheet/src/BottomSheet.tsx
···
··· 1 + import * as React from 'react' 2 + import { 3 + Dimensions, 4 + NativeSyntheticEvent, 5 + Platform, 6 + StyleProp, 7 + View, 8 + ViewStyle, 9 + } from 'react-native' 10 + import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core' 11 + 12 + import {BottomSheetState, BottomSheetViewProps} from './BottomSheet.types' 13 + 14 + const screenHeight = Dimensions.get('screen').height 15 + 16 + const NativeView: React.ComponentType< 17 + BottomSheetViewProps & { 18 + ref: React.RefObject<any> 19 + style: StyleProp<ViewStyle> 20 + } 21 + > = requireNativeViewManager('BottomSheet') 22 + 23 + const NativeModule = requireNativeModule('BottomSheet') 24 + 25 + export class BottomSheet extends React.Component< 26 + BottomSheetViewProps, 27 + { 28 + open: boolean 29 + } 30 + > { 31 + ref = React.createRef<any>() 32 + 33 + constructor(props: BottomSheetViewProps) { 34 + super(props) 35 + this.state = { 36 + open: false, 37 + } 38 + } 39 + 40 + present() { 41 + this.setState({open: true}) 42 + } 43 + 44 + dismiss() { 45 + this.ref.current?.dismiss() 46 + } 47 + 48 + private onStateChange = ( 49 + event: NativeSyntheticEvent<{state: BottomSheetState}>, 50 + ) => { 51 + const {state} = event.nativeEvent 52 + const isOpen = state !== 'closed' 53 + this.setState({open: isOpen}) 54 + this.props.onStateChange?.(event) 55 + } 56 + 57 + private updateLayout = () => { 58 + this.ref.current?.updateLayout() 59 + } 60 + 61 + static dismissAll = async () => { 62 + await NativeModule.dismissAll() 63 + } 64 + 65 + render() { 66 + const {children, backgroundColor, ...rest} = this.props 67 + const cornerRadius = rest.cornerRadius ?? 0 68 + 69 + if (!this.state.open) { 70 + return null 71 + } 72 + 73 + return ( 74 + <NativeView 75 + {...rest} 76 + onStateChange={this.onStateChange} 77 + ref={this.ref} 78 + style={{ 79 + position: 'absolute', 80 + height: screenHeight, 81 + width: '100%', 82 + }} 83 + containerBackgroundColor={backgroundColor}> 84 + <View 85 + style={[ 86 + { 87 + flex: 1, 88 + backgroundColor, 89 + }, 90 + Platform.OS === 'android' && { 91 + borderTopLeftRadius: cornerRadius, 92 + borderTopRightRadius: cornerRadius, 93 + }, 94 + ]}> 95 + <View onLayout={this.updateLayout}>{children}</View> 96 + </View> 97 + </NativeView> 98 + ) 99 + } 100 + }
+35
modules/bottom-sheet/src/BottomSheet.types.ts
···
··· 1 + import React from 'react' 2 + import {ColorValue, NativeSyntheticEvent} from 'react-native' 3 + 4 + export type BottomSheetState = 'closed' | 'closing' | 'open' | 'opening' 5 + 6 + export enum BottomSheetSnapPoint { 7 + Hidden, 8 + Partial, 9 + Full, 10 + } 11 + 12 + export type BottomSheetAttemptDismissEvent = NativeSyntheticEvent<object> 13 + export type BottomSheetSnapPointChangeEvent = NativeSyntheticEvent<{ 14 + snapPoint: BottomSheetSnapPoint 15 + }> 16 + export type BottomSheetStateChangeEvent = NativeSyntheticEvent<{ 17 + state: BottomSheetState 18 + }> 19 + 20 + export interface BottomSheetViewProps { 21 + children: React.ReactNode 22 + cornerRadius?: number 23 + preventDismiss?: boolean 24 + preventExpansion?: boolean 25 + backgroundColor?: ColorValue 26 + containerBackgroundColor?: ColorValue 27 + disableDrag?: boolean 28 + 29 + minHeight?: number 30 + maxHeight?: number 31 + 32 + onAttemptDismiss?: (event: BottomSheetAttemptDismissEvent) => void 33 + onSnapPointChange?: (event: BottomSheetSnapPointChangeEvent) => void 34 + onStateChange?: (event: BottomSheetStateChangeEvent) => void 35 + }
+5
modules/bottom-sheet/src/BottomSheet.web.tsx
···
··· 1 + import {BottomSheetViewProps} from './BottomSheet.types' 2 + 3 + export function BottomSheet(_: BottomSheetViewProps) { 4 + throw new Error('BottomSheetView is not available on web') 5 + }
+2 -3
package.json
··· 137 "expo-sharing": "^12.0.1", 138 "expo-splash-screen": "~0.27.4", 139 "expo-status-bar": "~1.12.1", 140 - "expo-system-ui": "~3.0.4", 141 "expo-task-manager": "~11.8.1", 142 "expo-updates": "~0.25.14", 143 "expo-web-browser": "~13.0.3", ··· 171 "react-native-compressor": "^1.8.24", 172 "react-native-date-picker": "^4.4.2", 173 "react-native-drawer-layout": "^4.0.0-alpha.3", 174 - "react-native-gesture-handler": "~2.16.2", 175 "react-native-get-random-values": "~1.11.0", 176 "react-native-image-crop-picker": "0.41.2", 177 "react-native-ios-context-menu": "^1.15.3", 178 - "react-native-keyboard-controller": "^1.12.1", 179 "react-native-mmkv": "^2.12.2", 180 "react-native-pager-view": "6.2.3", 181 "react-native-picker-select": "^9.1.3",
··· 137 "expo-sharing": "^12.0.1", 138 "expo-splash-screen": "~0.27.4", 139 "expo-status-bar": "~1.12.1", 140 "expo-task-manager": "~11.8.1", 141 "expo-updates": "~0.25.14", 142 "expo-web-browser": "~13.0.3", ··· 170 "react-native-compressor": "^1.8.24", 171 "react-native-date-picker": "^4.4.2", 172 "react-native-drawer-layout": "^4.0.0-alpha.3", 173 + "react-native-gesture-handler": "2.20.0", 174 "react-native-get-random-values": "~1.11.0", 175 "react-native-image-crop-picker": "0.41.2", 176 "react-native-ios-context-menu": "^1.15.3", 177 + "react-native-keyboard-controller": "^1.14.0", 178 "react-native-mmkv": "^2.12.2", 179 "react-native-pager-view": "6.2.3", 180 "react-native-picker-select": "^9.1.3",
+2 -2
src/App.native.tsx
··· 1 import 'react-native-url-polyfill/auto' 2 - import 'lib/sentry' // must be near top 3 - import 'view/icons' 4 5 import React, {useEffect, useState} from 'react' 6 import {GestureHandlerRootView} from 'react-native-gesture-handler'
··· 1 import 'react-native-url-polyfill/auto' 2 + import '#/lib/sentry' // must be near top 3 + import '#/view/icons' 4 5 import React, {useEffect, useState} from 'react' 6 import {GestureHandlerRootView} from 'react-native-gesture-handler'
+2 -4
src/alf/util/useColorModeTheme.ts
··· 1 import React from 'react' 2 import {ColorSchemeName, useColorScheme} from 'react-native' 3 - import * as SystemUI from 'expo-system-ui' 4 5 - import {isWeb} from 'platform/detection' 6 - import {useThemePrefs} from 'state/shell' 7 import {dark, dim, light} from '#/alf/themes' 8 import {ThemeName} from '#/alf/types' 9 ··· 12 13 React.useLayoutEffect(() => { 14 updateDocument(theme) 15 - SystemUI.setBackgroundColorAsync(getBackgroundColor(theme)) 16 }, [theme]) 17 18 return theme
··· 1 import React from 'react' 2 import {ColorSchemeName, useColorScheme} from 'react-native' 3 4 + import {isWeb} from '#/platform/detection' 5 + import {useThemePrefs} from '#/state/shell' 6 import {dark, dim, light} from '#/alf/themes' 7 import {ThemeName} from '#/alf/types' 8 ··· 11 12 React.useLayoutEffect(() => { 13 updateDocument(theme) 14 }, [theme]) 15 16 return theme
+5 -2
src/components/Button.tsx
··· 87 style?: StyleProp<ViewStyle> 88 hoverStyle?: StyleProp<ViewStyle> 89 children: NonTextElements | ((context: ButtonContext) => NonTextElements) 90 } 91 92 export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean} ··· 114 disabled = false, 115 style, 116 hoverStyle: hoverStyleProp, 117 ...rest 118 }, 119 ref, ··· 449 const flattenedBaseStyles = flatten([baseStyles, style]) 450 451 return ( 452 - <Pressable 453 role="button" 454 accessibilityHint={undefined} // optional 455 {...rest} 456 ref={ref} 457 aria-label={label} 458 aria-pressed={state.pressed} ··· 500 <Context.Provider value={context}> 501 {typeof children === 'function' ? children(context) : children} 502 </Context.Provider> 503 - </Pressable> 504 ) 505 }, 506 )
··· 87 style?: StyleProp<ViewStyle> 88 hoverStyle?: StyleProp<ViewStyle> 89 children: NonTextElements | ((context: ButtonContext) => NonTextElements) 90 + PressableComponent?: React.ComponentType<PressableProps> 91 } 92 93 export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean} ··· 115 disabled = false, 116 style, 117 hoverStyle: hoverStyleProp, 118 + PressableComponent = Pressable, 119 ...rest 120 }, 121 ref, ··· 451 const flattenedBaseStyles = flatten([baseStyles, style]) 452 453 return ( 454 + <PressableComponent 455 role="button" 456 accessibilityHint={undefined} // optional 457 {...rest} 458 + // @ts-ignore - this will always be a pressable 459 ref={ref} 460 aria-label={label} 461 aria-pressed={state.pressed} ··· 503 <Context.Provider value={context}> 504 {typeof children === 'function' ? children(context) : children} 505 </Context.Provider> 506 + </PressableComponent> 507 ) 508 }, 509 )
+5
src/components/Dialog/context.ts
··· 6 DialogControlRefProps, 7 DialogOuterProps, 8 } from '#/components/Dialog/types' 9 10 export const Context = React.createContext<DialogContextProps>({ 11 close: () => {}, 12 }) 13 14 export function useDialogContext() {
··· 6 DialogControlRefProps, 7 DialogOuterProps, 8 } from '#/components/Dialog/types' 9 + import {BottomSheetSnapPoint} from '../../../modules/bottom-sheet/src/BottomSheet.types' 10 11 export const Context = React.createContext<DialogContextProps>({ 12 close: () => {}, 13 + isNativeDialog: false, 14 + nativeSnapPoint: BottomSheetSnapPoint.Hidden, 15 + disableDrag: false, 16 + setDisableDrag: () => {}, 17 }) 18 19 export function useDialogContext() {
+173 -207
src/components/Dialog/index.tsx
··· 1 import React, {useImperativeHandle} from 'react' 2 import { 3 - Dimensions, 4 - Keyboard, 5 Pressable, 6 StyleProp, 7 View, 8 ViewStyle, 9 } from 'react-native' 10 - import Animated, {useAnimatedStyle} from 'react-native-reanimated' 11 import {useSafeAreaInsets} from 'react-native-safe-area-context' 12 - import BottomSheet, { 13 - BottomSheetBackdropProps, 14 - BottomSheetFlatList, 15 - BottomSheetFlatListMethods, 16 - BottomSheetScrollView, 17 - BottomSheetScrollViewMethods, 18 - BottomSheetTextInput, 19 - BottomSheetView, 20 - useBottomSheet, 21 - WINDOW_HEIGHT, 22 - } from '@discord/bottom-sheet/src' 23 - import {BottomSheetFlatListProps} from '@discord/bottom-sheet/src/components/bottomSheetScrollable/types' 24 25 import {logger} from '#/logger' 26 import {useDialogStateControlContext} from '#/state/dialogs' 27 - import {atoms as a, flatten, useTheme} from '#/alf' 28 - import {Context} from '#/components/Dialog/context' 29 import { 30 DialogControlProps, 31 DialogInnerProps, 32 DialogOuterProps, 33 } from '#/components/Dialog/types' 34 import {createInput} from '#/components/forms/TextField' 35 - import {FullWindowOverlay} from '#/components/FullWindowOverlay' 36 - import {Portal} from '#/components/Portal' 37 38 export {useDialogContext, useDialogControl} from '#/components/Dialog/context' 39 export * from '#/components/Dialog/types' 40 export * from '#/components/Dialog/utils' 41 // @ts-ignore 42 - export const Input = createInput(BottomSheetTextInput) 43 - 44 - function Backdrop(props: BottomSheetBackdropProps) { 45 - const t = useTheme() 46 - const bottomSheet = useBottomSheet() 47 - 48 - const animatedStyle = useAnimatedStyle(() => { 49 - const opacity = 50 - (Math.abs(WINDOW_HEIGHT - props.animatedPosition.value) - 50) / 1000 51 - 52 - return { 53 - opacity: Math.min(Math.max(opacity, 0), 0.55), 54 - } 55 - }) 56 - 57 - const onPress = React.useCallback(() => { 58 - bottomSheet.close() 59 - }, [bottomSheet]) 60 - 61 - return ( 62 - <Animated.View 63 - style={[ 64 - t.atoms.bg_contrast_300, 65 - { 66 - top: 0, 67 - left: 0, 68 - right: 0, 69 - bottom: 0, 70 - position: 'absolute', 71 - }, 72 - animatedStyle, 73 - ]}> 74 - <Pressable 75 - accessibilityRole="button" 76 - accessibilityLabel="Dialog backdrop" 77 - accessibilityHint="Press the backdrop to close the dialog" 78 - style={{flex: 1}} 79 - onPress={onPress} 80 - /> 81 - </Animated.View> 82 - ) 83 - } 84 85 export function Outer({ 86 children, ··· 88 onClose, 89 nativeOptions, 90 testID, 91 }: React.PropsWithChildren<DialogOuterProps>) { 92 const t = useTheme() 93 - const sheet = React.useRef<BottomSheet>(null) 94 - const sheetOptions = nativeOptions?.sheet || {} 95 - const hasSnapPoints = !!sheetOptions.snapPoints 96 - const insets = useSafeAreaInsets() 97 const closeCallbacks = React.useRef<(() => void)[]>([]) 98 - const {setDialogIsOpen} = useDialogStateControlContext() 99 100 - /* 101 - * Used to manage open/closed, but index is otherwise handled internally by `BottomSheet` 102 - */ 103 - const [openIndex, setOpenIndex] = React.useState(-1) 104 105 - /* 106 - * `openIndex` is the index of the snap point to open the bottom sheet to. If >0, the bottom sheet is open. 107 - */ 108 - const isOpen = openIndex > -1 109 110 const callQueuedCallbacks = React.useCallback(() => { 111 for (const cb of closeCallbacks.current) { ··· 119 closeCallbacks.current = [] 120 }, []) 121 122 - const open = React.useCallback<DialogControlProps['open']>( 123 - ({index} = {}) => { 124 - // Run any leftover callbacks that might have been queued up before calling `.open()` 125 - callQueuedCallbacks() 126 - 127 - setDialogIsOpen(control.id, true) 128 - // can be set to any index of `snapPoints`, but `0` is the first i.e. "open" 129 - setOpenIndex(index || 0) 130 - sheet.current?.snapToIndex(index || 0) 131 - }, 132 - [setDialogIsOpen, control.id, callQueuedCallbacks], 133 - ) 134 135 // This is the function that we call when we want to dismiss the dialog. 136 const close = React.useCallback<DialogControlProps['close']>(cb => { 137 if (typeof cb === 'function') { 138 closeCallbacks.current.push(cb) 139 } 140 - sheet.current?.close() 141 }, []) 142 143 // This is the actual thing we are doing once we "confirm" the dialog. We want the dialog's close animation to ··· 146 // This removes the dialog from our list of stored dialogs. Not super necessary on iOS, but on Android this 147 // tells us that we need to toggle the accessibility overlay setting 148 setDialogIsOpen(control.id, false) 149 - setOpenIndex(-1) 150 - 151 callQueuedCallbacks() 152 onClose?.() 153 }, [callQueuedCallbacks, control.id, onClose, setDialogIsOpen]) 154 155 useImperativeHandle( 156 control.ref, 157 () => ({ ··· 161 [open, close], 162 ) 163 164 - React.useEffect(() => { 165 - return () => { 166 - setDialogIsOpen(control.id, false) 167 - } 168 - }, [control.id, setDialogIsOpen]) 169 - 170 - const context = React.useMemo(() => ({close}), [close]) 171 172 return ( 173 - isOpen && ( 174 - <Portal> 175 - <FullWindowOverlay> 176 - <View 177 - // iOS 178 - accessibilityViewIsModal 179 - // Android 180 - importantForAccessibility="yes" 181 - style={[a.absolute, a.inset_0]} 182 - testID={testID} 183 - onTouchMove={() => Keyboard.dismiss()}> 184 - <BottomSheet 185 - enableDynamicSizing={!hasSnapPoints} 186 - enablePanDownToClose 187 - keyboardBehavior="interactive" 188 - android_keyboardInputMode="adjustResize" 189 - keyboardBlurBehavior="restore" 190 - topInset={insets.top} 191 - {...sheetOptions} 192 - snapPoints={sheetOptions.snapPoints || ['100%']} 193 - ref={sheet} 194 - index={openIndex} 195 - backgroundStyle={{backgroundColor: 'transparent'}} 196 - backdropComponent={Backdrop} 197 - handleIndicatorStyle={{backgroundColor: t.palette.primary_500}} 198 - handleStyle={{display: 'none'}} 199 - onClose={onCloseAnimationComplete}> 200 - <Context.Provider value={context}> 201 - <View 202 - style={[ 203 - a.absolute, 204 - a.inset_0, 205 - t.atoms.bg, 206 - { 207 - borderTopLeftRadius: 40, 208 - borderTopRightRadius: 40, 209 - height: Dimensions.get('window').height * 2, 210 - }, 211 - ]} 212 - /> 213 - {children} 214 - </Context.Provider> 215 - </BottomSheet> 216 - </View> 217 - </FullWindowOverlay> 218 - </Portal> 219 - ) 220 ) 221 } 222 223 export function Inner({children, style}: DialogInnerProps) { 224 const insets = useSafeAreaInsets() 225 return ( 226 - <BottomSheetView 227 style={[ 228 - a.py_xl, 229 a.px_xl, 230 { 231 - paddingTop: 40, 232 - borderTopLeftRadius: 40, 233 - borderTopRightRadius: 40, 234 - paddingBottom: insets.bottom + a.pb_5xl.paddingBottom, 235 }, 236 - flatten(style), 237 ]}> 238 {children} 239 - </BottomSheetView> 240 ) 241 } 242 243 - export const ScrollableInner = React.forwardRef< 244 - BottomSheetScrollViewMethods, 245 - DialogInnerProps 246 - >(function ScrollableInner({children, style}, ref) { 247 - const insets = useSafeAreaInsets() 248 - return ( 249 - <BottomSheetScrollView 250 - keyboardShouldPersistTaps="handled" 251 - style={[ 252 - a.flex_1, // main diff is this 253 - a.p_xl, 254 - a.h_full, 255 - { 256 - paddingTop: 40, 257 - borderTopLeftRadius: 40, 258 - borderTopRightRadius: 40, 259 - }, 260 - style, 261 - ]} 262 - contentContainerStyle={a.pb_4xl} 263 - ref={ref}> 264 - {children} 265 - <View style={{height: insets.bottom + a.pt_5xl.paddingTop}} /> 266 - </BottomSheetScrollView> 267 - ) 268 - }) 269 270 export const InnerFlatList = React.forwardRef< 271 - BottomSheetFlatListMethods, 272 - BottomSheetFlatListProps<any> & {webInnerStyle?: StyleProp<ViewStyle>} 273 - >(function InnerFlatList({style, contentContainerStyle, ...props}, ref) { 274 const insets = useSafeAreaInsets() 275 - 276 return ( 277 - <BottomSheetFlatList 278 keyboardShouldPersistTaps="handled" 279 - contentContainerStyle={[a.pb_4xl, flatten(contentContainerStyle)]} 280 ListFooterComponent={ 281 <View style={{height: insets.bottom + a.pt_5xl.paddingTop}} /> 282 } 283 ref={ref} 284 {...props} 285 - style={[ 286 - a.flex_1, 287 - a.p_xl, 288 - a.pt_0, 289 - a.h_full, 290 - { 291 - marginTop: 40, 292 - }, 293 - flatten(style), 294 - ]} 295 /> 296 ) 297 }) 298 299 export function Handle() { 300 const t = useTheme() 301 302 return ( 303 - <View style={[a.absolute, a.w_full, a.align_center, a.z_10, {height: 40}]}> 304 - <View 305 - style={[ 306 - a.rounded_sm, 307 - { 308 - top: a.pt_lg.paddingTop, 309 - width: 35, 310 - height: 4, 311 - alignSelf: 'center', 312 - backgroundColor: t.palette.contrast_900, 313 - opacity: 0.5, 314 - }, 315 - ]} 316 - /> 317 </View> 318 ) 319 }
··· 1 import React, {useImperativeHandle} from 'react' 2 import { 3 + NativeScrollEvent, 4 + NativeSyntheticEvent, 5 Pressable, 6 + ScrollView, 7 StyleProp, 8 + TextInput, 9 View, 10 ViewStyle, 11 } from 'react-native' 12 + import { 13 + KeyboardAwareScrollView, 14 + useKeyboardHandler, 15 + } from 'react-native-keyboard-controller' 16 + import {runOnJS} from 'react-native-reanimated' 17 import {useSafeAreaInsets} from 'react-native-safe-area-context' 18 + import {msg} from '@lingui/macro' 19 + import {useLingui} from '@lingui/react' 20 21 import {logger} from '#/logger' 22 + import {isAndroid, isIOS} from '#/platform/detection' 23 + import {useA11y} from '#/state/a11y' 24 import {useDialogStateControlContext} from '#/state/dialogs' 25 + import {List, ListMethods, ListProps} from '#/view/com/util/List' 26 + import {atoms as a, useTheme} from '#/alf' 27 + import {Context, useDialogContext} from '#/components/Dialog/context' 28 import { 29 DialogControlProps, 30 DialogInnerProps, 31 DialogOuterProps, 32 } from '#/components/Dialog/types' 33 import {createInput} from '#/components/forms/TextField' 34 + import {Portal as DefaultPortal} from '#/components/Portal' 35 + import {BottomSheet, BottomSheetSnapPoint} from '../../../modules/bottom-sheet' 36 + import { 37 + BottomSheetSnapPointChangeEvent, 38 + BottomSheetStateChangeEvent, 39 + } from '../../../modules/bottom-sheet/src/BottomSheet.types' 40 41 export {useDialogContext, useDialogControl} from '#/components/Dialog/context' 42 export * from '#/components/Dialog/types' 43 export * from '#/components/Dialog/utils' 44 // @ts-ignore 45 + export const Input = createInput(TextInput) 46 47 export function Outer({ 48 children, ··· 50 onClose, 51 nativeOptions, 52 testID, 53 + Portal = DefaultPortal, 54 }: React.PropsWithChildren<DialogOuterProps>) { 55 const t = useTheme() 56 + const ref = React.useRef<BottomSheet>(null) 57 const closeCallbacks = React.useRef<(() => void)[]>([]) 58 + const {setDialogIsOpen, setFullyExpandedCount} = 59 + useDialogStateControlContext() 60 61 + const prevSnapPoint = React.useRef<BottomSheetSnapPoint>( 62 + BottomSheetSnapPoint.Hidden, 63 + ) 64 65 + const [disableDrag, setDisableDrag] = React.useState(false) 66 + const [snapPoint, setSnapPoint] = React.useState<BottomSheetSnapPoint>( 67 + BottomSheetSnapPoint.Partial, 68 + ) 69 70 const callQueuedCallbacks = React.useCallback(() => { 71 for (const cb of closeCallbacks.current) { ··· 79 closeCallbacks.current = [] 80 }, []) 81 82 + const open = React.useCallback<DialogControlProps['open']>(() => { 83 + // Run any leftover callbacks that might have been queued up before calling `.open()` 84 + callQueuedCallbacks() 85 + setDialogIsOpen(control.id, true) 86 + ref.current?.present() 87 + }, [setDialogIsOpen, control.id, callQueuedCallbacks]) 88 89 // This is the function that we call when we want to dismiss the dialog. 90 const close = React.useCallback<DialogControlProps['close']>(cb => { 91 if (typeof cb === 'function') { 92 closeCallbacks.current.push(cb) 93 } 94 + ref.current?.dismiss() 95 }, []) 96 97 // This is the actual thing we are doing once we "confirm" the dialog. We want the dialog's close animation to ··· 100 // This removes the dialog from our list of stored dialogs. Not super necessary on iOS, but on Android this 101 // tells us that we need to toggle the accessibility overlay setting 102 setDialogIsOpen(control.id, false) 103 callQueuedCallbacks() 104 onClose?.() 105 }, [callQueuedCallbacks, control.id, onClose, setDialogIsOpen]) 106 107 + const onSnapPointChange = (e: BottomSheetSnapPointChangeEvent) => { 108 + const {snapPoint} = e.nativeEvent 109 + setSnapPoint(snapPoint) 110 + 111 + if ( 112 + snapPoint === BottomSheetSnapPoint.Full && 113 + prevSnapPoint.current !== BottomSheetSnapPoint.Full 114 + ) { 115 + setFullyExpandedCount(c => c + 1) 116 + } else if ( 117 + snapPoint !== BottomSheetSnapPoint.Full && 118 + prevSnapPoint.current === BottomSheetSnapPoint.Full 119 + ) { 120 + setFullyExpandedCount(c => c - 1) 121 + } 122 + prevSnapPoint.current = snapPoint 123 + } 124 + 125 + const onStateChange = (e: BottomSheetStateChangeEvent) => { 126 + if (e.nativeEvent.state === 'closed') { 127 + onCloseAnimationComplete() 128 + 129 + if (prevSnapPoint.current === BottomSheetSnapPoint.Full) { 130 + setFullyExpandedCount(c => c - 1) 131 + } 132 + prevSnapPoint.current = BottomSheetSnapPoint.Hidden 133 + } 134 + } 135 + 136 useImperativeHandle( 137 control.ref, 138 () => ({ ··· 142 [open, close], 143 ) 144 145 + const context = React.useMemo( 146 + () => ({ 147 + close, 148 + isNativeDialog: true, 149 + nativeSnapPoint: snapPoint, 150 + disableDrag, 151 + setDisableDrag, 152 + }), 153 + [close, snapPoint, disableDrag, setDisableDrag], 154 + ) 155 156 return ( 157 + <Portal> 158 + <Context.Provider value={context}> 159 + <BottomSheet 160 + ref={ref} 161 + cornerRadius={20} 162 + backgroundColor={t.atoms.bg.backgroundColor} 163 + {...nativeOptions} 164 + onSnapPointChange={onSnapPointChange} 165 + onStateChange={onStateChange} 166 + disableDrag={disableDrag}> 167 + <View testID={testID}>{children}</View> 168 + </BottomSheet> 169 + </Context.Provider> 170 + </Portal> 171 ) 172 } 173 174 export function Inner({children, style}: DialogInnerProps) { 175 const insets = useSafeAreaInsets() 176 return ( 177 + <View 178 style={[ 179 + a.pt_2xl, 180 a.px_xl, 181 { 182 + paddingBottom: insets.bottom + insets.top, 183 }, 184 + style, 185 ]}> 186 {children} 187 + </View> 188 ) 189 } 190 191 + export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>( 192 + function ScrollableInner({children, style, ...props}, ref) { 193 + const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext() 194 + const insets = useSafeAreaInsets() 195 + const [keyboardHeight, setKeyboardHeight] = React.useState(0) 196 + useKeyboardHandler({ 197 + onEnd: e => { 198 + 'worklet' 199 + runOnJS(setKeyboardHeight)(e.height) 200 + }, 201 + }) 202 + 203 + const basePading = 204 + (isIOS ? 30 : 50) + (isIOS ? keyboardHeight / 4 : keyboardHeight) 205 + const fullPaddingBase = insets.bottom + insets.top + basePading 206 + const fullPadding = isIOS ? fullPaddingBase : fullPaddingBase + 50 207 + 208 + const paddingBottom = 209 + nativeSnapPoint === BottomSheetSnapPoint.Full ? fullPadding : basePading 210 + 211 + const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => { 212 + const {contentOffset} = e.nativeEvent 213 + if (contentOffset.y > 0 && !disableDrag) { 214 + setDisableDrag(true) 215 + } else if (contentOffset.y <= 1 && disableDrag) { 216 + setDisableDrag(false) 217 + } 218 + } 219 + 220 + return ( 221 + <KeyboardAwareScrollView 222 + style={[style]} 223 + contentContainerStyle={[a.pt_2xl, a.px_xl, {paddingBottom}]} 224 + ref={ref} 225 + {...props} 226 + bounces={nativeSnapPoint === BottomSheetSnapPoint.Full} 227 + bottomOffset={30} 228 + scrollEventThrottle={50} 229 + onScroll={isAndroid ? onScroll : undefined}> 230 + {children} 231 + </KeyboardAwareScrollView> 232 + ) 233 + }, 234 + ) 235 236 export const InnerFlatList = React.forwardRef< 237 + ListMethods, 238 + ListProps<any> & {webInnerStyle?: StyleProp<ViewStyle>} 239 + >(function InnerFlatList({style, ...props}, ref) { 240 const insets = useSafeAreaInsets() 241 + const {nativeSnapPoint} = useDialogContext() 242 return ( 243 + <List 244 keyboardShouldPersistTaps="handled" 245 + bounces={nativeSnapPoint === BottomSheetSnapPoint.Full} 246 ListFooterComponent={ 247 <View style={{height: insets.bottom + a.pt_5xl.paddingTop}} /> 248 } 249 ref={ref} 250 {...props} 251 + style={[style]} 252 /> 253 ) 254 }) 255 256 export function Handle() { 257 const t = useTheme() 258 + const {_} = useLingui() 259 + const {screenReaderEnabled} = useA11y() 260 + const {close} = useDialogContext() 261 262 return ( 263 + <View style={[a.absolute, a.w_full, a.align_center, a.z_10, {height: 20}]}> 264 + <Pressable 265 + accessible={screenReaderEnabled} 266 + onPress={() => close()} 267 + accessibilityLabel={_(msg`Dismiss`)} 268 + accessibilityHint={_(msg`Double tap to close the dialog`)}> 269 + <View 270 + style={[ 271 + a.rounded_sm, 272 + { 273 + top: 10, 274 + width: 35, 275 + height: 5, 276 + alignSelf: 'center', 277 + backgroundColor: t.palette.contrast_975, 278 + opacity: 0.5, 279 + }, 280 + ]} 281 + /> 282 + </Pressable> 283 </View> 284 ) 285 }
+8 -4
src/components/Dialog/index.web.tsx
··· 103 const context = React.useMemo( 104 () => ({ 105 close, 106 }), 107 [close], 108 ) ··· 229 ) 230 }) 231 232 - export function Handle() { 233 - return null 234 - } 235 - 236 export function Close() { 237 const {_} = useLingui() 238 const {close} = React.useContext(Context) ··· 258 </View> 259 ) 260 }
··· 103 const context = React.useMemo( 104 () => ({ 105 close, 106 + isNativeDialog: false, 107 + nativeSnapPoint: 0, 108 + disableDrag: false, 109 + setDisableDrag: () => {}, 110 }), 111 [close], 112 ) ··· 233 ) 234 }) 235 236 export function Close() { 237 const {_} = useLingui() 238 const {close} = React.useContext(Context) ··· 258 </View> 259 ) 260 } 261 + 262 + export function Handle() { 263 + return null 264 + }
+20
src/components/Dialog/sheet-wrapper.ts
···
··· 1 + import {useCallback} from 'react' 2 + 3 + import {useDialogStateControlContext} from '#/state/dialogs' 4 + 5 + /** 6 + * If we're calling a system API like the image picker that opens a sheet 7 + * wrap it in this function to make sure the status bar is the correct color. 8 + */ 9 + export function useSheetWrapper() { 10 + const {setFullyExpandedCount} = useDialogStateControlContext() 11 + return useCallback( 12 + async <T>(promise: Promise<T>): Promise<T> => { 13 + setFullyExpandedCount(c => c + 1) 14 + const res = await promise 15 + setFullyExpandedCount(c => c - 1) 16 + return res 17 + }, 18 + [setFullyExpandedCount], 19 + ) 20 + }
+9 -4
src/components/Dialog/types.ts
··· 4 GestureResponderEvent, 5 ScrollViewProps, 6 } from 'react-native' 7 - import {BottomSheetProps} from '@discord/bottom-sheet/src' 8 9 import {ViewStyleProp} from '#/alf' 10 11 type A11yProps = Required<AccessibilityProps> 12 ··· 37 38 export type DialogContextProps = { 39 close: DialogControlProps['close'] 40 } 41 42 export type DialogControlOpenOptions = { ··· 52 export type DialogOuterProps = { 53 control: DialogControlProps 54 onClose?: () => void 55 - nativeOptions?: { 56 - sheet?: Omit<BottomSheetProps, 'children'> 57 - } 58 webOptions?: {} 59 testID?: string 60 } 61 62 type DialogInnerPropsBase<T> = React.PropsWithChildren<ViewStyleProp> & T
··· 4 GestureResponderEvent, 5 ScrollViewProps, 6 } from 'react-native' 7 8 import {ViewStyleProp} from '#/alf' 9 + import {PortalComponent} from '#/components/Portal' 10 + import {BottomSheetViewProps} from '../../../modules/bottom-sheet' 11 + import {BottomSheetSnapPoint} from '../../../modules/bottom-sheet/src/BottomSheet.types' 12 13 type A11yProps = Required<AccessibilityProps> 14 ··· 39 40 export type DialogContextProps = { 41 close: DialogControlProps['close'] 42 + isNativeDialog: boolean 43 + nativeSnapPoint: BottomSheetSnapPoint 44 + disableDrag: boolean 45 + setDisableDrag: React.Dispatch<React.SetStateAction<boolean>> 46 } 47 48 export type DialogControlOpenOptions = { ··· 58 export type DialogOuterProps = { 59 control: DialogControlProps 60 onClose?: () => void 61 + nativeOptions?: Omit<BottomSheetViewProps, 'children'> 62 webOptions?: {} 63 testID?: string 64 + Portal?: PortalComponent 65 } 66 67 type DialogInnerPropsBase<T> = React.PropsWithChildren<ViewStyleProp> & T
-31
src/components/KeyboardControllerPadding.android.tsx
··· 1 - import React from 'react' 2 - import {useKeyboardHandler} from 'react-native-keyboard-controller' 3 - import Animated, { 4 - useAnimatedStyle, 5 - useSharedValue, 6 - } from 'react-native-reanimated' 7 - 8 - export function KeyboardControllerPadding({maxHeight}: {maxHeight?: number}) { 9 - const keyboardHeight = useSharedValue(0) 10 - 11 - useKeyboardHandler( 12 - { 13 - onMove: e => { 14 - 'worklet' 15 - 16 - if (maxHeight && e.height > maxHeight) { 17 - keyboardHeight.value = maxHeight 18 - } else { 19 - keyboardHeight.value = e.height 20 - } 21 - }, 22 - }, 23 - [maxHeight], 24 - ) 25 - 26 - const animatedStyle = useAnimatedStyle(() => ({ 27 - height: keyboardHeight.value, 28 - })) 29 - 30 - return <Animated.View style={animatedStyle} /> 31 - }
···
-7
src/components/KeyboardControllerPadding.tsx
··· 1 - export function KeyboardControllerPadding({ 2 - maxHeight: _, 3 - }: { 4 - maxHeight?: number 5 - }) { 6 - return null 7 - }
···
+7 -9
src/components/LikesDialog.tsx
··· 1 - import React, {useMemo, useCallback} from 'react' 2 import {ActivityIndicator, FlatList, View} from 'react-native' 3 import {msg, Trans} from '@lingui/macro' 4 import {useLingui} from '@lingui/react' 5 - import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api' 6 7 - import {useResolveUriQuery} from '#/state/queries/resolve-uri' 8 - import {useLikedByQuery} from '#/state/queries/post-liked-by' 9 import {cleanError} from '#/lib/strings/errors' 10 import {logger} from '#/logger' 11 - 12 import {atoms as a, useTheme} from '#/alf' 13 - import {Text} from '#/components/Typography' 14 import * as Dialog from '#/components/Dialog' 15 - import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 16 - import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard' 17 import {Loader} from '#/components/Loader' 18 19 interface LikesDialogProps { 20 control: Dialog.DialogOuterProps['control'] ··· 25 return ( 26 <Dialog.Outer control={props.control}> 27 <Dialog.Handle /> 28 - 29 <LikesDialogInner {...props} /> 30 </Dialog.Outer> 31 )
··· 1 + import React, {useCallback, useMemo} from 'react' 2 import {ActivityIndicator, FlatList, View} from 'react-native' 3 + import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api' 4 import {msg, Trans} from '@lingui/macro' 5 import {useLingui} from '@lingui/react' 6 7 import {cleanError} from '#/lib/strings/errors' 8 import {logger} from '#/logger' 9 + import {useLikedByQuery} from '#/state/queries/post-liked-by' 10 + import {useResolveUriQuery} from '#/state/queries/resolve-uri' 11 + import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard' 12 + import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 13 import {atoms as a, useTheme} from '#/alf' 14 import * as Dialog from '#/components/Dialog' 15 import {Loader} from '#/components/Loader' 16 + import {Text} from '#/components/Typography' 17 18 interface LikesDialogProps { 19 control: Dialog.DialogOuterProps['control'] ··· 24 return ( 25 <Dialog.Outer control={props.control}> 26 <Dialog.Handle /> 27 <LikesDialogInner {...props} /> 28 </Dialog.Outer> 29 )
+3 -3
src/components/Link.tsx
··· 103 linkRequiresWarning(href, displayText), 104 ) 105 106 - if (requiresWarning) { 107 e.preventDefault() 108 109 openModal({ 110 name: 'link-warning', 111 text: displayText, 112 href: href, 113 }) 114 } else { 115 - e.preventDefault() 116 - 117 if (isExternal) { 118 openLink(href) 119 } else {
··· 103 linkRequiresWarning(href, displayText), 104 ) 105 106 + if (isWeb) { 107 e.preventDefault() 108 + } 109 110 + if (requiresWarning) { 111 openModal({ 112 name: 'link-warning', 113 text: displayText, 114 href: href, 115 }) 116 } else { 117 if (isExternal) { 118 openLink(href) 119 } else {
+11 -10
src/components/Menu/index.tsx
··· 4 import {useLingui} from '@lingui/react' 5 import flattenReactChildren from 'react-keyed-flatten-children' 6 7 - import {isNative} from 'platform/detection' 8 import {atoms as a, useTheme} from '#/alf' 9 import {Button, ButtonText} from '#/components/Button' 10 import * as Dialog from '#/components/Dialog' ··· 82 style?: StyleProp<ViewStyle> 83 }>) { 84 const context = React.useContext(Context) 85 86 return ( 87 - <Dialog.Outer control={context.control}> 88 <Dialog.Handle /> 89 - 90 {/* Re-wrap with context since Dialogs are portal-ed to root */} 91 <Context.Provider value={context}> 92 - <Dialog.ScrollableInner label="Menu TODO"> 93 <View style={[a.gap_lg]}> 94 {children} 95 {isNative && showCancel && <Cancel />} 96 </View> 97 - <View style={{height: a.gap_lg.gap}} /> 98 </Dialog.ScrollableInner> 99 </Context.Provider> 100 </Dialog.Outer> ··· 116 {...rest} 117 accessibilityHint="" 118 accessibilityLabel={label} 119 - onPress={e => { 120 - onPress(e) 121 - 122 if (!e.defaultPrevented) { 123 control?.close() 124 } 125 }} 126 - onFocus={onFocus} 127 - onBlur={onBlur} 128 onPressIn={e => { 129 onPressIn() 130 rest.onPressIn?.(e)
··· 4 import {useLingui} from '@lingui/react' 5 import flattenReactChildren from 'react-keyed-flatten-children' 6 7 + import {isNative} from '#/platform/detection' 8 import {atoms as a, useTheme} from '#/alf' 9 import {Button, ButtonText} from '#/components/Button' 10 import * as Dialog from '#/components/Dialog' ··· 82 style?: StyleProp<ViewStyle> 83 }>) { 84 const context = React.useContext(Context) 85 + const {_} = useLingui() 86 87 return ( 88 + <Dialog.Outer 89 + control={context.control} 90 + nativeOptions={{preventExpansion: true}}> 91 <Dialog.Handle /> 92 {/* Re-wrap with context since Dialogs are portal-ed to root */} 93 <Context.Provider value={context}> 94 + <Dialog.ScrollableInner label={_(msg`Menu`)} style={[a.pt_sm]}> 95 <View style={[a.gap_lg]}> 96 {children} 97 {isNative && showCancel && <Cancel />} 98 + <View style={[{height: a.pb_lg.paddingBottom}]} /> 99 </View> 100 </Dialog.ScrollableInner> 101 </Context.Provider> 102 </Dialog.Outer> ··· 118 {...rest} 119 accessibilityHint="" 120 accessibilityLabel={label} 121 + onFocus={onFocus} 122 + onBlur={onBlur} 123 + onPress={async e => { 124 + await onPress(e) 125 if (!e.defaultPrevented) { 126 control?.close() 127 } 128 }} 129 onPressIn={e => { 130 onPressIn() 131 rest.onPressIn?.(e)
+3 -3
src/components/NewskieDialog.tsx
··· 5 import {useLingui} from '@lingui/react' 6 import {differenceInSeconds} from 'date-fns' 7 8 import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo' 9 import {isNative} from '#/platform/detection' 10 import {useModerationOpts} from '#/state/preferences/moderation-opts' 11 - import {HITSLOP_10} from 'lib/constants' 12 - import {sanitizeDisplayName} from 'lib/strings/display-names' 13 - import {useSession} from 'state/session' 14 import {atoms as a, useTheme} from '#/alf' 15 import {Button, ButtonText} from '#/components/Button' 16 import * as Dialog from '#/components/Dialog'
··· 5 import {useLingui} from '@lingui/react' 6 import {differenceInSeconds} from 'date-fns' 7 8 + import {HITSLOP_10} from '#/lib/constants' 9 import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo' 10 + import {sanitizeDisplayName} from '#/lib/strings/display-names' 11 import {isNative} from '#/platform/detection' 12 import {useModerationOpts} from '#/state/preferences/moderation-opts' 13 + import {useSession} from '#/state/session' 14 import {atoms as a, useTheme} from '#/alf' 15 import {Button, ButtonText} from '#/components/Button' 16 import * as Dialog from '#/components/Dialog'
+2
src/components/Portal.tsx
··· 12 [id: string]: Component 13 } 14 15 export function createPortalGroup() { 16 const Context = React.createContext<ContextType>({ 17 outlet: null,
··· 12 [id: string]: Component 13 } 14 15 + export type PortalComponent = ({children}: {children?: React.ReactNode}) => null 16 + 17 export function createPortalGroup() { 18 const Context = React.createContext<ContextType>({ 19 outlet: null,
+11 -7
src/components/Prompt.tsx
··· 4 import {useLingui} from '@lingui/react' 5 6 import {atoms as a, useBreakpoints, useTheme} from '#/alf' 7 - import {Button, ButtonColor, ButtonProps, ButtonText} from '#/components/Button' 8 import * as Dialog from '#/components/Dialog' 9 import {Text} from '#/components/Typography' 10 11 export { ··· 25 children, 26 control, 27 testID, 28 }: React.PropsWithChildren<{ 29 control: Dialog.DialogControlProps 30 testID?: string 31 }>) { 32 const {gtMobile} = useBreakpoints() 33 const titleId = React.useId() ··· 39 ) 40 41 return ( 42 - <Dialog.Outer control={control} testID={testID}> 43 <Context.Provider value={context}> 44 - <Dialog.Handle /> 45 - 46 <Dialog.ScrollableInner 47 accessibilityLabelledBy={titleId} 48 accessibilityDescribedBy={descriptionId} ··· 141 * Note: The dialog will close automatically when the action is pressed, you 142 * should NOT close the dialog as a side effect of this method. 143 */ 144 - onPress: ButtonProps['onPress'] 145 color?: ButtonColor 146 /** 147 * Optional i18n string. If undefined, it will default to "Confirm". ··· 181 onConfirm, 182 confirmButtonColor, 183 showCancel = true, 184 }: React.PropsWithChildren<{ 185 control: Dialog.DialogOuterProps['control'] 186 title: string ··· 194 * Note: The dialog will close automatically when the action is pressed, you 195 * should NOT close the dialog as a side effect of this method. 196 */ 197 - onConfirm: ButtonProps['onPress'] 198 confirmButtonColor?: ButtonColor 199 showCancel?: boolean 200 }>) { 201 return ( 202 - <Outer control={control} testID="confirmModal"> 203 <TitleText>{title}</TitleText> 204 <DescriptionText>{description}</DescriptionText> 205 <Actions>
··· 4 import {useLingui} from '@lingui/react' 5 6 import {atoms as a, useBreakpoints, useTheme} from '#/alf' 7 + import {Button, ButtonColor, ButtonText} from '#/components/Button' 8 import * as Dialog from '#/components/Dialog' 9 + import {PortalComponent} from '#/components/Portal' 10 import {Text} from '#/components/Typography' 11 12 export { ··· 26 children, 27 control, 28 testID, 29 + Portal, 30 }: React.PropsWithChildren<{ 31 control: Dialog.DialogControlProps 32 testID?: string 33 + Portal?: PortalComponent 34 }>) { 35 const {gtMobile} = useBreakpoints() 36 const titleId = React.useId() ··· 42 ) 43 44 return ( 45 + <Dialog.Outer control={control} testID={testID} Portal={Portal}> 46 + <Dialog.Handle /> 47 <Context.Provider value={context}> 48 <Dialog.ScrollableInner 49 accessibilityLabelledBy={titleId} 50 accessibilityDescribedBy={descriptionId} ··· 143 * Note: The dialog will close automatically when the action is pressed, you 144 * should NOT close the dialog as a side effect of this method. 145 */ 146 + onPress: (e: GestureResponderEvent) => void 147 color?: ButtonColor 148 /** 149 * Optional i18n string. If undefined, it will default to "Confirm". ··· 183 onConfirm, 184 confirmButtonColor, 185 showCancel = true, 186 + Portal, 187 }: React.PropsWithChildren<{ 188 control: Dialog.DialogOuterProps['control'] 189 title: string ··· 197 * Note: The dialog will close automatically when the action is pressed, you 198 * should NOT close the dialog as a side effect of this method. 199 */ 200 + onConfirm: (e: GestureResponderEvent) => void 201 confirmButtonColor?: ButtonColor 202 showCancel?: boolean 203 + Portal?: PortalComponent 204 }>) { 205 return ( 206 + <Outer control={control} testID="confirmModal" Portal={Portal}> 207 <TitleText>{title}</TitleText> 208 <DescriptionText>{description}</DescriptionText> 209 <Actions>
-1
src/components/ReportDialog/SelectLabelerView.tsx
··· 4 import {msg, Trans} from '@lingui/macro' 5 import {useLingui} from '@lingui/react' 6 7 - export {useDialogControl as useReportDialogControl} from '#/components/Dialog' 8 import {getLabelingServiceTitle} from '#/lib/moderation' 9 import {atoms as a, useBreakpoints, useTheme} from '#/alf' 10 import {Button, useButtonContext} from '#/components/Button'
··· 4 import {msg, Trans} from '@lingui/macro' 5 import {useLingui} from '@lingui/react' 6 7 import {getLabelingServiceTitle} from '#/lib/moderation' 8 import {atoms as a, useBreakpoints, useTheme} from '#/alf' 9 import {Button, useButtonContext} from '#/components/Button'
+3
src/components/ReportDialog/SubmitView.tsx
··· 6 7 import {getLabelingServiceTitle} from '#/lib/moderation' 8 import {ReportOption} from '#/lib/moderation/useReportOptions' 9 import {useAgent} from '#/state/session' 10 import {CharProgress} from '#/view/com/composer/char-progress/CharProgress' 11 import * as Toast from '#/view/com/util/Toast' ··· 225 {submitting && <ButtonIcon icon={Loader} />} 226 </Button> 227 </View> 228 </View> 229 ) 230 }
··· 6 7 import {getLabelingServiceTitle} from '#/lib/moderation' 8 import {ReportOption} from '#/lib/moderation/useReportOptions' 9 + import {isAndroid} from '#/platform/detection' 10 import {useAgent} from '#/state/session' 11 import {CharProgress} from '#/view/com/composer/char-progress/CharProgress' 12 import * as Toast from '#/view/com/util/Toast' ··· 226 {submitting && <ButtonIcon icon={Loader} />} 227 </Button> 228 </View> 229 + {/* Maybe fix this later -h */} 230 + {isAndroid ? <View style={{height: 300}} /> : null} 231 </View> 232 ) 233 }
+2 -7
src/components/ReportDialog/index.tsx
··· 1 import React from 'react' 2 import {Pressable, View} from 'react-native' 3 import {msg, Trans} from '@lingui/macro' 4 import {useLingui} from '@lingui/react' 5 ··· 8 export {useDialogControl as useReportDialogControl} from '#/components/Dialog' 9 10 import {AppBskyLabelerDefs} from '@atproto/api' 11 - import {BottomSheetScrollViewMethods} from '@discord/bottom-sheet/src' 12 13 import {atoms as a} from '#/alf' 14 import * as Dialog from '#/components/Dialog' 15 import {useDelayedLoading} from '#/components/hooks/useDelayedLoading' 16 - import {useOnKeyboardDidShow} from '#/components/hooks/useOnKeyboard' 17 import {Loader} from '#/components/Loader' 18 import {Text} from '#/components/Typography' 19 import {SelectLabelerView} from './SelectLabelerView' ··· 25 return ( 26 <Dialog.Outer control={props.control}> 27 <Dialog.Handle /> 28 - 29 <ReportDialogInner {...props} /> 30 </Dialog.Outer> 31 ) ··· 40 } = useMyLabelersQuery() 41 const isLoading = useDelayedLoading(500, isLabelerLoading) 42 43 - const ref = React.useRef<BottomSheetScrollViewMethods>(null) 44 - useOnKeyboardDidShow(() => { 45 - ref.current?.scrollToEnd({animated: true}) 46 - }) 47 48 return ( 49 <Dialog.ScrollableInner label={_(msg`Report dialog`)} ref={ref}>
··· 1 import React from 'react' 2 import {Pressable, View} from 'react-native' 3 + import {ScrollView} from 'react-native-gesture-handler' 4 import {msg, Trans} from '@lingui/macro' 5 import {useLingui} from '@lingui/react' 6 ··· 9 export {useDialogControl as useReportDialogControl} from '#/components/Dialog' 10 11 import {AppBskyLabelerDefs} from '@atproto/api' 12 13 import {atoms as a} from '#/alf' 14 import * as Dialog from '#/components/Dialog' 15 import {useDelayedLoading} from '#/components/hooks/useDelayedLoading' 16 import {Loader} from '#/components/Loader' 17 import {Text} from '#/components/Typography' 18 import {SelectLabelerView} from './SelectLabelerView' ··· 24 return ( 25 <Dialog.Outer control={props.control}> 26 <Dialog.Handle /> 27 <ReportDialogInner {...props} /> 28 </Dialog.Outer> 29 ) ··· 38 } = useMyLabelersQuery() 39 const isLoading = useDelayedLoading(500, isLabelerLoading) 40 41 + const ref = React.useRef<ScrollView>(null) 42 43 return ( 44 <Dialog.ScrollableInner label={_(msg`Report dialog`)} ref={ref}>
-1
src/components/StarterPack/QrCodeDialog.tsx
··· 149 150 return ( 151 <Dialog.Outer control={control}> 152 - <Dialog.Handle /> 153 <Dialog.ScrollableInner 154 label={_(msg`Create a QR code for a starter pack`)}> 155 <View style={[a.flex_1, a.align_center, a.gap_5xl]}>
··· 149 150 return ( 151 <Dialog.Outer control={control}> 152 <Dialog.ScrollableInner 153 label={_(msg`Create a QR code for a starter pack`)}> 154 <View style={[a.flex_1, a.align_center, a.gap_5xl]}>
+8 -8
src/components/StarterPack/ShareDialog.tsx
··· 6 import {msg, Trans} from '@lingui/macro' 7 import {useLingui} from '@lingui/react' 8 9 import {logger} from '#/logger' 10 - import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 11 - import {saveImageToMediaLibrary} from 'lib/media/manip' 12 - import {shareUrl} from 'lib/sharing' 13 - import {logEvent} from 'lib/statsig/statsig' 14 - import {getStarterPackOgCard} from 'lib/strings/starter-pack' 15 - import {isNative, isWeb} from 'platform/detection' 16 - import * as Toast from 'view/com/util/Toast' 17 import {atoms as a, useTheme} from '#/alf' 18 import {Button, ButtonText} from '#/components/Button' 19 import {DialogControlProps} from '#/components/Dialog' ··· 32 export function ShareDialog(props: Props) { 33 return ( 34 <Dialog.Outer control={props.control}> 35 <ShareDialogInner {...props} /> 36 </Dialog.Outer> 37 ) ··· 84 85 return ( 86 <> 87 - <Dialog.Handle /> 88 <Dialog.ScrollableInner label={_(msg`Share link dialog`)}> 89 {!imageLoaded || !link ? ( 90 <View style={[a.p_xl, a.align_center]}>
··· 6 import {msg, Trans} from '@lingui/macro' 7 import {useLingui} from '@lingui/react' 8 9 + import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 10 + import {saveImageToMediaLibrary} from '#/lib/media/manip' 11 + import {shareUrl} from '#/lib/sharing' 12 + import {logEvent} from '#/lib/statsig/statsig' 13 + import {getStarterPackOgCard} from '#/lib/strings/starter-pack' 14 import {logger} from '#/logger' 15 + import {isNative, isWeb} from '#/platform/detection' 16 + import * as Toast from '#/view/com/util/Toast' 17 import {atoms as a, useTheme} from '#/alf' 18 import {Button, ButtonText} from '#/components/Button' 19 import {DialogControlProps} from '#/components/Dialog' ··· 32 export function ShareDialog(props: Props) { 33 return ( 34 <Dialog.Outer control={props.control}> 35 + <Dialog.Handle /> 36 <ShareDialogInner {...props} /> 37 </Dialog.Outer> 38 ) ··· 85 86 return ( 87 <> 88 <Dialog.ScrollableInner label={_(msg`Share link dialog`)}> 89 {!imageLoaded || !link ? ( 90 <View style={[a.p_xl, a.align_center]}>
+8 -18
src/components/StarterPack/Wizard/WizardEditListDialog.tsx
··· 3 import {View} from 'react-native' 4 import {AppBskyActorDefs, ModerationOpts} from '@atproto/api' 5 import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs' 6 - import {BottomSheetFlatListMethods} from '@discord/bottom-sheet' 7 import {msg, Trans} from '@lingui/macro' 8 import {useLingui} from '@lingui/react' 9 10 - import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender' 11 - import {isWeb} from 'platform/detection' 12 - import {useSession} from 'state/session' 13 import {WizardAction, WizardState} from '#/screens/StarterPack/Wizard/State' 14 import {atoms as a, native, useTheme, web} from '#/alf' 15 import {Button, ButtonText} from '#/components/Button' ··· 45 const {currentAccount} = useSession() 46 const initialNumToRender = useInitialNumToRender() 47 48 - const listRef = useRef<BottomSheetFlatListMethods>(null) 49 50 const getData = () => { 51 if (state.currentStep === 'Feeds') return state.feeds ··· 76 ) 77 78 return ( 79 - <Dialog.Outer 80 - control={control} 81 - testID="newChatDialog" 82 - nativeOptions={{sheet: {snapPoints: ['95%']}}}> 83 <Dialog.Handle /> 84 <Dialog.InnerFlatList 85 ref={listRef} ··· 89 ListHeaderComponent={ 90 <View 91 style={[ 92 a.flex_row, 93 a.justify_between, 94 a.border_b, ··· 103 height: 48, 104 }, 105 ] 106 - : [ 107 - a.pb_sm, 108 - a.align_end, 109 - { 110 - height: 68, 111 - }, 112 - ], 113 ]}> 114 <View style={{width: 60}} /> 115 <Text style={[a.font_bold, a.text_xl]}> ··· 143 paddingHorizontal: 0, 144 marginTop: 0, 145 paddingTop: 0, 146 - borderTopLeftRadius: 40, 147 - borderTopRightRadius: 40, 148 }), 149 ]} 150 webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]}
··· 3 import {View} from 'react-native' 4 import {AppBskyActorDefs, ModerationOpts} from '@atproto/api' 5 import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs' 6 import {msg, Trans} from '@lingui/macro' 7 import {useLingui} from '@lingui/react' 8 9 + import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 10 + import {isWeb} from '#/platform/detection' 11 + import {useSession} from '#/state/session' 12 + import {ListMethods} from '#/view/com/util/List' 13 import {WizardAction, WizardState} from '#/screens/StarterPack/Wizard/State' 14 import {atoms as a, native, useTheme, web} from '#/alf' 15 import {Button, ButtonText} from '#/components/Button' ··· 45 const {currentAccount} = useSession() 46 const initialNumToRender = useInitialNumToRender() 47 48 + const listRef = useRef<ListMethods>(null) 49 50 const getData = () => { 51 if (state.currentStep === 'Feeds') return state.feeds ··· 76 ) 77 78 return ( 79 + <Dialog.Outer control={control} testID="newChatDialog"> 80 <Dialog.Handle /> 81 <Dialog.InnerFlatList 82 ref={listRef} ··· 86 ListHeaderComponent={ 87 <View 88 style={[ 89 + native(a.pt_4xl), 90 a.flex_row, 91 a.justify_between, 92 a.border_b, ··· 101 height: 48, 102 }, 103 ] 104 + : [a.pb_sm, a.align_end], 105 ]}> 106 <View style={{width: 60}} /> 107 <Text style={[a.font_bold, a.text_xl]}> ··· 135 paddingHorizontal: 0, 136 marginTop: 0, 137 paddingTop: 0, 138 }), 139 ]} 140 webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]}
-1
src/components/TagMenu/index.tsx
··· 85 86 <Dialog.Outer control={control}> 87 <Dialog.Handle /> 88 - 89 <Dialog.Inner label={_(msg`Tag menu: ${displayTag}`)}> 90 {isPreferencesLoading ? ( 91 <View style={[a.w_full, a.align_center]}>
··· 85 86 <Dialog.Outer control={control}> 87 <Dialog.Handle /> 88 <Dialog.Inner label={_(msg`Tag menu: ${displayTag}`)}> 89 {isPreferencesLoading ? ( 90 <View style={[a.w_full, a.align_center]}>
-1
src/components/dialogs/BirthDateSettings.tsx
··· 31 return ( 32 <Dialog.Outer control={control}> 33 <Dialog.Handle /> 34 - 35 <Dialog.ScrollableInner label={_(msg`My Birthday`)}> 36 <View style={[a.gap_sm, a.pb_lg]}> 37 <Text style={[a.text_2xl, a.font_bold]}>
··· 31 return ( 32 <Dialog.Outer control={control}> 33 <Dialog.Handle /> 34 <Dialog.ScrollableInner label={_(msg`My Birthday`)}> 35 <View style={[a.gap_sm, a.pb_lg]}> 36 <Text style={[a.text_2xl, a.font_bold]}>
-1
src/components/dialogs/EmbedConsent.tsx
··· 50 return ( 51 <Dialog.Outer control={control}> 52 <Dialog.Handle /> 53 - 54 <Dialog.ScrollableInner 55 label={_(msg`External Media`)} 56 style={[gtMobile ? {width: 'auto', maxWidth: 400} : a.w_full]}>
··· 50 return ( 51 <Dialog.Outer control={control}> 52 <Dialog.Handle /> 53 <Dialog.ScrollableInner 54 label={_(msg`External Media`)} 55 style={[gtMobile ? {width: 'auto', maxWidth: 400} : a.w_full]}>
-255
src/components/dialogs/GifSelect.ios.tsx
··· 1 - import React, { 2 - useCallback, 3 - useImperativeHandle, 4 - useMemo, 5 - useRef, 6 - useState, 7 - } from 'react' 8 - import {Modal, ScrollView, TextInput, View} from 'react-native' 9 - import {msg, Trans} from '@lingui/macro' 10 - import {useLingui} from '@lingui/react' 11 - 12 - import {cleanError} from '#/lib/strings/errors' 13 - import { 14 - Gif, 15 - useFeaturedGifsQuery, 16 - useGifSearchQuery, 17 - } from '#/state/queries/tenor' 18 - import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' 19 - import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' 20 - import {FlatList_INTERNAL} from '#/view/com/util/Views' 21 - import {atoms as a, useBreakpoints, useTheme} from '#/alf' 22 - import * as TextField from '#/components/forms/TextField' 23 - import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2' 24 - import {Button, ButtonText} from '../Button' 25 - import {Handle} from '../Dialog' 26 - import {useThrottledValue} from '../hooks/useThrottledValue' 27 - import {ListFooter, ListMaybePlaceholder} from '../Lists' 28 - import {GifPreview} from './GifSelect.shared' 29 - 30 - export function GifSelectDialog({ 31 - controlRef, 32 - onClose, 33 - onSelectGif: onSelectGifProp, 34 - }: { 35 - controlRef: React.RefObject<{open: () => void}> 36 - onClose: () => void 37 - onSelectGif: (gif: Gif) => void 38 - }) { 39 - const t = useTheme() 40 - const [open, setOpen] = useState(false) 41 - 42 - useImperativeHandle(controlRef, () => ({ 43 - open: () => setOpen(true), 44 - })) 45 - 46 - const close = useCallback(() => { 47 - setOpen(false) 48 - onClose() 49 - }, [onClose]) 50 - 51 - const onSelectGif = useCallback( 52 - (gif: Gif) => { 53 - onSelectGifProp(gif) 54 - close() 55 - }, 56 - [onSelectGifProp, close], 57 - ) 58 - 59 - const renderErrorBoundary = useCallback( 60 - (error: any) => <ModalError details={String(error)} close={close} />, 61 - [close], 62 - ) 63 - 64 - return ( 65 - <Modal 66 - visible={open} 67 - animationType="slide" 68 - presentationStyle="formSheet" 69 - onRequestClose={close} 70 - aria-modal 71 - accessibilityViewIsModal> 72 - <View style={[a.flex_1, t.atoms.bg]}> 73 - <Handle /> 74 - <ErrorBoundary renderError={renderErrorBoundary}> 75 - <GifList onSelectGif={onSelectGif} close={close} /> 76 - </ErrorBoundary> 77 - </View> 78 - </Modal> 79 - ) 80 - } 81 - 82 - function GifList({ 83 - onSelectGif, 84 - }: { 85 - close: () => void 86 - onSelectGif: (gif: Gif) => void 87 - }) { 88 - const {_} = useLingui() 89 - const t = useTheme() 90 - const {gtMobile} = useBreakpoints() 91 - const textInputRef = useRef<TextInput>(null) 92 - const listRef = useRef<FlatList_INTERNAL>(null) 93 - const [undeferredSearch, setSearch] = useState('') 94 - const search = useThrottledValue(undeferredSearch, 500) 95 - 96 - const isSearching = search.length > 0 97 - 98 - const trendingQuery = useFeaturedGifsQuery() 99 - const searchQuery = useGifSearchQuery(search) 100 - 101 - const { 102 - data, 103 - fetchNextPage, 104 - isFetchingNextPage, 105 - hasNextPage, 106 - error, 107 - isLoading, 108 - isError, 109 - refetch, 110 - } = isSearching ? searchQuery : trendingQuery 111 - 112 - const flattenedData = useMemo(() => { 113 - return data?.pages.flatMap(page => page.results) || [] 114 - }, [data]) 115 - 116 - const renderItem = useCallback( 117 - ({item}: {item: Gif}) => { 118 - return <GifPreview gif={item} onSelectGif={onSelectGif} /> 119 - }, 120 - [onSelectGif], 121 - ) 122 - 123 - const onEndReached = React.useCallback(() => { 124 - if (isFetchingNextPage || !hasNextPage || error) return 125 - fetchNextPage() 126 - }, [isFetchingNextPage, hasNextPage, error, fetchNextPage]) 127 - 128 - const hasData = flattenedData.length > 0 129 - 130 - const onGoBack = useCallback(() => { 131 - if (isSearching) { 132 - // clear the input and reset the state 133 - textInputRef.current?.clear() 134 - setSearch('') 135 - } else { 136 - close() 137 - } 138 - }, [isSearching]) 139 - 140 - const listHeader = useMemo(() => { 141 - return ( 142 - <View style={[a.relative, a.mb_lg, a.pt_4xl, a.flex_row, a.align_center]}> 143 - {/* cover top corners */} 144 - <View 145 - style={[ 146 - a.absolute, 147 - a.inset_0, 148 - { 149 - borderBottomLeftRadius: 8, 150 - borderBottomRightRadius: 8, 151 - }, 152 - t.atoms.bg, 153 - ]} 154 - /> 155 - 156 - <TextField.Root> 157 - <TextField.Icon icon={Search} /> 158 - <TextField.Input 159 - label={_(msg`Search GIFs`)} 160 - placeholder={_(msg`Search Tenor`)} 161 - onChangeText={text => { 162 - setSearch(text) 163 - listRef.current?.scrollToOffset({offset: 0, animated: false}) 164 - }} 165 - returnKeyType="search" 166 - clearButtonMode="while-editing" 167 - inputRef={textInputRef} 168 - maxLength={50} 169 - /> 170 - </TextField.Root> 171 - </View> 172 - ) 173 - }, [t.atoms.bg, _]) 174 - 175 - return ( 176 - <FlatList_INTERNAL 177 - ref={listRef} 178 - key={gtMobile ? '3 cols' : '2 cols'} 179 - data={flattenedData} 180 - renderItem={renderItem} 181 - numColumns={gtMobile ? 3 : 2} 182 - columnWrapperStyle={a.gap_sm} 183 - contentContainerStyle={a.px_lg} 184 - ListHeaderComponent={ 185 - <> 186 - {listHeader} 187 - {!hasData && ( 188 - <ListMaybePlaceholder 189 - isLoading={isLoading} 190 - isError={isError} 191 - onRetry={refetch} 192 - onGoBack={onGoBack} 193 - emptyType="results" 194 - sideBorders={false} 195 - topBorder={false} 196 - errorTitle={_(msg`Failed to load GIFs`)} 197 - errorMessage={_(msg`There was an issue connecting to Tenor.`)} 198 - emptyMessage={ 199 - isSearching 200 - ? _(msg`No search results found for "${search}".`) 201 - : _( 202 - msg`No featured GIFs found. There may be an issue with Tenor.`, 203 - ) 204 - } 205 - /> 206 - )} 207 - </> 208 - } 209 - stickyHeaderIndices={[0]} 210 - onEndReached={onEndReached} 211 - onEndReachedThreshold={4} 212 - keyExtractor={(item: Gif) => item.id} 213 - keyboardDismissMode="on-drag" 214 - ListFooterComponent={ 215 - hasData ? ( 216 - <ListFooter 217 - isFetchingNextPage={isFetchingNextPage} 218 - error={cleanError(error)} 219 - onRetry={fetchNextPage} 220 - style={{borderTopWidth: 0}} 221 - /> 222 - ) : null 223 - } 224 - /> 225 - ) 226 - } 227 - 228 - function ModalError({details, close}: {details?: string; close: () => void}) { 229 - const {_} = useLingui() 230 - 231 - return ( 232 - <ScrollView 233 - style={[a.flex_1, a.gap_md]} 234 - centerContent 235 - contentContainerStyle={a.px_lg}> 236 - <ErrorScreen 237 - title={_(msg`Oh no!`)} 238 - message={_( 239 - msg`There was an unexpected issue in the application. Please let us know if this happened to you!`, 240 - )} 241 - details={details} 242 - /> 243 - <Button 244 - label={_(msg`Close dialog`)} 245 - onPress={close} 246 - color="primary" 247 - size="large" 248 - variant="solid"> 249 - <ButtonText> 250 - <Trans>Close</Trans> 251 - </ButtonText> 252 - </Button> 253 - </ScrollView> 254 - ) 255 - }
···
-53
src/components/dialogs/GifSelect.shared.tsx
··· 1 - import React, {useCallback} from 'react' 2 - import {Image} from 'expo-image' 3 - import {msg} from '@lingui/macro' 4 - import {useLingui} from '@lingui/react' 5 - 6 - import {logEvent} from '#/lib/statsig/statsig' 7 - import {Gif} from '#/state/queries/tenor' 8 - import {atoms as a, useBreakpoints, useTheme} from '#/alf' 9 - import {Button} from '../Button' 10 - 11 - export function GifPreview({ 12 - gif, 13 - onSelectGif, 14 - }: { 15 - gif: Gif 16 - onSelectGif: (gif: Gif) => void 17 - }) { 18 - const {gtTablet} = useBreakpoints() 19 - const {_} = useLingui() 20 - const t = useTheme() 21 - 22 - const onPress = useCallback(() => { 23 - logEvent('composer:gif:select', {}) 24 - onSelectGif(gif) 25 - }, [onSelectGif, gif]) 26 - 27 - return ( 28 - <Button 29 - label={_(msg`Select GIF "${gif.title}"`)} 30 - style={[a.flex_1, gtTablet ? {maxWidth: '33%'} : {maxWidth: '50%'}]} 31 - onPress={onPress}> 32 - {({pressed}) => ( 33 - <Image 34 - style={[ 35 - a.flex_1, 36 - a.mb_sm, 37 - a.rounded_sm, 38 - {aspectRatio: 1, opacity: pressed ? 0.8 : 1}, 39 - t.atoms.bg_contrast_25, 40 - ]} 41 - source={{ 42 - uri: gif.media_formats.tinygif.url, 43 - }} 44 - contentFit="cover" 45 - accessibilityLabel={gif.title} 46 - accessibilityHint="" 47 - cachePolicy="none" 48 - accessibilityIgnoresInvertColors 49 - /> 50 - )} 51 - </Button> 52 - ) 53 - }
···
+66 -9
src/components/dialogs/GifSelect.tsx
··· 6 useState, 7 } from 'react' 8 import {TextInput, View} from 'react-native' 9 - import {BottomSheetFlatListMethods} from '@discord/bottom-sheet' 10 import {msg, Trans} from '@lingui/macro' 11 import {useLingui} from '@lingui/react' 12 13 import {cleanError} from '#/lib/strings/errors' 14 import {isWeb} from '#/platform/detection' 15 import { ··· 19 } from '#/state/queries/tenor' 20 import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' 21 import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' 22 - import {atoms as a, useBreakpoints, useTheme} from '#/alf' 23 import * as Dialog from '#/components/Dialog' 24 import * as TextField from '#/components/forms/TextField' 25 import {useThrottledValue} from '#/components/hooks/useThrottledValue' ··· 27 import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2' 28 import {Button, ButtonIcon, ButtonText} from '../Button' 29 import {ListFooter, ListMaybePlaceholder} from '../Lists' 30 - import {GifPreview} from './GifSelect.shared' 31 32 export function GifSelectDialog({ 33 controlRef, 34 onClose, 35 onSelectGif: onSelectGifProp, 36 }: { 37 controlRef: React.RefObject<{open: () => void}> 38 onClose: () => void 39 onSelectGif: (gif: Gif) => void 40 }) { 41 const control = Dialog.useDialogControl() 42 ··· 59 return ( 60 <Dialog.Outer 61 control={control} 62 - nativeOptions={{sheet: {snapPoints: ['100%']}}} 63 - onClose={onClose}> 64 <Dialog.Handle /> 65 <ErrorBoundary renderError={renderErrorBoundary}> 66 <GifList control={control} onSelectGif={onSelectGif} /> ··· 80 const t = useTheme() 81 const {gtMobile} = useBreakpoints() 82 const textInputRef = useRef<TextInput>(null) 83 - const listRef = useRef<BottomSheetFlatListMethods>(null) 84 const [undeferredSearch, setSearch] = useState('') 85 const search = useThrottledValue(undeferredSearch, 500) 86 87 const isSearching = search.length > 0 88 ··· 95 isFetchingNextPage, 96 hasNextPage, 97 error, 98 - isLoading, 99 isError, 100 refetch, 101 } = isSearching ? searchQuery : trendingQuery ··· 132 return ( 133 <View 134 style={[ 135 a.relative, 136 a.mb_lg, 137 a.flex_row, ··· 196 data={flattenedData} 197 renderItem={renderItem} 198 numColumns={gtMobile ? 3 : 2} 199 - columnWrapperStyle={a.gap_sm} 200 ListHeaderComponent={ 201 <> 202 {listHeader} 203 {!hasData && ( 204 <ListMaybePlaceholder 205 - isLoading={isLoading} 206 isError={isError} 207 onRetry={refetch} 208 onGoBack={onGoBack} ··· 273 </Dialog.ScrollableInner> 274 ) 275 }
··· 6 useState, 7 } from 'react' 8 import {TextInput, View} from 'react-native' 9 + import {useWindowDimensions} from 'react-native' 10 + import {Image} from 'expo-image' 11 import {msg, Trans} from '@lingui/macro' 12 import {useLingui} from '@lingui/react' 13 14 + import {logEvent} from '#/lib/statsig/statsig' 15 import {cleanError} from '#/lib/strings/errors' 16 import {isWeb} from '#/platform/detection' 17 import { ··· 21 } from '#/state/queries/tenor' 22 import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' 23 import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' 24 + import {ListMethods} from '#/view/com/util/List' 25 + import {atoms as a, ios, native, useBreakpoints, useTheme} from '#/alf' 26 import * as Dialog from '#/components/Dialog' 27 import * as TextField from '#/components/forms/TextField' 28 import {useThrottledValue} from '#/components/hooks/useThrottledValue' ··· 30 import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2' 31 import {Button, ButtonIcon, ButtonText} from '../Button' 32 import {ListFooter, ListMaybePlaceholder} from '../Lists' 33 + import {PortalComponent} from '../Portal' 34 35 export function GifSelectDialog({ 36 controlRef, 37 onClose, 38 onSelectGif: onSelectGifProp, 39 + Portal, 40 }: { 41 controlRef: React.RefObject<{open: () => void}> 42 onClose: () => void 43 onSelectGif: (gif: Gif) => void 44 + Portal?: PortalComponent 45 }) { 46 const control = Dialog.useDialogControl() 47 ··· 64 return ( 65 <Dialog.Outer 66 control={control} 67 + onClose={onClose} 68 + Portal={Portal} 69 + nativeOptions={{ 70 + bottomInset: 0, 71 + // use system corner radius on iOS 72 + ...ios({cornerRadius: undefined}), 73 + }}> 74 <Dialog.Handle /> 75 <ErrorBoundary renderError={renderErrorBoundary}> 76 <GifList control={control} onSelectGif={onSelectGif} /> ··· 90 const t = useTheme() 91 const {gtMobile} = useBreakpoints() 92 const textInputRef = useRef<TextInput>(null) 93 + const listRef = useRef<ListMethods>(null) 94 const [undeferredSearch, setSearch] = useState('') 95 const search = useThrottledValue(undeferredSearch, 500) 96 + const {height} = useWindowDimensions() 97 98 const isSearching = search.length > 0 99 ··· 106 isFetchingNextPage, 107 hasNextPage, 108 error, 109 + isPending, 110 isError, 111 refetch, 112 } = isSearching ? searchQuery : trendingQuery ··· 143 return ( 144 <View 145 style={[ 146 + native(a.pt_4xl), 147 a.relative, 148 a.mb_lg, 149 a.flex_row, ··· 208 data={flattenedData} 209 renderItem={renderItem} 210 numColumns={gtMobile ? 3 : 2} 211 + columnWrapperStyle={[a.gap_sm]} 212 + contentContainerStyle={[native([a.px_xl, {minHeight: height}])]} 213 ListHeaderComponent={ 214 <> 215 {listHeader} 216 {!hasData && ( 217 <ListMaybePlaceholder 218 + isLoading={isPending} 219 isError={isError} 220 onRetry={refetch} 221 onGoBack={onGoBack} ··· 286 </Dialog.ScrollableInner> 287 ) 288 } 289 + 290 + export function GifPreview({ 291 + gif, 292 + onSelectGif, 293 + }: { 294 + gif: Gif 295 + onSelectGif: (gif: Gif) => void 296 + }) { 297 + const {gtTablet} = useBreakpoints() 298 + const {_} = useLingui() 299 + const t = useTheme() 300 + 301 + const onPress = useCallback(() => { 302 + logEvent('composer:gif:select', {}) 303 + onSelectGif(gif) 304 + }, [onSelectGif, gif]) 305 + 306 + return ( 307 + <Button 308 + label={_(msg`Select GIF "${gif.title}"`)} 309 + style={[a.flex_1, gtTablet ? {maxWidth: '33%'} : {maxWidth: '50%'}]} 310 + onPress={onPress}> 311 + {({pressed}) => ( 312 + <Image 313 + style={[ 314 + a.flex_1, 315 + a.mb_sm, 316 + a.rounded_sm, 317 + {aspectRatio: 1, opacity: pressed ? 0.8 : 1}, 318 + t.atoms.bg_contrast_25, 319 + ]} 320 + source={{ 321 + uri: gif.media_formats.tinygif.url, 322 + }} 323 + contentFit="cover" 324 + accessibilityLabel={gif.title} 325 + accessibilityHint="" 326 + cachePolicy="none" 327 + accessibilityIgnoresInvertColors 328 + /> 329 + )} 330 + </Button> 331 + ) 332 + }
+286 -240
src/components/dialogs/MutedWords.tsx
··· 30 import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' 31 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' 32 import {Loader} from '#/components/Loader' 33 import * as Prompt from '#/components/Prompt' 34 import {Text} from '#/components/Typography' 35 36 const ONE_DAY = 24 * 60 * 60 * 1000 37 38 export function MutedWordsDialog() { 39 const {mutedWordsDialogControl: control} = useGlobalDialogsControlContext() ··· 105 }, [_, field, targets, addMutedWord, setField, durations, excludeFollowing]) 106 107 return ( 108 - <Dialog.ScrollableInner label={_(msg`Manage your muted words and tags`)}> 109 - <View> 110 - <Text 111 - style={[a.text_md, a.font_bold, a.pb_sm, t.atoms.text_contrast_high]}> 112 - <Trans>Add muted words and tags</Trans> 113 - </Text> 114 - <Text style={[a.pb_lg, a.leading_snug, t.atoms.text_contrast_medium]}> 115 - <Trans> 116 - Posts can be muted based on their text, their tags, or both. We 117 - recommend avoiding common words that appear in many posts, since it 118 - can result in no posts being shown. 119 - </Trans> 120 - </Text> 121 122 - <View style={[a.pb_sm]}> 123 - <Dialog.Input 124 - autoCorrect={false} 125 - autoCapitalize="none" 126 - autoComplete="off" 127 - label={_(msg`Enter a word or tag`)} 128 - placeholder={_(msg`Enter a word or tag`)} 129 - value={field} 130 - onChangeText={value => { 131 - if (error) { 132 - setError('') 133 - } 134 - setField(value) 135 - }} 136 - onSubmitEditing={submit} 137 - /> 138 - </View> 139 140 - <View style={[a.pb_xl, a.gap_sm]}> 141 - <Toggle.Group 142 - label={_(msg`Select how long to mute this word for.`)} 143 - type="radio" 144 - values={durations} 145 - onChange={setDurations}> 146 - <Text 147 - style={[ 148 - a.pb_xs, 149 - a.text_sm, 150 - a.font_bold, 151 - t.atoms.text_contrast_medium, 152 - ]}> 153 - <Trans>Duration:</Trans> 154 - </Text> 155 156 - <View 157 - style={[ 158 - gtMobile && [a.flex_row, a.align_center, a.justify_start], 159 - a.gap_sm, 160 - ]}> 161 <View 162 style={[ 163 - a.flex_1, 164 - a.flex_row, 165 - a.justify_start, 166 - a.align_center, 167 a.gap_sm, 168 ]}> 169 - <Toggle.Item 170 - label={_(msg`Mute this word until you unmute it`)} 171 - name="forever" 172 - style={[a.flex_1]}> 173 - <TargetToggle> 174 - <View 175 - style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}> 176 - <Toggle.Radio /> 177 - <Toggle.LabelText style={[a.flex_1, a.leading_tight]}> 178 - <Trans>Forever</Trans> 179 - </Toggle.LabelText> 180 - </View> 181 - </TargetToggle> 182 - </Toggle.Item> 183 184 - <Toggle.Item 185 - label={_(msg`Mute this word for 24 hours`)} 186 - name="24_hours" 187 - style={[a.flex_1]}> 188 - <TargetToggle> 189 - <View 190 - style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}> 191 - <Toggle.Radio /> 192 - <Toggle.LabelText style={[a.flex_1, a.leading_tight]}> 193 - <Trans>24 hours</Trans> 194 - </Toggle.LabelText> 195 - </View> 196 - </TargetToggle> 197 - </Toggle.Item> 198 </View> 199 200 - <View 201 style={[ 202 - a.flex_1, 203 - a.flex_row, 204 - a.justify_start, 205 - a.align_center, 206 - a.gap_sm, 207 ]}> 208 <Toggle.Item 209 - label={_(msg`Mute this word for 7 days`)} 210 - name="7_days" 211 style={[a.flex_1]}> 212 <TargetToggle> 213 <View 214 style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}> 215 <Toggle.Radio /> 216 <Toggle.LabelText style={[a.flex_1, a.leading_tight]}> 217 - <Trans>7 days</Trans> 218 </Toggle.LabelText> 219 </View> 220 </TargetToggle> 221 </Toggle.Item> 222 223 <Toggle.Item 224 - label={_(msg`Mute this word for 30 days`)} 225 - name="30_days" 226 style={[a.flex_1]}> 227 <TargetToggle> 228 <View 229 style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}> 230 <Toggle.Radio /> 231 <Toggle.LabelText style={[a.flex_1, a.leading_tight]}> 232 - <Trans>30 days</Trans> 233 </Toggle.LabelText> 234 </View> 235 </TargetToggle> 236 </Toggle.Item> 237 </View> 238 - </View> 239 - </Toggle.Group> 240 241 - <Toggle.Group 242 - label={_(msg`Select what content this mute word should apply to.`)} 243 - type="radio" 244 - values={targets} 245 - onChange={setTargets}> 246 - <Text 247 - style={[ 248 - a.pb_xs, 249 - a.text_sm, 250 - a.font_bold, 251 - t.atoms.text_contrast_medium, 252 - ]}> 253 - <Trans>Mute in:</Trans> 254 - </Text> 255 - 256 - <View style={[a.flex_row, a.align_center, a.gap_sm, a.flex_wrap]}> 257 <Toggle.Item 258 - label={_(msg`Mute this word in post text and tags`)} 259 - name="content" 260 - style={[a.flex_1]}> 261 <TargetToggle> 262 <View 263 style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}> 264 - <Toggle.Radio /> 265 <Toggle.LabelText style={[a.flex_1, a.leading_tight]}> 266 - <Trans>Text & tags</Trans> 267 </Toggle.LabelText> 268 </View> 269 - <PageText size="sm" /> 270 </TargetToggle> 271 </Toggle.Item> 272 273 - <Toggle.Item 274 - label={_(msg`Mute this word in tags only`)} 275 - name="tag" 276 - style={[a.flex_1]}> 277 - <TargetToggle> 278 - <View 279 - style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}> 280 - <Toggle.Radio /> 281 - <Toggle.LabelText style={[a.flex_1, a.leading_tight]}> 282 - <Trans>Tags only</Trans> 283 - </Toggle.LabelText> 284 - </View> 285 - <Hashtag size="sm" /> 286 - </TargetToggle> 287 - </Toggle.Item> 288 </View> 289 - </Toggle.Group> 290 291 - <View> 292 <Text 293 style={[ 294 - a.pb_xs, 295 - a.text_sm, 296 a.font_bold, 297 - t.atoms.text_contrast_medium, 298 ]}> 299 - <Trans>Options:</Trans> 300 </Text> 301 - <Toggle.Item 302 - label={_(msg`Do not apply this mute word to users you follow`)} 303 - name="exclude_following" 304 - style={[a.flex_row, a.justify_between]} 305 - value={excludeFollowing} 306 - onChange={setExcludeFollowing}> 307 - <TargetToggle> 308 - <View style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}> 309 - <Toggle.Checkbox /> 310 - <Toggle.LabelText style={[a.flex_1, a.leading_tight]}> 311 - <Trans>Exclude users you follow</Trans> 312 - </Toggle.LabelText> 313 - </View> 314 - </TargetToggle> 315 - </Toggle.Item> 316 - </View> 317 318 - <View style={[a.pt_xs]}> 319 - <Button 320 - disabled={isPending || !field} 321 - label={_(msg`Add mute word for configured settings`)} 322 - size="large" 323 - color="primary" 324 - variant="solid" 325 - style={[]} 326 - onPress={submit}> 327 - <ButtonText> 328 - <Trans>Add</Trans> 329 - </ButtonText> 330 - <ButtonIcon icon={isPending ? Loader : Plus} position="right" /> 331 - </Button> 332 - </View> 333 - 334 - {error && ( 335 - <View 336 - style={[ 337 - a.mb_lg, 338 - a.flex_row, 339 - a.rounded_sm, 340 - a.p_md, 341 - a.mb_xs, 342 - t.atoms.bg_contrast_25, 343 - { 344 - backgroundColor: t.palette.negative_400, 345 - }, 346 - ]}> 347 - <Text 348 style={[ 349 - a.italic, 350 - {color: t.palette.white}, 351 - native({marginTop: 2}), 352 ]}> 353 - {error} 354 - </Text> 355 - </View> 356 - )} 357 - </View> 358 359 - <Divider /> 360 - 361 - <View style={[a.pt_2xl]}> 362 - <Text 363 - style={[ 364 - a.text_md, 365 - a.font_bold, 366 - a.pb_md, 367 - t.atoms.text_contrast_high, 368 - ]}> 369 - <Trans>Your muted words</Trans> 370 - </Text> 371 - 372 - {isPreferencesLoading ? ( 373 - <Loader /> 374 - ) : preferencesError || !preferences ? ( 375 - <View 376 - style={[a.py_md, a.px_lg, a.rounded_md, t.atoms.bg_contrast_25]}> 377 - <Text style={[a.italic, t.atoms.text_contrast_high]}> 378 - <Trans> 379 - We're sorry, but we weren't able to load your muted words at 380 - this time. Please try again. 381 - </Trans> 382 - </Text> 383 - </View> 384 - ) : preferences.moderationPrefs.mutedWords.length ? ( 385 - [...preferences.moderationPrefs.mutedWords] 386 - .reverse() 387 - .map((word, i) => ( 388 - <MutedWordRow 389 - key={word.value + i} 390 - word={word} 391 - style={[i % 2 === 0 && t.atoms.bg_contrast_25]} 392 - /> 393 - )) 394 - ) : ( 395 - <View 396 - style={[a.py_md, a.px_lg, a.rounded_md, t.atoms.bg_contrast_25]}> 397 - <Text style={[a.italic, t.atoms.text_contrast_high]}> 398 - <Trans>You haven't muted any words or tags yet</Trans> 399 - </Text> 400 - </View> 401 - )} 402 </View> 403 404 - {isNative && <View style={{height: 20}} />} 405 - </View> 406 407 - <Dialog.Close /> 408 - </Dialog.ScrollableInner> 409 ) 410 } 411 ··· 437 onConfirm={remove} 438 confirmButtonCta={_(msg`Remove`)} 439 confirmButtonColor="negative" 440 /> 441 442 <View
··· 30 import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' 31 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' 32 import {Loader} from '#/components/Loader' 33 + import {createPortalGroup} from '#/components/Portal' 34 import * as Prompt from '#/components/Prompt' 35 import {Text} from '#/components/Typography' 36 37 const ONE_DAY = 24 * 60 * 60 * 1000 38 + 39 + const Portal = createPortalGroup() 40 41 export function MutedWordsDialog() { 42 const {mutedWordsDialogControl: control} = useGlobalDialogsControlContext() ··· 108 }, [_, field, targets, addMutedWord, setField, durations, excludeFollowing]) 109 110 return ( 111 + <Portal.Provider> 112 + <Dialog.ScrollableInner label={_(msg`Manage your muted words and tags`)}> 113 + <View> 114 + <Text 115 + style={[ 116 + a.text_md, 117 + a.font_bold, 118 + a.pb_sm, 119 + t.atoms.text_contrast_high, 120 + ]}> 121 + <Trans>Add muted words and tags</Trans> 122 + </Text> 123 + <Text style={[a.pb_lg, a.leading_snug, t.atoms.text_contrast_medium]}> 124 + <Trans> 125 + Posts can be muted based on their text, their tags, or both. We 126 + recommend avoiding common words that appear in many posts, since 127 + it can result in no posts being shown. 128 + </Trans> 129 + </Text> 130 131 + <View style={[a.pb_sm]}> 132 + <Dialog.Input 133 + autoCorrect={false} 134 + autoCapitalize="none" 135 + autoComplete="off" 136 + label={_(msg`Enter a word or tag`)} 137 + placeholder={_(msg`Enter a word or tag`)} 138 + value={field} 139 + onChangeText={value => { 140 + if (error) { 141 + setError('') 142 + } 143 + setField(value) 144 + }} 145 + onSubmitEditing={submit} 146 + /> 147 + </View> 148 149 + <View style={[a.pb_xl, a.gap_sm]}> 150 + <Toggle.Group 151 + label={_(msg`Select how long to mute this word for.`)} 152 + type="radio" 153 + values={durations} 154 + onChange={setDurations}> 155 + <Text 156 + style={[ 157 + a.pb_xs, 158 + a.text_sm, 159 + a.font_bold, 160 + t.atoms.text_contrast_medium, 161 + ]}> 162 + <Trans>Duration:</Trans> 163 + </Text> 164 165 <View 166 style={[ 167 + gtMobile && [a.flex_row, a.align_center, a.justify_start], 168 a.gap_sm, 169 ]}> 170 + <View 171 + style={[ 172 + a.flex_1, 173 + a.flex_row, 174 + a.justify_start, 175 + a.align_center, 176 + a.gap_sm, 177 + ]}> 178 + <Toggle.Item 179 + label={_(msg`Mute this word until you unmute it`)} 180 + name="forever" 181 + style={[a.flex_1]}> 182 + <TargetToggle> 183 + <View 184 + style={[ 185 + a.flex_1, 186 + a.flex_row, 187 + a.align_center, 188 + a.gap_sm, 189 + ]}> 190 + <Toggle.Radio /> 191 + <Toggle.LabelText style={[a.flex_1, a.leading_tight]}> 192 + <Trans>Forever</Trans> 193 + </Toggle.LabelText> 194 + </View> 195 + </TargetToggle> 196 + </Toggle.Item> 197 + 198 + <Toggle.Item 199 + label={_(msg`Mute this word for 24 hours`)} 200 + name="24_hours" 201 + style={[a.flex_1]}> 202 + <TargetToggle> 203 + <View 204 + style={[ 205 + a.flex_1, 206 + a.flex_row, 207 + a.align_center, 208 + a.gap_sm, 209 + ]}> 210 + <Toggle.Radio /> 211 + <Toggle.LabelText style={[a.flex_1, a.leading_tight]}> 212 + <Trans>24 hours</Trans> 213 + </Toggle.LabelText> 214 + </View> 215 + </TargetToggle> 216 + </Toggle.Item> 217 + </View> 218 + 219 + <View 220 + style={[ 221 + a.flex_1, 222 + a.flex_row, 223 + a.justify_start, 224 + a.align_center, 225 + a.gap_sm, 226 + ]}> 227 + <Toggle.Item 228 + label={_(msg`Mute this word for 7 days`)} 229 + name="7_days" 230 + style={[a.flex_1]}> 231 + <TargetToggle> 232 + <View 233 + style={[ 234 + a.flex_1, 235 + a.flex_row, 236 + a.align_center, 237 + a.gap_sm, 238 + ]}> 239 + <Toggle.Radio /> 240 + <Toggle.LabelText style={[a.flex_1, a.leading_tight]}> 241 + <Trans>7 days</Trans> 242 + </Toggle.LabelText> 243 + </View> 244 + </TargetToggle> 245 + </Toggle.Item> 246 247 + <Toggle.Item 248 + label={_(msg`Mute this word for 30 days`)} 249 + name="30_days" 250 + style={[a.flex_1]}> 251 + <TargetToggle> 252 + <View 253 + style={[ 254 + a.flex_1, 255 + a.flex_row, 256 + a.align_center, 257 + a.gap_sm, 258 + ]}> 259 + <Toggle.Radio /> 260 + <Toggle.LabelText style={[a.flex_1, a.leading_tight]}> 261 + <Trans>30 days</Trans> 262 + </Toggle.LabelText> 263 + </View> 264 + </TargetToggle> 265 + </Toggle.Item> 266 + </View> 267 </View> 268 + </Toggle.Group> 269 270 + <Toggle.Group 271 + label={_( 272 + msg`Select what content this mute word should apply to.`, 273 + )} 274 + type="radio" 275 + values={targets} 276 + onChange={setTargets}> 277 + <Text 278 style={[ 279 + a.pb_xs, 280 + a.text_sm, 281 + a.font_bold, 282 + t.atoms.text_contrast_medium, 283 ]}> 284 + <Trans>Mute in:</Trans> 285 + </Text> 286 + 287 + <View style={[a.flex_row, a.align_center, a.gap_sm, a.flex_wrap]}> 288 <Toggle.Item 289 + label={_(msg`Mute this word in post text and tags`)} 290 + name="content" 291 style={[a.flex_1]}> 292 <TargetToggle> 293 <View 294 style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}> 295 <Toggle.Radio /> 296 <Toggle.LabelText style={[a.flex_1, a.leading_tight]}> 297 + <Trans>Text & tags</Trans> 298 </Toggle.LabelText> 299 </View> 300 + <PageText size="sm" /> 301 </TargetToggle> 302 </Toggle.Item> 303 304 <Toggle.Item 305 + label={_(msg`Mute this word in tags only`)} 306 + name="tag" 307 style={[a.flex_1]}> 308 <TargetToggle> 309 <View 310 style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}> 311 <Toggle.Radio /> 312 <Toggle.LabelText style={[a.flex_1, a.leading_tight]}> 313 + <Trans>Tags only</Trans> 314 </Toggle.LabelText> 315 </View> 316 + <Hashtag size="sm" /> 317 </TargetToggle> 318 </Toggle.Item> 319 </View> 320 + </Toggle.Group> 321 322 + <View> 323 + <Text 324 + style={[ 325 + a.pb_xs, 326 + a.text_sm, 327 + a.font_bold, 328 + t.atoms.text_contrast_medium, 329 + ]}> 330 + <Trans>Options:</Trans> 331 + </Text> 332 <Toggle.Item 333 + label={_(msg`Do not apply this mute word to users you follow`)} 334 + name="exclude_following" 335 + style={[a.flex_row, a.justify_between]} 336 + value={excludeFollowing} 337 + onChange={setExcludeFollowing}> 338 <TargetToggle> 339 <View 340 style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}> 341 + <Toggle.Checkbox /> 342 <Toggle.LabelText style={[a.flex_1, a.leading_tight]}> 343 + <Trans>Exclude users you follow</Trans> 344 </Toggle.LabelText> 345 </View> 346 </TargetToggle> 347 </Toggle.Item> 348 + </View> 349 350 + <View style={[a.pt_xs]}> 351 + <Button 352 + disabled={isPending || !field} 353 + label={_(msg`Add mute word for configured settings`)} 354 + size="large" 355 + color="primary" 356 + variant="solid" 357 + style={[]} 358 + onPress={submit}> 359 + <ButtonText> 360 + <Trans>Add</Trans> 361 + </ButtonText> 362 + <ButtonIcon icon={isPending ? Loader : Plus} position="right" /> 363 + </Button> 364 </View> 365 366 + {error && ( 367 + <View 368 + style={[ 369 + a.mb_lg, 370 + a.flex_row, 371 + a.rounded_sm, 372 + a.p_md, 373 + a.mb_xs, 374 + t.atoms.bg_contrast_25, 375 + { 376 + backgroundColor: t.palette.negative_400, 377 + }, 378 + ]}> 379 + <Text 380 + style={[ 381 + a.italic, 382 + {color: t.palette.white}, 383 + native({marginTop: 2}), 384 + ]}> 385 + {error} 386 + </Text> 387 + </View> 388 + )} 389 + </View> 390 + 391 + <Divider /> 392 + 393 + <View style={[a.pt_2xl]}> 394 <Text 395 style={[ 396 + a.text_md, 397 a.font_bold, 398 + a.pb_md, 399 + t.atoms.text_contrast_high, 400 ]}> 401 + <Trans>Your muted words</Trans> 402 </Text> 403 404 + {isPreferencesLoading ? ( 405 + <Loader /> 406 + ) : preferencesError || !preferences ? ( 407 + <View 408 style={[ 409 + a.py_md, 410 + a.px_lg, 411 + a.rounded_md, 412 + t.atoms.bg_contrast_25, 413 + ]}> 414 + <Text style={[a.italic, t.atoms.text_contrast_high]}> 415 + <Trans> 416 + We're sorry, but we weren't able to load your muted words at 417 + this time. Please try again. 418 + </Trans> 419 + </Text> 420 + </View> 421 + ) : preferences.moderationPrefs.mutedWords.length ? ( 422 + [...preferences.moderationPrefs.mutedWords] 423 + .reverse() 424 + .map((word, i) => ( 425 + <MutedWordRow 426 + key={word.value + i} 427 + word={word} 428 + style={[i % 2 === 0 && t.atoms.bg_contrast_25]} 429 + /> 430 + )) 431 + ) : ( 432 + <View 433 + style={[ 434 + a.py_md, 435 + a.px_lg, 436 + a.rounded_md, 437 + t.atoms.bg_contrast_25, 438 ]}> 439 + <Text style={[a.italic, t.atoms.text_contrast_high]}> 440 + <Trans>You haven't muted any words or tags yet</Trans> 441 + </Text> 442 + </View> 443 + )} 444 + </View> 445 446 + {isNative && <View style={{height: 20}} />} 447 </View> 448 449 + <Dialog.Close /> 450 + </Dialog.ScrollableInner> 451 452 + <Portal.Outlet /> 453 + </Portal.Provider> 454 ) 455 } 456 ··· 482 onConfirm={remove} 483 confirmButtonCta={_(msg`Remove`)} 484 confirmButtonColor="negative" 485 + Portal={Portal.Portal} 486 /> 487 488 <View
+4 -3
src/components/dialogs/PostInteractionSettingsDialog.tsx
··· 37 import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' 38 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' 39 import {Loader} from '#/components/Loader' 40 import {Text} from '#/components/Typography' 41 42 export type PostInteractionSettingsFormProps = { ··· 54 55 export function PostInteractionSettingsControlledDialog({ 56 control, 57 ...rest 58 }: PostInteractionSettingsFormProps & { 59 control: Dialog.DialogControlProps 60 }) { 61 const {_} = useLingui() 62 return ( 63 - <Dialog.Outer control={control}> 64 <Dialog.Handle /> 65 <Dialog.ScrollableInner 66 label={_(msg`Edit post interaction settings`)} ··· 231 }: PostInteractionSettingsFormProps) { 232 const t = useTheme() 233 const {_} = useLingui() 234 - const control = Dialog.useDialogContext() 235 const {data: lists} = useMyListsQuery('curate') 236 const [quotesEnabled, setQuotesEnabled] = React.useState( 237 !( ··· 437 <Button 438 label={_(msg`Save`)} 439 onPress={onSave} 440 - onAccessibilityEscape={control.close} 441 color="primary" 442 size="large" 443 variant="solid"
··· 37 import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' 38 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' 39 import {Loader} from '#/components/Loader' 40 + import {PortalComponent} from '#/components/Portal' 41 import {Text} from '#/components/Typography' 42 43 export type PostInteractionSettingsFormProps = { ··· 55 56 export function PostInteractionSettingsControlledDialog({ 57 control, 58 + Portal, 59 ...rest 60 }: PostInteractionSettingsFormProps & { 61 control: Dialog.DialogControlProps 62 + Portal?: PortalComponent 63 }) { 64 const {_} = useLingui() 65 return ( 66 + <Dialog.Outer control={control} Portal={Portal}> 67 <Dialog.Handle /> 68 <Dialog.ScrollableInner 69 label={_(msg`Edit post interaction settings`)} ··· 234 }: PostInteractionSettingsFormProps) { 235 const t = useTheme() 236 const {_} = useLingui() 237 const {data: lists} = useMyListsQuery('curate') 238 const [quotesEnabled, setQuotesEnabled] = React.useState( 239 !( ··· 439 <Button 440 label={_(msg`Save`)} 441 onPress={onSave} 442 color="primary" 443 size="large" 444 variant="solid"
-1
src/components/dialogs/SwitchAccount.tsx
··· 43 return ( 44 <Dialog.Outer control={control}> 45 <Dialog.Handle /> 46 - 47 <Dialog.ScrollableInner label={_(msg`Switch Account`)}> 48 <View style={[a.gap_lg]}> 49 <Text style={[a.text_2xl, a.font_bold]}>
··· 43 return ( 44 <Dialog.Outer control={control}> 45 <Dialog.Handle /> 46 <Dialog.ScrollableInner label={_(msg`Switch Account`)}> 47 <View style={[a.gap_lg]}> 48 <Text style={[a.text_2xl, a.font_bold]}>
-1
src/components/dialogs/nuxs/NeueTypography.tsx
··· 44 return ( 45 <Dialog.Outer control={control} onClose={onClose}> 46 <Dialog.Handle /> 47 - 48 <Dialog.ScrollableInner label={_(msg`Introducing new font settings`)}> 49 <View style={[a.gap_xl]}> 50 <View style={[a.gap_md]}>
··· 44 return ( 45 <Dialog.Outer control={control} onClose={onClose}> 46 <Dialog.Handle /> 47 <Dialog.ScrollableInner label={_(msg`Introducing new font settings`)}> 48 <View style={[a.gap_xl]}> 49 <View style={[a.gap_md]}>
+3 -3
src/components/dms/ConvoMenu.tsx
··· 136 <Menu.Outer> 137 <Menu.Item 138 label={_(msg`Leave conversation`)} 139 - onPress={leaveConvoControl.open}> 140 <Menu.ItemText> 141 <Trans>Leave conversation</Trans> 142 </Menu.ItemText> ··· 195 </Menu.Item> 196 <Menu.Item 197 label={_(msg`Report conversation`)} 198 - onPress={reportControl.open}> 199 <Menu.ItemText> 200 <Trans>Report conversation</Trans> 201 </Menu.ItemText> ··· 206 <Menu.Group> 207 <Menu.Item 208 label={_(msg`Leave conversation`)} 209 - onPress={leaveConvoControl.open}> 210 <Menu.ItemText> 211 <Trans>Leave conversation</Trans> 212 </Menu.ItemText>
··· 136 <Menu.Outer> 137 <Menu.Item 138 label={_(msg`Leave conversation`)} 139 + onPress={() => leaveConvoControl.open()}> 140 <Menu.ItemText> 141 <Trans>Leave conversation</Trans> 142 </Menu.ItemText> ··· 195 </Menu.Item> 196 <Menu.Item 197 label={_(msg`Report conversation`)} 198 + onPress={() => reportControl.open()}> 199 <Menu.ItemText> 200 <Trans>Report conversation</Trans> 201 </Menu.ItemText> ··· 206 <Menu.Group> 207 <Menu.Item 208 label={_(msg`Leave conversation`)} 209 + onPress={() => leaveConvoControl.open()}> 210 <Menu.ItemText> 211 <Trans>Leave conversation</Trans> 212 </Menu.ItemText>
+5 -5
src/components/dms/MessageMenu.tsx
··· 7 8 import {richTextToString} from '#/lib/strings/rich-text-helpers' 9 import {getTranslatorLink} from '#/locale/helpers' 10 import {useLanguagePrefs} from '#/state/preferences' 11 import {useOpenLink} from '#/state/preferences/in-app-browser' 12 - import {isWeb} from 'platform/detection' 13 - import {useConvoActive} from 'state/messages/convo' 14 - import {useSession} from 'state/session' 15 import * as Toast from '#/view/com/util/Toast' 16 import {atoms as a, useTheme} from '#/alf' 17 import {ReportDialog} from '#/components/dms/ReportDialog' ··· 120 <Menu.Item 121 testID="messageDropdownDeleteBtn" 122 label={_(msg`Delete message for me`)} 123 - onPress={deleteControl.open}> 124 <Menu.ItemText>{_(msg`Delete for me`)}</Menu.ItemText> 125 <Menu.ItemIcon icon={Trash} position="right" /> 126 </Menu.Item> ··· 128 <Menu.Item 129 testID="messageDropdownReportBtn" 130 label={_(msg`Report message`)} 131 - onPress={reportControl.open}> 132 <Menu.ItemText>{_(msg`Report`)}</Menu.ItemText> 133 <Menu.ItemIcon icon={Warning} position="right" /> 134 </Menu.Item>
··· 7 8 import {richTextToString} from '#/lib/strings/rich-text-helpers' 9 import {getTranslatorLink} from '#/locale/helpers' 10 + import {isWeb} from '#/platform/detection' 11 + import {useConvoActive} from '#/state/messages/convo' 12 import {useLanguagePrefs} from '#/state/preferences' 13 import {useOpenLink} from '#/state/preferences/in-app-browser' 14 + import {useSession} from '#/state/session' 15 import * as Toast from '#/view/com/util/Toast' 16 import {atoms as a, useTheme} from '#/alf' 17 import {ReportDialog} from '#/components/dms/ReportDialog' ··· 120 <Menu.Item 121 testID="messageDropdownDeleteBtn" 122 label={_(msg`Delete message for me`)} 123 + onPress={() => deleteControl.open()}> 124 <Menu.ItemText>{_(msg`Delete for me`)}</Menu.ItemText> 125 <Menu.ItemIcon icon={Trash} position="right" /> 126 </Menu.Item> ··· 128 <Menu.Item 129 testID="messageDropdownReportBtn" 130 label={_(msg`Report message`)} 131 + onPress={() => reportControl.open()}> 132 <Menu.ItemText>{_(msg`Report`)}</Menu.ItemText> 133 <Menu.ItemIcon icon={Warning} position="right" /> 134 </Menu.Item>
+1 -6
src/components/dms/ReportDialog.tsx
··· 10 import {useMutation} from '@tanstack/react-query' 11 12 import {ReportOption} from '#/lib/moderation/useReportOptions' 13 - import {isAndroid} from '#/platform/detection' 14 import {useAgent} from '#/state/session' 15 import {CharProgress} from '#/view/com/composer/char-progress/CharProgress' 16 import * as Toast from '#/view/com/util/Toast' 17 import {atoms as a, useBreakpoints, useTheme} from '#/alf' 18 import * as Dialog from '#/components/Dialog' 19 - import {KeyboardControllerPadding} from '#/components/KeyboardControllerPadding' 20 import {Button, ButtonIcon, ButtonText} from '../Button' 21 import {Divider} from '../Divider' 22 import {ChevronLeft_Stroke2_Corner0_Rounded as Chevron} from '../icons/Chevron' ··· 41 }): React.ReactNode => { 42 const {_} = useLingui() 43 return ( 44 - <Dialog.Outer 45 - control={control} 46 - nativeOptions={isAndroid ? {sheet: {snapPoints: ['100%']}} : {}}> 47 <Dialog.Handle /> 48 <Dialog.ScrollableInner label={_(msg`Report this message`)}> 49 <DialogInner params={params} /> 50 <Dialog.Close /> 51 - <KeyboardControllerPadding /> 52 </Dialog.ScrollableInner> 53 </Dialog.Outer> 54 )
··· 10 import {useMutation} from '@tanstack/react-query' 11 12 import {ReportOption} from '#/lib/moderation/useReportOptions' 13 import {useAgent} from '#/state/session' 14 import {CharProgress} from '#/view/com/composer/char-progress/CharProgress' 15 import * as Toast from '#/view/com/util/Toast' 16 import {atoms as a, useBreakpoints, useTheme} from '#/alf' 17 import * as Dialog from '#/components/Dialog' 18 import {Button, ButtonIcon, ButtonText} from '../Button' 19 import {Divider} from '../Divider' 20 import {ChevronLeft_Stroke2_Corner0_Rounded as Chevron} from '../icons/Chevron' ··· 39 }): React.ReactNode => { 40 const {_} = useLingui() 41 return ( 42 + <Dialog.Outer control={control}> 43 <Dialog.Handle /> 44 <Dialog.ScrollableInner label={_(msg`Report this message`)}> 45 <DialogInner params={params} /> 46 <Dialog.Close /> 47 </Dialog.ScrollableInner> 48 </Dialog.Outer> 49 )
+3 -5
src/components/dms/dialogs/NewChatDialog.tsx
··· 2 import {msg} from '@lingui/macro' 3 import {useLingui} from '@lingui/react' 4 5 import {logger} from '#/logger' 6 import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members' 7 - import {logEvent} from 'lib/statsig/statsig' 8 import {FAB} from '#/view/com/util/fab/FAB' 9 import * as Toast from '#/view/com/util/Toast' 10 import {useTheme} from '#/alf' ··· 55 accessibilityHint="" 56 /> 57 58 - <Dialog.Outer 59 - control={control} 60 - testID="newChatDialog" 61 - nativeOptions={{sheet: {snapPoints: ['100%']}}}> 62 <SearchablePeopleList 63 title={_(msg`Start a new chat`)} 64 onSelectChat={onCreateChat}
··· 2 import {msg} from '@lingui/macro' 3 import {useLingui} from '@lingui/react' 4 5 + import {logEvent} from '#/lib/statsig/statsig' 6 import {logger} from '#/logger' 7 import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members' 8 import {FAB} from '#/view/com/util/fab/FAB' 9 import * as Toast from '#/view/com/util/Toast' 10 import {useTheme} from '#/alf' ··· 55 accessibilityHint="" 56 /> 57 58 + <Dialog.Outer control={control} testID="newChatDialog"> 59 + <Dialog.Handle /> 60 <SearchablePeopleList 61 title={_(msg`Start a new chat`)} 62 onSelectChat={onCreateChat}
+40 -61
src/components/dms/dialogs/SearchablePeopleList.tsx
··· 5 useRef, 6 useState, 7 } from 'react' 8 - import type {TextInput as TextInputType} from 'react-native' 9 - import {View} from 'react-native' 10 import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api' 11 - import {BottomSheetFlatListMethods} from '@discord/bottom-sheet' 12 import {msg, Trans} from '@lingui/macro' 13 import {useLingui} from '@lingui/react' 14 ··· 16 import {sanitizeHandle} from '#/lib/strings/handles' 17 import {isWeb} from '#/platform/detection' 18 import {useModerationOpts} from '#/state/preferences/moderation-opts' 19 import {useListConvosQuery} from '#/state/queries/messages/list-converations' 20 import {useProfileFollowsQuery} from '#/state/queries/profile-follows' 21 import {useSession} from '#/state/session' 22 - import {useActorAutocompleteQuery} from 'state/queries/actor-autocomplete' 23 import {UserAvatar} from '#/view/com/util/UserAvatar' 24 import {atoms as a, native, useTheme, web} from '#/alf' 25 - import {Button} from '#/components/Button' 26 import * as Dialog from '#/components/Dialog' 27 - import {TextInput} from '#/components/dms/dialogs/TextInput' 28 import {canBeMessaged} from '#/components/dms/util' 29 import {useInteractionState} from '#/components/hooks/useInteractionState' 30 - import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron' 31 import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2' 32 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' 33 import {Text} from '#/components/Typography' ··· 66 const {_} = useLingui() 67 const moderationOpts = useModerationOpts() 68 const control = Dialog.useDialogContext() 69 - const listRef = useRef<BottomSheetFlatListMethods>(null) 70 const {currentAccount} = useSession() 71 - const inputRef = useRef<TextInputType>(null) 72 73 const [searchText, setSearchText] = useState('') 74 ··· 101 }) 102 } 103 104 - _items = _items.sort(a => { 105 // @ts-ignore 106 - return a.enabled ? -1 : 1 107 }) 108 } 109 } else { 110 const placeholders: Item[] = Array(10) 111 .fill(0) 112 - .map((_, i) => ({ 113 type: 'placeholder', 114 key: i + '', 115 })) ··· 155 } 156 157 // only sort follows 158 - followsItems = followsItems.sort(a => { 159 // @ts-ignore 160 - return a.enabled ? -1 : 1 161 }) 162 163 // then append ··· 177 } 178 } 179 180 - _items = _items.sort(a => { 181 // @ts-ignore 182 - return a.enabled ? -1 : 1 183 }) 184 } else { 185 _items.push(...placeholders) ··· 242 <View 243 style={[ 244 a.relative, 245 - a.pt_md, 246 a.pb_xs, 247 a.px_lg, 248 a.border_b, 249 t.atoms.border_contrast_low, 250 t.atoms.bg, 251 - native([a.pt_lg]), 252 ]}> 253 - <View 254 - style={[ 255 - a.relative, 256 - native(a.align_center), 257 - a.justify_center, 258 - {height: 32}, 259 - ]}> 260 - <Button 261 - label={_(msg`Close`)} 262 - size="small" 263 - shape="round" 264 - variant="ghost" 265 - color="secondary" 266 - style={[ 267 - a.absolute, 268 - a.z_20, 269 - native({ 270 - left: -7, 271 - }), 272 - web({ 273 - right: -4, 274 - }), 275 - ]} 276 - onPress={() => control.close()}> 277 - {isWeb ? ( 278 - <X size="md" fill={t.palette.contrast_500} /> 279 - ) : ( 280 - <ChevronLeft size="md" fill={t.palette.contrast_500} /> 281 - )} 282 - </Button> 283 <Text 284 style={[ 285 a.z_10, 286 a.text_lg, 287 - a.font_bold, 288 a.leading_tight, 289 t.atoms.text_contrast_high, 290 ]}> 291 {title} 292 </Text> 293 </View> 294 295 - <View style={[native([a.pt_sm]), web([a.pt_xs])]}> 296 <SearchInput 297 inputRef={inputRef} 298 value={searchText} ··· 309 t.atoms.border_contrast_low, 310 t.atoms.bg, 311 t.atoms.text_contrast_high, 312 - t.palette.contrast_500, 313 _, 314 title, 315 searchText, ··· 326 keyExtractor={(item: Item) => item.key} 327 style={[ 328 web([a.py_0, {height: '100vh', maxHeight: 600}, a.px_0]), 329 - native({ 330 - height: '100%', 331 - paddingHorizontal: 0, 332 - marginTop: 0, 333 - paddingTop: 0, 334 - borderTopLeftRadius: 40, 335 - borderTopRightRadius: 40, 336 - }), 337 ]} 338 webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]} 339 keyboardDismissMode="on-drag" ··· 396 <View style={[a.flex_1, a.gap_2xs]}> 397 <Text 398 style={[t.atoms.text, a.font_bold, a.leading_tight, a.self_start]} 399 - numberOfLines={1}> 400 {displayName} 401 </Text> 402 <Text ··· 474 value: string 475 onChangeText: (text: string) => void 476 onEscape: () => void 477 - inputRef: React.RefObject<TextInputType> 478 }) { 479 const t = useTheme() 480 const {_} = useLingui()
··· 5 useRef, 6 useState, 7 } from 'react' 8 + import {TextInput, View} from 'react-native' 9 import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api' 10 import {msg, Trans} from '@lingui/macro' 11 import {useLingui} from '@lingui/react' 12 ··· 14 import {sanitizeHandle} from '#/lib/strings/handles' 15 import {isWeb} from '#/platform/detection' 16 import {useModerationOpts} from '#/state/preferences/moderation-opts' 17 + import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete' 18 import {useListConvosQuery} from '#/state/queries/messages/list-converations' 19 import {useProfileFollowsQuery} from '#/state/queries/profile-follows' 20 import {useSession} from '#/state/session' 21 + import {ListMethods} from '#/view/com/util/List' 22 import {UserAvatar} from '#/view/com/util/UserAvatar' 23 import {atoms as a, native, useTheme, web} from '#/alf' 24 + import {Button, ButtonIcon} from '#/components/Button' 25 import * as Dialog from '#/components/Dialog' 26 import {canBeMessaged} from '#/components/dms/util' 27 import {useInteractionState} from '#/components/hooks/useInteractionState' 28 import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2' 29 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' 30 import {Text} from '#/components/Typography' ··· 63 const {_} = useLingui() 64 const moderationOpts = useModerationOpts() 65 const control = Dialog.useDialogContext() 66 + const listRef = useRef<ListMethods>(null) 67 const {currentAccount} = useSession() 68 + const inputRef = useRef<TextInput>(null) 69 70 const [searchText, setSearchText] = useState('') 71 ··· 98 }) 99 } 100 101 + _items = _items.sort(item => { 102 // @ts-ignore 103 + return item.enabled ? -1 : 1 104 }) 105 } 106 } else { 107 const placeholders: Item[] = Array(10) 108 .fill(0) 109 + .map((__, i) => ({ 110 type: 'placeholder', 111 key: i + '', 112 })) ··· 152 } 153 154 // only sort follows 155 + followsItems = followsItems.sort(item => { 156 // @ts-ignore 157 + return item.enabled ? -1 : 1 158 }) 159 160 // then append ··· 174 } 175 } 176 177 + _items = _items.sort(item => { 178 // @ts-ignore 179 + return item.enabled ? -1 : 1 180 }) 181 } else { 182 _items.push(...placeholders) ··· 239 <View 240 style={[ 241 a.relative, 242 + web(a.pt_lg), 243 + native(a.pt_4xl), 244 a.pb_xs, 245 a.px_lg, 246 a.border_b, 247 t.atoms.border_contrast_low, 248 t.atoms.bg, 249 ]}> 250 + <View style={[a.relative, native(a.align_center), a.justify_center]}> 251 <Text 252 style={[ 253 a.z_10, 254 a.text_lg, 255 + a.font_heavy, 256 a.leading_tight, 257 t.atoms.text_contrast_high, 258 ]}> 259 {title} 260 </Text> 261 + {isWeb ? ( 262 + <Button 263 + label={_(msg`Close`)} 264 + size="small" 265 + shape="round" 266 + variant={isWeb ? 'ghost' : 'solid'} 267 + color="secondary" 268 + style={[ 269 + a.absolute, 270 + a.z_20, 271 + web({right: -4}), 272 + native({right: 0}), 273 + native({height: 32, width: 32, borderRadius: 16}), 274 + ]} 275 + onPress={() => control.close()}> 276 + <ButtonIcon icon={X} size="md" /> 277 + </Button> 278 + ) : null} 279 </View> 280 281 + <View style={[, web([a.pt_xs])]}> 282 <SearchInput 283 inputRef={inputRef} 284 value={searchText} ··· 295 t.atoms.border_contrast_low, 296 t.atoms.bg, 297 t.atoms.text_contrast_high, 298 _, 299 title, 300 searchText, ··· 311 keyExtractor={(item: Item) => item.key} 312 style={[ 313 web([a.py_0, {height: '100vh', maxHeight: 600}, a.px_0]), 314 + native({height: '100%'}), 315 ]} 316 webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]} 317 keyboardDismissMode="on-drag" ··· 374 <View style={[a.flex_1, a.gap_2xs]}> 375 <Text 376 style={[t.atoms.text, a.font_bold, a.leading_tight, a.self_start]} 377 + numberOfLines={1} 378 + emoji> 379 {displayName} 380 </Text> 381 <Text ··· 453 value: string 454 onChangeText: (text: string) => void 455 onEscape: () => void 456 + inputRef: React.RefObject<TextInput> 457 }) { 458 const t = useTheme() 459 const {_} = useLingui()
+3 -5
src/components/dms/dialogs/ShareViaChatDialog.tsx
··· 2 import {msg} from '@lingui/macro' 3 import {useLingui} from '@lingui/react' 4 5 import {logger} from '#/logger' 6 import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members' 7 - import {logEvent} from 'lib/statsig/statsig' 8 import * as Toast from '#/view/com/util/Toast' 9 import * as Dialog from '#/components/Dialog' 10 import {SearchablePeopleList} from './SearchablePeopleList' ··· 17 onSelectChat: (chatId: string) => void 18 }) { 19 return ( 20 - <Dialog.Outer 21 - control={control} 22 - testID="sendViaChatChatDialog" 23 - nativeOptions={{sheet: {snapPoints: ['100%']}}}> 24 <SendViaChatDialogInner control={control} onSelectChat={onSelectChat} /> 25 </Dialog.Outer> 26 )
··· 2 import {msg} from '@lingui/macro' 3 import {useLingui} from '@lingui/react' 4 5 + import {logEvent} from '#/lib/statsig/statsig' 6 import {logger} from '#/logger' 7 import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members' 8 import * as Toast from '#/view/com/util/Toast' 9 import * as Dialog from '#/components/Dialog' 10 import {SearchablePeopleList} from './SearchablePeopleList' ··· 17 onSelectChat: (chatId: string) => void 18 }) { 19 return ( 20 + <Dialog.Outer control={control} testID="sendViaChatChatDialog"> 21 + <Dialog.Handle /> 22 <SendViaChatDialogInner control={control} onSelectChat={onSelectChat} /> 23 </Dialog.Outer> 24 )
+1 -1
src/components/forms/Toggle.tsx
··· 2 import {Pressable, View, ViewStyle} from 'react-native' 3 import Animated, {LinearTransition} from 'react-native-reanimated' 4 5 import {isNative} from '#/platform/detection' 6 - import {HITSLOP_10} from 'lib/constants' 7 import { 8 atoms as a, 9 flatten,
··· 2 import {Pressable, View, ViewStyle} from 'react-native' 3 import Animated, {LinearTransition} from 'react-native-reanimated' 4 5 + import {HITSLOP_10} from '#/lib/constants' 6 import {isNative} from '#/platform/detection' 7 import { 8 atoms as a, 9 flatten,
+36 -35
src/components/moderation/LabelsOnMeDialog.tsx
··· 32 return ( 33 <Dialog.Outer control={props.control}> 34 <Dialog.Handle /> 35 - 36 <LabelsOnMeDialogInner {...props} /> 37 </Dialog.Outer> 38 ) ··· 158 <Divider /> 159 160 <View style={[a.px_md, a.py_sm, t.atoms.bg_contrast_25]}> 161 - <Text style={[t.atoms.text_contrast_medium]}> 162 - {isSelfLabel ? ( 163 <Trans>This label was applied by you.</Trans> 164 - ) : ( 165 - <Trans> 166 - Source:{' '} 167 - <InlineLinkText 168 - label={sourceName} 169 - to={makeProfileLink( 170 - labeler ? labeler.creator : {did: label.src, handle: ''}, 171 - )} 172 - onPress={() => control.close()}> 173 - {sourceName} 174 - </InlineLinkText> 175 - </Trans> 176 - )} 177 - </Text> 178 </View> 179 </View> 180 ) ··· 236 237 return ( 238 <> 239 - <Text style={[a.text_2xl, a.font_bold, a.pb_xs, a.leading_tight]}> 240 - <Trans>Appeal "{strings.name}" label</Trans> 241 - </Text> 242 - <Text style={[a.text_md, a.leading_snug]}> 243 - <Trans> 244 - This appeal will be sent to{' '} 245 - <InlineLinkText 246 - label={sourceName} 247 - to={makeProfileLink( 248 - labeler ? labeler.creator : {did: label.src, handle: ''}, 249 - )} 250 - onPress={() => control.close()} 251 - style={[a.text_md, a.leading_snug]}> 252 - {sourceName} 253 - </InlineLinkText> 254 - . 255 - </Trans> 256 - </Text> 257 <View style={[a.my_md]}> 258 <Dialog.Input 259 label={_(msg`Text input field`)}
··· 32 return ( 33 <Dialog.Outer control={props.control}> 34 <Dialog.Handle /> 35 <LabelsOnMeDialogInner {...props} /> 36 </Dialog.Outer> 37 ) ··· 157 <Divider /> 158 159 <View style={[a.px_md, a.py_sm, t.atoms.bg_contrast_25]}> 160 + {isSelfLabel ? ( 161 + <Text style={[t.atoms.text_contrast_medium]}> 162 <Trans>This label was applied by you.</Trans> 163 + </Text> 164 + ) : ( 165 + <View style={{flexDirection: 'row'}}> 166 + <Text style={[t.atoms.text_contrast_medium]}> 167 + <Trans>Source: </Trans>{' '} 168 + </Text> 169 + <InlineLinkText 170 + label={sourceName} 171 + to={makeProfileLink( 172 + labeler ? labeler.creator : {did: label.src, handle: ''}, 173 + )} 174 + onPress={() => control.close()}> 175 + {sourceName} 176 + </InlineLinkText> 177 + </View> 178 + )} 179 </View> 180 </View> 181 ) ··· 237 238 return ( 239 <> 240 + <View style={{flexWrap: 'wrap', flexDirection: 'row'}}> 241 + <Text style={[a.text_2xl, a.font_bold, a.pb_xs, a.leading_tight]}> 242 + <Trans>Appeal "{strings.name}" label</Trans> 243 + </Text> 244 + <Text style={[a.text_md, a.leading_snug]}> 245 + <Trans>This appeal will be sent to</Trans>{' '} 246 + </Text> 247 + <InlineLinkText 248 + label={sourceName} 249 + to={makeProfileLink( 250 + labeler ? labeler.creator : {did: label.src, handle: ''}, 251 + )} 252 + onPress={() => control.close()} 253 + style={[a.text_md, a.leading_snug]}> 254 + {sourceName} 255 + </InlineLinkText> 256 + <Text style={[a.text_md, a.leading_snug]}>.</Text> 257 + </View> 258 <View style={[a.my_md]}> 259 <Dialog.Input 260 label={_(msg`Text input field`)}
+17 -16
src/components/moderation/ModerationDetailsDialog.tsx
··· 141 {modcause?.type === 'label' && ( 142 <View style={[a.pt_lg]}> 143 <Divider /> 144 - <Text style={[t.atoms.text, a.text_md, a.leading_snug, a.mt_lg]}> 145 - {modcause.source.type === 'user' ? ( 146 <Trans>This label was applied by the author.</Trans> 147 - ) : ( 148 - <Trans> 149 - This label was applied by{' '} 150 - <InlineLinkText 151 - label={desc.source || _(msg`an unknown labeler`)} 152 - to={makeProfileLink({did: modcause.label.src, handle: ''})} 153 - onPress={() => control.close()} 154 - style={a.text_md}> 155 - {desc.source || _(msg`an unknown labeler`)} 156 - </InlineLinkText> 157 - . 158 - </Trans> 159 - )} 160 - </Text> 161 </View> 162 )} 163
··· 141 {modcause?.type === 'label' && ( 142 <View style={[a.pt_lg]}> 143 <Divider /> 144 + {modcause.source.type === 'user' ? ( 145 + <Text style={[t.atoms.text, a.text_md, a.leading_snug, a.mt_lg]}> 146 <Trans>This label was applied by the author.</Trans> 147 + </Text> 148 + ) : ( 149 + <> 150 + <Text style={[t.atoms.text, a.text_md, a.leading_snug, a.mt_lg]}> 151 + <Trans>This label was applied by </Trans> 152 + </Text> 153 + <InlineLinkText 154 + label={desc.source || _(msg`an unknown labeler`)} 155 + to={makeProfileLink({did: modcause.label.src, handle: ''})} 156 + onPress={() => control.close()} 157 + style={a.text_md}> 158 + {desc.source || _(msg`an unknown labeler`)} 159 + </InlineLinkText> 160 + </> 161 + )} 162 </View> 163 )} 164
+1 -1
src/lib/media/video/upload.ts
··· 7 import {AbortError} from '#/lib/async/cancelable' 8 import {ServerError} from '#/lib/media/video/errors' 9 import {CompressedVideo} from '#/lib/media/video/types' 10 - import {createVideoEndpointUrl, mimeToExt} from './util' 11 import {getServiceAuthToken, getVideoUploadLimits} from './upload.shared' 12 13 export async function uploadVideo({ 14 video,
··· 7 import {AbortError} from '#/lib/async/cancelable' 8 import {ServerError} from '#/lib/media/video/errors' 9 import {CompressedVideo} from '#/lib/media/video/types' 10 import {getServiceAuthToken, getVideoUploadLimits} from './upload.shared' 11 + import {createVideoEndpointUrl, mimeToExt} from './util' 12 13 export async function uploadVideo({ 14 video,
+1 -1
src/lib/media/video/upload.web.ts
··· 7 import {AbortError} from '#/lib/async/cancelable' 8 import {ServerError} from '#/lib/media/video/errors' 9 import {CompressedVideo} from '#/lib/media/video/types' 10 - import {createVideoEndpointUrl, mimeToExt} from './util' 11 import {getServiceAuthToken, getVideoUploadLimits} from './upload.shared' 12 13 export async function uploadVideo({ 14 video,
··· 7 import {AbortError} from '#/lib/async/cancelable' 8 import {ServerError} from '#/lib/media/video/errors' 9 import {CompressedVideo} from '#/lib/media/video/types' 10 import {getServiceAuthToken, getVideoUploadLimits} from './upload.shared' 11 + import {createVideoEndpointUrl, mimeToExt} from './util' 12 13 export async function uploadVideo({ 14 video,
+24 -13
src/screens/Onboarding/StepProfile/index.tsx
··· 32 import {atoms as a, useBreakpoints, useTheme} from '#/alf' 33 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 34 import * as Dialog from '#/components/Dialog' 35 import {IconCircle} from '#/components/IconCircle' 36 import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' 37 import {CircleInfo_Stroke2_Corner0_Rounded} from '#/components/icons/CircleInfo' ··· 89 requestNotificationsPermission('StartOnboarding') 90 }, [gate, requestNotificationsPermission]) 91 92 const openPicker = React.useCallback( 93 async (opts?: ImagePickerOptions) => { 94 - const response = await launchImageLibraryAsync({ 95 - exif: false, 96 - mediaTypes: MediaTypeOptions.Images, 97 - quality: 1, 98 - ...opts, 99 - legacy: true, 100 - }) 101 102 return (response.assets ?? []) 103 .slice(0, 1) ··· 121 size: getDataUriSize(image.uri), 122 })) 123 }, 124 - [_, setError], 125 ) 126 127 const onContinue = React.useCallback(async () => { ··· 168 169 setError('') 170 171 - const items = await openPicker({ 172 - aspect: [1, 1], 173 - }) 174 let image = items[0] 175 if (!image) return 176 ··· 196 image, 197 useCreatedAvatar: false, 198 })) 199 - }, [requestPhotoAccessIfNeeded, setAvatar, openPicker, setError]) 200 201 const onSecondaryPress = React.useCallback(() => { 202 if (avatar.useCreatedAvatar) { ··· 286 </View> 287 288 <Dialog.Outer control={creatorControl}> 289 - <Dialog.Handle /> 290 <Dialog.Inner 291 label="Avatar creator" 292 style={[
··· 32 import {atoms as a, useBreakpoints, useTheme} from '#/alf' 33 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 34 import * as Dialog from '#/components/Dialog' 35 + import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper' 36 import {IconCircle} from '#/components/IconCircle' 37 import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' 38 import {CircleInfo_Stroke2_Corner0_Rounded} from '#/components/icons/CircleInfo' ··· 90 requestNotificationsPermission('StartOnboarding') 91 }, [gate, requestNotificationsPermission]) 92 93 + const sheetWrapper = useSheetWrapper() 94 const openPicker = React.useCallback( 95 async (opts?: ImagePickerOptions) => { 96 + const response = await sheetWrapper( 97 + launchImageLibraryAsync({ 98 + exif: false, 99 + mediaTypes: MediaTypeOptions.Images, 100 + quality: 1, 101 + ...opts, 102 + legacy: true, 103 + }), 104 + ) 105 106 return (response.assets ?? []) 107 .slice(0, 1) ··· 125 size: getDataUriSize(image.uri), 126 })) 127 }, 128 + [_, setError, sheetWrapper], 129 ) 130 131 const onContinue = React.useCallback(async () => { ··· 172 173 setError('') 174 175 + const items = await sheetWrapper( 176 + openPicker({ 177 + aspect: [1, 1], 178 + }), 179 + ) 180 let image = items[0] 181 if (!image) return 182 ··· 202 image, 203 useCreatedAvatar: false, 204 })) 205 + }, [ 206 + requestPhotoAccessIfNeeded, 207 + setAvatar, 208 + openPicker, 209 + setError, 210 + sheetWrapper, 211 + ]) 212 213 const onSecondaryPress = React.useCallback(() => { 214 if (avatar.useCreatedAvatar) { ··· 298 </View> 299 300 <Dialog.Outer control={creatorControl}> 301 <Dialog.Inner 302 label="Avatar creator" 303 style={[
+22 -22
src/screens/StarterPack/StarterPackScreen.tsx
··· 15 import {NativeStackScreenProps} from '@react-navigation/native-stack' 16 import {useQueryClient} from '@tanstack/react-query' 17 18 import {cleanError} from '#/lib/strings/errors' 19 import {logger} from '#/logger' 20 import {useDeleteStarterPackMutation} from '#/state/queries/starter-packs' 21 import { 22 ProgressGuideAction, 23 useProgressGuideControls, 24 } from '#/state/shell/progress-guide' 25 - import {batchedUpdates} from 'lib/batchedUpdates' 26 - import {HITSLOP_20} from 'lib/constants' 27 - import {isBlockedOrBlocking, isMuted} from 'lib/moderation/blocked-and-muted' 28 - import {makeProfileLink, makeStarterPackLink} from 'lib/routes/links' 29 - import {CommonNavigatorParams, NavigationProp} from 'lib/routes/types' 30 - import {logEvent} from 'lib/statsig/statsig' 31 - import {getStarterPackOgCard} from 'lib/strings/starter-pack' 32 - import {isWeb} from 'platform/detection' 33 - import {updateProfileShadow} from 'state/cache/profile-shadow' 34 - import {useModerationOpts} from 'state/preferences/moderation-opts' 35 - import {getAllListMembers} from 'state/queries/list-members' 36 - import {useResolvedStarterPackShortLink} from 'state/queries/resolve-short-link' 37 - import {useResolveDidQuery} from 'state/queries/resolve-uri' 38 - import {useShortenLink} from 'state/queries/shorten-link' 39 - import {useStarterPackQuery} from 'state/queries/starter-packs' 40 - import {useAgent, useSession} from 'state/session' 41 - import {useLoggedOutViewControls} from 'state/shell/logged-out' 42 - import {useSetActiveStarterPack} from 'state/shell/starter-pack' 43 import * as Toast from '#/view/com/util/Toast' 44 - import {PagerWithHeader} from 'view/com/pager/PagerWithHeader' 45 - import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader' 46 - import {CenteredView} from 'view/com/util/Views' 47 import {bulkWriteFollows} from '#/screens/Onboarding/util' 48 import {atoms as a, useBreakpoints, useTheme} from '#/alf' 49 import {Button, ButtonIcon, ButtonText} from '#/components/Button' ··· 591 592 <Menu.Item 593 label={_(msg`Report starter pack`)} 594 - onPress={reportDialogControl.open}> 595 <Menu.ItemText> 596 <Trans>Report starter pack</Trans> 597 </Menu.ItemText>
··· 15 import {NativeStackScreenProps} from '@react-navigation/native-stack' 16 import {useQueryClient} from '@tanstack/react-query' 17 18 + import {batchedUpdates} from '#/lib/batchedUpdates' 19 + import {HITSLOP_20} from '#/lib/constants' 20 + import {isBlockedOrBlocking, isMuted} from '#/lib/moderation/blocked-and-muted' 21 + import {makeProfileLink, makeStarterPackLink} from '#/lib/routes/links' 22 + import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types' 23 + import {logEvent} from '#/lib/statsig/statsig' 24 import {cleanError} from '#/lib/strings/errors' 25 + import {getStarterPackOgCard} from '#/lib/strings/starter-pack' 26 import {logger} from '#/logger' 27 + import {isWeb} from '#/platform/detection' 28 + import {updateProfileShadow} from '#/state/cache/profile-shadow' 29 + import {useModerationOpts} from '#/state/preferences/moderation-opts' 30 + import {getAllListMembers} from '#/state/queries/list-members' 31 + import {useResolvedStarterPackShortLink} from '#/state/queries/resolve-short-link' 32 + import {useResolveDidQuery} from '#/state/queries/resolve-uri' 33 + import {useShortenLink} from '#/state/queries/shorten-link' 34 import {useDeleteStarterPackMutation} from '#/state/queries/starter-packs' 35 + import {useStarterPackQuery} from '#/state/queries/starter-packs' 36 + import {useAgent, useSession} from '#/state/session' 37 + import {useLoggedOutViewControls} from '#/state/shell/logged-out' 38 import { 39 ProgressGuideAction, 40 useProgressGuideControls, 41 } from '#/state/shell/progress-guide' 42 + import {useSetActiveStarterPack} from '#/state/shell/starter-pack' 43 + import {PagerWithHeader} from '#/view/com/pager/PagerWithHeader' 44 + import {ProfileSubpageHeader} from '#/view/com/profile/ProfileSubpageHeader' 45 import * as Toast from '#/view/com/util/Toast' 46 + import {CenteredView} from '#/view/com/util/Views' 47 import {bulkWriteFollows} from '#/screens/Onboarding/util' 48 import {atoms as a, useBreakpoints, useTheme} from '#/alf' 49 import {Button, ButtonIcon, ButtonText} from '#/components/Button' ··· 591 592 <Menu.Item 593 label={_(msg`Report starter pack`)} 594 + onPress={() => reportDialogControl.open()}> 595 <Menu.ItemText> 596 <Trans>Report starter pack</Trans> 597 </Menu.ItemText>
+47 -40
src/state/dialogs/index.tsx
··· 1 import React from 'react' 2 - import {SharedValue, useSharedValue} from 'react-native-reanimated' 3 4 import {DialogControlRefProps} from '#/components/Dialog' 5 import {Provider as GlobalDialogsProvider} from '#/components/dialogs/Context' 6 7 interface IDialogContext { 8 /** ··· 16 * `useId`. 17 */ 18 openDialogs: React.MutableRefObject<Set<string>> 19 /** 20 - * The counterpart to `accessibilityViewIsModal` for Android. This property 21 - * applies to the parent of all non-modal views, and prevents TalkBack from 22 - * navigating within content beneath an open dialog. 23 - * 24 - * @see https://reactnative.dev/docs/accessibility#importantforaccessibility-android 25 */ 26 - importantForAccessibility: SharedValue<'auto' | 'no-hide-descendants'> 27 } 28 29 const DialogContext = React.createContext<IDialogContext>({} as IDialogContext) 30 31 - const DialogControlContext = React.createContext<{ 32 - closeAllDialogs(): boolean 33 - setDialogIsOpen(id: string, isOpen: boolean): void 34 - }>({ 35 - closeAllDialogs: () => false, 36 - setDialogIsOpen: () => {}, 37 - }) 38 39 export function useDialogStateContext() { 40 return React.useContext(DialogContext) ··· 45 } 46 47 export function Provider({children}: React.PropsWithChildren<{}>) { 48 const activeDialogs = React.useRef< 49 Map<string, React.MutableRefObject<DialogControlRefProps>> 50 >(new Map()) 51 const openDialogs = React.useRef<Set<string>>(new Set()) 52 - const importantForAccessibility = useSharedValue< 53 - 'auto' | 'no-hide-descendants' 54 - >('auto') 55 56 const closeAllDialogs = React.useCallback(() => { 57 - openDialogs.current.forEach(id => { 58 - const dialog = activeDialogs.current.get(id) 59 - if (dialog) dialog.current.close() 60 - }) 61 - return openDialogs.current.size > 0 62 }, []) 63 64 - const setDialogIsOpen = React.useCallback( 65 - (id: string, isOpen: boolean) => { 66 - if (isOpen) { 67 - openDialogs.current.add(id) 68 - importantForAccessibility.value = 'no-hide-descendants' 69 - } else { 70 - openDialogs.current.delete(id) 71 - if (openDialogs.current.size < 1) { 72 - importantForAccessibility.value = 'auto' 73 - } 74 - } 75 - }, 76 - [importantForAccessibility], 77 - ) 78 79 const context = React.useMemo<IDialogContext>( 80 () => ({ 81 activeDialogs, 82 openDialogs, 83 - importantForAccessibility, 84 }), 85 - [importantForAccessibility, activeDialogs, openDialogs], 86 ) 87 const controls = React.useMemo( 88 - () => ({closeAllDialogs, setDialogIsOpen}), 89 - [closeAllDialogs, setDialogIsOpen], 90 ) 91 92 return (
··· 1 import React from 'react' 2 3 + import {isWeb} from '#/platform/detection' 4 import {DialogControlRefProps} from '#/components/Dialog' 5 import {Provider as GlobalDialogsProvider} from '#/components/dialogs/Context' 6 + import {BottomSheet} from '../../../modules/bottom-sheet' 7 8 interface IDialogContext { 9 /** ··· 17 * `useId`. 18 */ 19 openDialogs: React.MutableRefObject<Set<string>> 20 + } 21 + 22 + interface IDialogControlContext { 23 + closeAllDialogs(): boolean 24 + setDialogIsOpen(id: string, isOpen: boolean): void 25 /** 26 + * The number of dialogs that are fully expanded. This is used to determine the backgground color of the status bar 27 + * on iOS. 28 */ 29 + fullyExpandedCount: number 30 + setFullyExpandedCount: React.Dispatch<React.SetStateAction<number>> 31 } 32 33 const DialogContext = React.createContext<IDialogContext>({} as IDialogContext) 34 35 + const DialogControlContext = React.createContext<IDialogControlContext>( 36 + {} as IDialogControlContext, 37 + ) 38 39 export function useDialogStateContext() { 40 return React.useContext(DialogContext) ··· 45 } 46 47 export function Provider({children}: React.PropsWithChildren<{}>) { 48 + const [fullyExpandedCount, setFullyExpandedCount] = React.useState(0) 49 + 50 const activeDialogs = React.useRef< 51 Map<string, React.MutableRefObject<DialogControlRefProps>> 52 >(new Map()) 53 const openDialogs = React.useRef<Set<string>>(new Set()) 54 55 const closeAllDialogs = React.useCallback(() => { 56 + if (isWeb) { 57 + openDialogs.current.forEach(id => { 58 + const dialog = activeDialogs.current.get(id) 59 + if (dialog) dialog.current.close() 60 + }) 61 + 62 + return openDialogs.current.size > 0 63 + } else { 64 + BottomSheet.dismissAll() 65 + return false 66 + } 67 }, []) 68 69 + const setDialogIsOpen = React.useCallback((id: string, isOpen: boolean) => { 70 + if (isOpen) { 71 + openDialogs.current.add(id) 72 + } else { 73 + openDialogs.current.delete(id) 74 + } 75 + }, []) 76 77 const context = React.useMemo<IDialogContext>( 78 () => ({ 79 activeDialogs, 80 openDialogs, 81 }), 82 + [activeDialogs, openDialogs], 83 ) 84 const controls = React.useMemo( 85 + () => ({ 86 + closeAllDialogs, 87 + setDialogIsOpen, 88 + fullyExpandedCount, 89 + setFullyExpandedCount, 90 + }), 91 + [ 92 + closeAllDialogs, 93 + setDialogIsOpen, 94 + fullyExpandedCount, 95 + setFullyExpandedCount, 96 + ], 97 ) 98 99 return (
+6 -6
src/state/preferences/in-app-browser.tsx
··· 2 import {Linking} from 'react-native' 3 import * as WebBrowser from 'expo-web-browser' 4 5 - import {isNative} from '#/platform/detection' 6 - import * as persisted from '#/state/persisted' 7 - import {usePalette} from 'lib/hooks/usePalette' 8 import { 9 createBskyAppAbsoluteUrl, 10 isBskyRSSUrl, 11 isRelativeUrl, 12 - } from 'lib/strings/url-helpers' 13 import {useModalControls} from '../modals' 14 15 type StateContext = persisted.Schema['useInAppBrowser'] ··· 62 const pal = usePalette('default') 63 64 const openLink = React.useCallback( 65 - (url: string, override?: boolean) => { 66 if (isBskyRSSUrl(url) && isRelativeUrl(url)) { 67 url = createBskyAppAbsoluteUrl(url) 68 } ··· 75 }) 76 return 77 } else if (override ?? enabled) { 78 - WebBrowser.openBrowserAsync(url, { 79 presentationStyle: 80 WebBrowser.WebBrowserPresentationStyle.FULL_SCREEN, 81 toolbarColor: pal.colors.backgroundLight,
··· 2 import {Linking} from 'react-native' 3 import * as WebBrowser from 'expo-web-browser' 4 5 + import {usePalette} from '#/lib/hooks/usePalette' 6 import { 7 createBskyAppAbsoluteUrl, 8 isBskyRSSUrl, 9 isRelativeUrl, 10 + } from '#/lib/strings/url-helpers' 11 + import {isNative} from '#/platform/detection' 12 + import * as persisted from '#/state/persisted' 13 import {useModalControls} from '../modals' 14 15 type StateContext = persisted.Schema['useInAppBrowser'] ··· 62 const pal = usePalette('default') 63 64 const openLink = React.useCallback( 65 + async (url: string, override?: boolean) => { 66 if (isBskyRSSUrl(url) && isRelativeUrl(url)) { 67 url = createBskyAppAbsoluteUrl(url) 68 } ··· 75 }) 76 return 77 } else if (override ?? enabled) { 78 + await WebBrowser.openBrowserAsync(url, { 79 presentationStyle: 80 WebBrowser.WebBrowserPresentationStyle.FULL_SCREEN, 81 toolbarColor: pal.colors.backgroundLight,
+1 -5
src/view/com/auth/server-input/index.tsx
··· 66 ]) 67 68 return ( 69 - <Dialog.Outer 70 - control={control} 71 - nativeOptions={{sheet: {snapPoints: ['100%']}}} 72 - onClose={onClose}> 73 <Dialog.Handle /> 74 - 75 <Dialog.ScrollableInner 76 accessibilityDescribedBy="dialog-description" 77 accessibilityLabelledBy="dialog-title">
··· 66 ]) 67 68 return ( 69 + <Dialog.Outer control={control} onClose={onClose}> 70 <Dialog.Handle /> 71 <Dialog.ScrollableInner 72 accessibilityDescribedBy="dialog-description" 73 accessibilityLabelledBy="dialog-title">
+294 -274
src/view/com/composer/Composer.tsx
··· 114 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' 115 import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji' 116 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' 117 import * as Prompt from '#/components/Prompt' 118 import {Text as NewText} from '#/components/Typography' 119 import {composerReducer, createComposerState} from './state/composer' 120 import {NO_VIDEO, NoVideoState, processVideo, VideoState} from './state/video' 121 122 const MAX_IMAGES = 4 123 ··· 613 const keyboardVerticalOffset = useKeyboardVerticalOffset() 614 615 return ( 616 - <KeyboardAvoidingView 617 - testID="composePostView" 618 - behavior={isIOS ? 'padding' : 'height'} 619 - keyboardVerticalOffset={keyboardVerticalOffset} 620 - style={a.flex_1}> 621 - <View style={[a.flex_1, viewStyles]} aria-modal accessibilityViewIsModal> 622 - <Animated.View 623 - style={topBarAnimatedStyle} 624 - layout={native(LinearTransition)}> 625 - <View style={styles.topbarInner}> 626 - <Button 627 - label={_(msg`Cancel`)} 628 - variant="ghost" 629 - color="primary" 630 - shape="default" 631 - size="small" 632 - style={[ 633 - a.rounded_full, 634 - a.py_sm, 635 - {paddingLeft: 7, paddingRight: 7}, 636 - ]} 637 - onPress={onPressCancel} 638 - accessibilityHint={_( 639 - msg`Closes post composer and discards post draft`, 640 - )}> 641 - <ButtonText style={[a.text_md]}> 642 - <Trans>Cancel</Trans> 643 - </ButtonText> 644 - </Button> 645 - <View style={a.flex_1} /> 646 - {isProcessing ? ( 647 - <> 648 - <Text style={pal.textLight}>{processingState}</Text> 649 - <View style={styles.postBtn}> 650 - <ActivityIndicator /> 651 </View> 652 - </> 653 - ) : ( 654 - <View style={[styles.postBtnWrapper]}> 655 - <LabelsBtn 656 - labels={labels} 657 - onChange={setLabels} 658 - hasMedia={hasMedia} 659 - /> 660 - {canPost ? ( 661 - <Button 662 - testID="composerPublishBtn" 663 - label={ 664 - replyTo ? _(msg`Publish reply`) : _(msg`Publish post`) 665 - } 666 - variant="solid" 667 - color="primary" 668 - shape="default" 669 - size="small" 670 - style={[a.rounded_full, a.py_sm]} 671 - onPress={() => onPressPublish()} 672 - disabled={videoState.status !== 'idle' && publishOnUpload}> 673 - <ButtonText style={[a.text_md]}> 674 - {replyTo ? ( 675 - <Trans context="action">Reply</Trans> 676 - ) : ( 677 - <Trans context="action">Post</Trans> 678 - )} 679 - </ButtonText> 680 - </Button> 681 - ) : ( 682 - <View style={[styles.postBtn, pal.btn]}> 683 - <Text style={[pal.textLight, s.f16, s.bold]}> 684 - <Trans context="action">Post</Trans> 685 - </Text> 686 - </View> 687 - )} 688 </View> 689 )} 690 - </View> 691 - 692 - {isAltTextRequiredAndMissing && ( 693 - <View style={[styles.reminderLine, pal.viewLight]}> 694 - <View style={styles.errorIcon}> 695 - <FontAwesomeIcon 696 - icon="exclamation" 697 - style={{color: colors.red4}} 698 - size={10} 699 - /> 700 - </View> 701 - <Text style={[pal.text, a.flex_1]}> 702 - <Trans>One or more images is missing alt text.</Trans> 703 - </Text> 704 - </View> 705 - )} 706 - <ErrorBanner 707 - error={error} 708 - videoState={videoState} 709 - clearError={() => setError('')} 710 - clearVideo={clearVideo} 711 - /> 712 - </Animated.View> 713 - <Animated.ScrollView 714 - layout={native(LinearTransition)} 715 - onScroll={scrollHandler} 716 - style={styles.scrollView} 717 - keyboardShouldPersistTaps="always" 718 - onContentSizeChange={onScrollViewContentSizeChange} 719 - onLayout={onScrollViewLayout}> 720 - {replyTo ? <ComposerReplyTo replyTo={replyTo} /> : undefined} 721 - 722 - <View 723 - style={[ 724 - styles.textInputLayout, 725 - isNative && styles.textInputLayoutMobile, 726 - ]}> 727 - <UserAvatar 728 - avatar={currentProfile?.avatar} 729 - size={50} 730 - type={currentProfile?.associated?.labeler ? 'labeler' : 'user'} 731 /> 732 - <TextInput 733 - ref={textInput} 734 - richtext={richtext} 735 - placeholder={selectTextInputPlaceholder} 736 - autoFocus 737 - setRichText={setRichText} 738 - onPhotoPasted={onPhotoPasted} 739 - onPressPublish={() => onPressPublish()} 740 - onNewLink={onNewLink} 741 - onError={setError} 742 - accessible={true} 743 - accessibilityLabel={_(msg`Write post`)} 744 - accessibilityHint={_( 745 - msg`Compose posts up to ${MAX_GRAPHEME_LENGTH} characters in length`, 746 - )} 747 - /> 748 - </View> 749 750 - <Gallery images={images} dispatch={dispatch} /> 751 - {images.length === 0 && extLink && ( 752 - <View style={a.relative}> 753 - <ExternalEmbed 754 - link={extLink} 755 - gif={extGif} 756 - onRemove={() => { 757 - if (extGif) { 758 - dispatch({type: 'embed_remove_gif'}) 759 - } else { 760 - dispatch({type: 'embed_remove_link'}) 761 - } 762 - setExtLink(undefined) 763 - setExtGif(undefined) 764 - }} 765 /> 766 - <GifAltText 767 - link={extLink} 768 - gif={extGif} 769 - onSubmit={handleChangeGifAltText} 770 /> 771 </View> 772 - )} 773 - <LayoutAnimationConfig skipExiting> 774 - {hasVideo && ( 775 - <Animated.View 776 - style={[a.w_full, a.mt_lg]} 777 - entering={native(ZoomIn)} 778 - exiting={native(ZoomOut)}> 779 - {videoState.asset && 780 - (videoState.status === 'compressing' ? ( 781 - <VideoTranscodeProgress 782 - asset={videoState.asset} 783 - progress={videoState.progress} 784 - clear={clearVideo} 785 - /> 786 - ) : videoState.video ? ( 787 - <VideoPreview 788 - asset={videoState.asset} 789 - video={videoState.video} 790 - setDimensions={updateVideoDimensions} 791 - clear={clearVideo} 792 - /> 793 - ) : null)} 794 - <SubtitleDialogBtn 795 - defaultAltText={videoState.altText} 796 - saveAltText={altText => 797 - dispatch({ 798 - type: 'embed_update_video', 799 - videoAction: { 800 - type: 'update_alt_text', 801 - altText, 802 - signal: videoState.abortController.signal, 803 - }, 804 - }) 805 - } 806 - captions={videoState.captions} 807 - setCaptions={updater => { 808 - dispatch({ 809 - type: 'embed_update_video', 810 - videoAction: { 811 - type: 'update_captions', 812 - updater, 813 - signal: videoState.abortController.signal, 814 - }, 815 - }) 816 }} 817 /> 818 - </Animated.View> 819 )} 820 - </LayoutAnimationConfig> 821 - <View style={!hasVideo ? [a.mt_md] : []}> 822 - {quote ? ( 823 - <View style={[s.mt5, s.mb2, isWeb && s.mb10]}> 824 - <View style={{pointerEvents: 'none'}}> 825 - <QuoteEmbed quote={quote} /> 826 - </View> 827 - {quote.uri !== initQuote?.uri && ( 828 - <QuoteX 829 - onRemove={() => { 830 - dispatch({type: 'embed_remove_quote'}) 831 - setQuote(undefined) 832 }} 833 /> 834 - )} 835 - </View> 836 - ) : null} 837 - </View> 838 - </Animated.ScrollView> 839 - <SuggestedLanguage text={richtext.text} /> 840 - 841 - {replyTo ? null : ( 842 - <ThreadgateBtn 843 - postgate={postgate} 844 - onChangePostgate={setPostgate} 845 - threadgateAllowUISettings={threadgateAllowUISettings} 846 - onChangeThreadgateAllowUISettings={ 847 - onChangeThreadgateAllowUISettings 848 - } 849 - style={bottomBarAnimatedStyle} 850 - /> 851 - )} 852 - <View 853 - style={[ 854 - t.atoms.bg, 855 - t.atoms.border_contrast_medium, 856 - styles.bottomBar, 857 - ]}> 858 - {videoState.status !== 'idle' && videoState.status !== 'done' ? ( 859 - <VideoUploadToolbar state={videoState} /> 860 - ) : ( 861 - <ToolbarWrapper style={[a.flex_row, a.align_center, a.gap_xs]}> 862 - <SelectPhotoBtn 863 - size={images.length} 864 - disabled={!canSelectImages} 865 - onAdd={onImageAdd} 866 - /> 867 - <SelectVideoBtn 868 - onSelectVideo={selectVideo} 869 - disabled={!canSelectImages || images?.length > 0} 870 - setError={setError} 871 - /> 872 - <OpenCameraBtn disabled={!canSelectImages} onAdd={onImageAdd} /> 873 - <SelectGifBtn 874 - onClose={focusTextInput} 875 - onSelectGif={onSelectGif} 876 - disabled={hasMedia} 877 - /> 878 - {!isMobile ? ( 879 - <Button 880 - onPress={onEmojiButtonPress} 881 - style={a.p_sm} 882 - label={_(msg`Open emoji picker`)} 883 - accessibilityHint={_(msg`Open emoji picker`)} 884 - variant="ghost" 885 - shape="round" 886 - color="primary"> 887 - <EmojiSmile size="lg" /> 888 - </Button> 889 ) : null} 890 - </ToolbarWrapper> 891 )} 892 - <View style={a.flex_1} /> 893 - <SelectLangBtn /> 894 - <CharProgress count={graphemeLength} /> 895 </View> 896 - </View> 897 - <Prompt.Basic 898 - control={discardPromptControl} 899 - title={_(msg`Discard draft?`)} 900 - description={_(msg`Are you sure you'd like to discard this draft?`)} 901 - onConfirm={onClose} 902 - confirmButtonCta={_(msg`Discard`)} 903 - confirmButtonColor="negative" 904 - /> 905 - </KeyboardAvoidingView> 906 ) 907 } 908
··· 114 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' 115 import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji' 116 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' 117 + import {createPortalGroup} from '#/components/Portal' 118 import * as Prompt from '#/components/Prompt' 119 import {Text as NewText} from '#/components/Typography' 120 import {composerReducer, createComposerState} from './state/composer' 121 import {NO_VIDEO, NoVideoState, processVideo, VideoState} from './state/video' 122 + 123 + const Portal = createPortalGroup() 124 125 const MAX_IMAGES = 4 126 ··· 616 const keyboardVerticalOffset = useKeyboardVerticalOffset() 617 618 return ( 619 + <Portal.Provider> 620 + <KeyboardAvoidingView 621 + testID="composePostView" 622 + behavior={isIOS ? 'padding' : 'height'} 623 + keyboardVerticalOffset={keyboardVerticalOffset} 624 + style={a.flex_1}> 625 + <View 626 + style={[a.flex_1, viewStyles]} 627 + aria-modal 628 + accessibilityViewIsModal> 629 + <Animated.View 630 + style={topBarAnimatedStyle} 631 + layout={native(LinearTransition)}> 632 + <View style={styles.topbarInner}> 633 + <Button 634 + label={_(msg`Cancel`)} 635 + variant="ghost" 636 + color="primary" 637 + shape="default" 638 + size="small" 639 + style={[ 640 + a.rounded_full, 641 + a.py_sm, 642 + {paddingLeft: 7, paddingRight: 7}, 643 + ]} 644 + onPress={onPressCancel} 645 + accessibilityHint={_( 646 + msg`Closes post composer and discards post draft`, 647 + )}> 648 + <ButtonText style={[a.text_md]}> 649 + <Trans>Cancel</Trans> 650 + </ButtonText> 651 + </Button> 652 + <View style={a.flex_1} /> 653 + {isProcessing ? ( 654 + <> 655 + <Text style={pal.textLight}>{processingState}</Text> 656 + <View style={styles.postBtn}> 657 + <ActivityIndicator /> 658 + </View> 659 + </> 660 + ) : ( 661 + <View style={[styles.postBtnWrapper]}> 662 + <LabelsBtn 663 + labels={labels} 664 + onChange={setLabels} 665 + hasMedia={hasMedia} 666 + /> 667 + {canPost ? ( 668 + <Button 669 + testID="composerPublishBtn" 670 + label={ 671 + replyTo ? _(msg`Publish reply`) : _(msg`Publish post`) 672 + } 673 + variant="solid" 674 + color="primary" 675 + shape="default" 676 + size="small" 677 + style={[a.rounded_full, a.py_sm]} 678 + onPress={() => onPressPublish()} 679 + disabled={ 680 + videoState.status !== 'idle' && publishOnUpload 681 + }> 682 + <ButtonText style={[a.text_md]}> 683 + {replyTo ? ( 684 + <Trans context="action">Reply</Trans> 685 + ) : ( 686 + <Trans context="action">Post</Trans> 687 + )} 688 + </ButtonText> 689 + </Button> 690 + ) : ( 691 + <View style={[styles.postBtn, pal.btn]}> 692 + <Text style={[pal.textLight, s.f16, s.bold]}> 693 + <Trans context="action">Post</Trans> 694 + </Text> 695 + </View> 696 + )} 697 + </View> 698 + )} 699 + </View> 700 + 701 + {isAltTextRequiredAndMissing && ( 702 + <View style={[styles.reminderLine, pal.viewLight]}> 703 + <View style={styles.errorIcon}> 704 + <FontAwesomeIcon 705 + icon="exclamation" 706 + style={{color: colors.red4}} 707 + size={10} 708 + /> 709 </View> 710 + <Text style={[pal.text, a.flex_1]}> 711 + <Trans>One or more images is missing alt text.</Trans> 712 + </Text> 713 </View> 714 )} 715 + <ErrorBanner 716 + error={error} 717 + videoState={videoState} 718 + clearError={() => setError('')} 719 + clearVideo={clearVideo} 720 /> 721 + </Animated.View> 722 + <Animated.ScrollView 723 + layout={native(LinearTransition)} 724 + onScroll={scrollHandler} 725 + style={styles.scrollView} 726 + keyboardShouldPersistTaps="always" 727 + onContentSizeChange={onScrollViewContentSizeChange} 728 + onLayout={onScrollViewLayout}> 729 + {replyTo ? <ComposerReplyTo replyTo={replyTo} /> : undefined} 730 731 + <View 732 + style={[ 733 + styles.textInputLayout, 734 + isNative && styles.textInputLayoutMobile, 735 + ]}> 736 + <UserAvatar 737 + avatar={currentProfile?.avatar} 738 + size={50} 739 + type={currentProfile?.associated?.labeler ? 'labeler' : 'user'} 740 /> 741 + <TextInput 742 + ref={textInput} 743 + richtext={richtext} 744 + placeholder={selectTextInputPlaceholder} 745 + autoFocus 746 + setRichText={setRichText} 747 + onPhotoPasted={onPhotoPasted} 748 + onPressPublish={() => onPressPublish()} 749 + onNewLink={onNewLink} 750 + onError={setError} 751 + accessible={true} 752 + accessibilityLabel={_(msg`Write post`)} 753 + accessibilityHint={_( 754 + msg`Compose posts up to ${MAX_GRAPHEME_LENGTH} characters in length`, 755 + )} 756 /> 757 </View> 758 + 759 + <Gallery 760 + images={images} 761 + dispatch={dispatch} 762 + Portal={Portal.Portal} 763 + /> 764 + {images.length === 0 && extLink && ( 765 + <View style={a.relative}> 766 + <ExternalEmbed 767 + link={extLink} 768 + gif={extGif} 769 + onRemove={() => { 770 + if (extGif) { 771 + dispatch({type: 'embed_remove_gif'}) 772 + } else { 773 + dispatch({type: 'embed_remove_link'}) 774 + } 775 + setExtLink(undefined) 776 + setExtGif(undefined) 777 }} 778 /> 779 + <GifAltText 780 + link={extLink} 781 + gif={extGif} 782 + onSubmit={handleChangeGifAltText} 783 + Portal={Portal.Portal} 784 + /> 785 + </View> 786 )} 787 + <LayoutAnimationConfig skipExiting> 788 + {hasVideo && ( 789 + <Animated.View 790 + style={[a.w_full, a.mt_lg]} 791 + entering={native(ZoomIn)} 792 + exiting={native(ZoomOut)}> 793 + {videoState.asset && 794 + (videoState.status === 'compressing' ? ( 795 + <VideoTranscodeProgress 796 + asset={videoState.asset} 797 + progress={videoState.progress} 798 + clear={clearVideo} 799 + /> 800 + ) : videoState.video ? ( 801 + <VideoPreview 802 + asset={videoState.asset} 803 + video={videoState.video} 804 + setDimensions={updateVideoDimensions} 805 + clear={clearVideo} 806 + /> 807 + ) : null)} 808 + <SubtitleDialogBtn 809 + defaultAltText={videoState.altText} 810 + saveAltText={altText => 811 + dispatch({ 812 + type: 'embed_update_video', 813 + videoAction: { 814 + type: 'update_alt_text', 815 + altText, 816 + signal: videoState.abortController.signal, 817 + }, 818 + }) 819 + } 820 + captions={videoState.captions} 821 + setCaptions={updater => { 822 + dispatch({ 823 + type: 'embed_update_video', 824 + videoAction: { 825 + type: 'update_captions', 826 + updater, 827 + signal: videoState.abortController.signal, 828 + }, 829 + }) 830 }} 831 + Portal={Portal.Portal} 832 /> 833 + </Animated.View> 834 + )} 835 + </LayoutAnimationConfig> 836 + <View style={!hasVideo ? [a.mt_md] : []}> 837 + {quote ? ( 838 + <View style={[s.mt5, s.mb2, isWeb && s.mb10]}> 839 + <View style={{pointerEvents: 'none'}}> 840 + <QuoteEmbed quote={quote} /> 841 + </View> 842 + {quote.uri !== initQuote?.uri && ( 843 + <QuoteX 844 + onRemove={() => { 845 + dispatch({type: 'embed_remove_quote'}) 846 + setQuote(undefined) 847 + }} 848 + /> 849 + )} 850 + </View> 851 ) : null} 852 + </View> 853 + </Animated.ScrollView> 854 + <SuggestedLanguage text={richtext.text} /> 855 + 856 + {replyTo ? null : ( 857 + <ThreadgateBtn 858 + postgate={postgate} 859 + onChangePostgate={setPostgate} 860 + threadgateAllowUISettings={threadgateAllowUISettings} 861 + onChangeThreadgateAllowUISettings={ 862 + onChangeThreadgateAllowUISettings 863 + } 864 + style={bottomBarAnimatedStyle} 865 + Portal={Portal.Portal} 866 + /> 867 )} 868 + <View 869 + style={[ 870 + t.atoms.bg, 871 + t.atoms.border_contrast_medium, 872 + styles.bottomBar, 873 + ]}> 874 + {videoState.status !== 'idle' && videoState.status !== 'done' ? ( 875 + <VideoUploadToolbar state={videoState} /> 876 + ) : ( 877 + <ToolbarWrapper style={[a.flex_row, a.align_center, a.gap_xs]}> 878 + <SelectPhotoBtn 879 + size={images.length} 880 + disabled={!canSelectImages} 881 + onAdd={onImageAdd} 882 + /> 883 + <SelectVideoBtn 884 + onSelectVideo={selectVideo} 885 + disabled={!canSelectImages || images?.length > 0} 886 + setError={setError} 887 + /> 888 + <OpenCameraBtn disabled={!canSelectImages} onAdd={onImageAdd} /> 889 + <SelectGifBtn 890 + onClose={focusTextInput} 891 + onSelectGif={onSelectGif} 892 + disabled={hasMedia} 893 + Portal={Portal.Portal} 894 + /> 895 + {!isMobile ? ( 896 + <Button 897 + onPress={onEmojiButtonPress} 898 + style={a.p_sm} 899 + label={_(msg`Open emoji picker`)} 900 + accessibilityHint={_(msg`Open emoji picker`)} 901 + variant="ghost" 902 + shape="round" 903 + color="primary"> 904 + <EmojiSmile size="lg" /> 905 + </Button> 906 + ) : null} 907 + </ToolbarWrapper> 908 + )} 909 + <View style={a.flex_1} /> 910 + <SelectLangBtn /> 911 + <CharProgress count={graphemeLength} /> 912 + </View> 913 </View> 914 + <Prompt.Basic 915 + control={discardPromptControl} 916 + title={_(msg`Discard draft?`)} 917 + description={_(msg`Are you sure you'd like to discard this draft?`)} 918 + onConfirm={onClose} 919 + confirmButtonCta={_(msg`Discard`)} 920 + confirmButtonColor="negative" 921 + Portal={Portal.Portal} 922 + /> 923 + </KeyboardAvoidingView> 924 + <Portal.Outlet /> 925 + </Portal.Provider> 926 ) 927 } 928
+6 -3
src/view/com/composer/GifAltText.tsx
··· 20 import * as TextField from '#/components/forms/TextField' 21 import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' 22 import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' 23 import {Text} from '#/components/Typography' 24 import {GifEmbed} from '../util/post-embeds/GifEmbed' 25 import {AltTextReminder} from './photos/Gallery' ··· 28 link: linkProp, 29 gif, 30 onSubmit, 31 }: { 32 link: ExternalEmbedDraft 33 gif?: Gif 34 onSubmit: (alt: string) => void 35 }) { 36 const control = Dialog.useDialogControl() 37 const {_} = useLingui() ··· 96 97 <AltTextReminder /> 98 99 - <Dialog.Outer 100 - control={control} 101 - nativeOptions={isAndroid ? {sheet: {snapPoints: ['100%']}} : {}}> 102 <Dialog.Handle /> 103 <AltTextInner 104 onSubmit={onPressSubmit} ··· 185 </View> 186 </View> 187 <Dialog.Close /> 188 </Dialog.ScrollableInner> 189 ) 190 }
··· 20 import * as TextField from '#/components/forms/TextField' 21 import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' 22 import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' 23 + import {PortalComponent} from '#/components/Portal' 24 import {Text} from '#/components/Typography' 25 import {GifEmbed} from '../util/post-embeds/GifEmbed' 26 import {AltTextReminder} from './photos/Gallery' ··· 29 link: linkProp, 30 gif, 31 onSubmit, 32 + Portal, 33 }: { 34 link: ExternalEmbedDraft 35 gif?: Gif 36 onSubmit: (alt: string) => void 37 + Portal: PortalComponent 38 }) { 39 const control = Dialog.useDialogControl() 40 const {_} = useLingui() ··· 99 100 <AltTextReminder /> 101 102 + <Dialog.Outer control={control} Portal={Portal}> 103 <Dialog.Handle /> 104 <AltTextInner 105 onSubmit={onPressSubmit} ··· 186 </View> 187 </View> 188 <Dialog.Close /> 189 + {/* Maybe fix this later -h */} 190 + {isAndroid ? <View style={{height: 300}} /> : null} 191 </Dialog.ScrollableInner> 192 ) 193 }
+1
src/view/com/composer/photos/EditImageDialog.web.tsx
··· 20 export const EditImageDialog = (props: EditImageDialogProps) => { 21 return ( 22 <Dialog.Outer control={props.control}> 23 <EditImageInner key={props.image.source.id} {...props} /> 24 </Dialog.Outer> 25 )
··· 20 export const EditImageDialog = (props: EditImageDialogProps) => { 21 return ( 22 <Dialog.Outer control={props.control}> 23 + <Dialog.Handle /> 24 <EditImageInner key={props.image.source.id} {...props} /> 25 </Dialog.Outer> 26 )
+12 -1
src/view/com/composer/photos/Gallery.tsx
··· 21 import {Text} from '#/view/com/util/text/Text' 22 import {useTheme} from '#/alf' 23 import * as Dialog from '#/components/Dialog' 24 import {ComposerAction} from '../state/composer' 25 import {EditImageDialog} from './EditImageDialog' 26 import {ImageAltTextDialog} from './ImageAltTextDialog' ··· 30 interface GalleryProps { 31 images: ComposerImage[] 32 dispatch: (action: ComposerAction) => void 33 } 34 35 export let Gallery = (props: GalleryProps): React.ReactNode => { ··· 57 containerInfo: Dimensions 58 } 59 60 - const GalleryInner = ({images, containerInfo, dispatch}: GalleryInnerProps) => { 61 const {isMobile} = useWebMediaQueries() 62 63 const {altTextControlStyle, imageControlsStyle, imageStyle} = ··· 111 onRemove={() => { 112 dispatch({type: 'embed_remove_image', image}) 113 }} 114 /> 115 ) 116 })} ··· 127 imageStyle?: ViewStyle 128 onChange: (next: ComposerImage) => void 129 onRemove: () => void 130 } 131 132 const GalleryItem = ({ ··· 136 imageStyle, 137 onChange, 138 onRemove, 139 }: GalleryItemProps): React.ReactNode => { 140 const {_} = useLingui() 141 const t = useTheme() ··· 230 control={altTextControl} 231 image={image} 232 onChange={onChange} 233 /> 234 235 <EditImageDialog
··· 21 import {Text} from '#/view/com/util/text/Text' 22 import {useTheme} from '#/alf' 23 import * as Dialog from '#/components/Dialog' 24 + import {PortalComponent} from '#/components/Portal' 25 import {ComposerAction} from '../state/composer' 26 import {EditImageDialog} from './EditImageDialog' 27 import {ImageAltTextDialog} from './ImageAltTextDialog' ··· 31 interface GalleryProps { 32 images: ComposerImage[] 33 dispatch: (action: ComposerAction) => void 34 + Portal: PortalComponent 35 } 36 37 export let Gallery = (props: GalleryProps): React.ReactNode => { ··· 59 containerInfo: Dimensions 60 } 61 62 + const GalleryInner = ({ 63 + images, 64 + containerInfo, 65 + dispatch, 66 + Portal, 67 + }: GalleryInnerProps) => { 68 const {isMobile} = useWebMediaQueries() 69 70 const {altTextControlStyle, imageControlsStyle, imageStyle} = ··· 118 onRemove={() => { 119 dispatch({type: 'embed_remove_image', image}) 120 }} 121 + Portal={Portal} 122 /> 123 ) 124 })} ··· 135 imageStyle?: ViewStyle 136 onChange: (next: ComposerImage) => void 137 onRemove: () => void 138 + Portal: PortalComponent 139 } 140 141 const GalleryItem = ({ ··· 145 imageStyle, 146 onChange, 147 onRemove, 148 + Portal, 149 }: GalleryItemProps): React.ReactNode => { 150 const {_} = useLingui() 151 const t = useTheme() ··· 240 control={altTextControl} 241 image={image} 242 onChange={onChange} 243 + Portal={Portal} 244 /> 245 246 <EditImageDialog
+6 -3
src/view/com/composer/photos/ImageAltTextDialog.tsx
··· 5 import {useLingui} from '@lingui/react' 6 7 import {MAX_ALT_TEXT} from '#/lib/constants' 8 - import {isWeb} from '#/platform/detection' 9 import {ComposerImage} from '#/state/gallery' 10 import {atoms as a, useTheme} from '#/alf' 11 import {Button, ButtonText} from '#/components/Button' 12 import * as Dialog from '#/components/Dialog' 13 import * as TextField from '#/components/forms/TextField' 14 import {Text} from '#/components/Typography' 15 16 type Props = { 17 control: Dialog.DialogOuterProps['control'] 18 image: ComposerImage 19 onChange: (next: ComposerImage) => void 20 } 21 22 export const ImageAltTextDialog = (props: Props): React.ReactNode => { 23 return ( 24 - <Dialog.Outer control={props.control}> 25 <Dialog.Handle /> 26 - 27 <ImageAltTextInner {...props} /> 28 </Dialog.Outer> 29 ) ··· 116 </ButtonText> 117 </Button> 118 </View> 119 </Dialog.ScrollableInner> 120 ) 121 }
··· 5 import {useLingui} from '@lingui/react' 6 7 import {MAX_ALT_TEXT} from '#/lib/constants' 8 + import {isAndroid, isWeb} from '#/platform/detection' 9 import {ComposerImage} from '#/state/gallery' 10 import {atoms as a, useTheme} from '#/alf' 11 import {Button, ButtonText} from '#/components/Button' 12 import * as Dialog from '#/components/Dialog' 13 import * as TextField from '#/components/forms/TextField' 14 + import {PortalComponent} from '#/components/Portal' 15 import {Text} from '#/components/Typography' 16 17 type Props = { 18 control: Dialog.DialogOuterProps['control'] 19 image: ComposerImage 20 onChange: (next: ComposerImage) => void 21 + Portal: PortalComponent 22 } 23 24 export const ImageAltTextDialog = (props: Props): React.ReactNode => { 25 return ( 26 + <Dialog.Outer control={props.control} Portal={props.Portal}> 27 <Dialog.Handle /> 28 <ImageAltTextInner {...props} /> 29 </Dialog.Outer> 30 ) ··· 117 </ButtonText> 118 </Button> 119 </View> 120 + {/* Maybe fix this later -h */} 121 + {isAndroid ? <View style={{height: 300}} /> : null} 122 </Dialog.ScrollableInner> 123 ) 124 }
+4 -1
src/view/com/composer/photos/SelectGifBtn.tsx
··· 9 import {Button} from '#/components/Button' 10 import {GifSelectDialog} from '#/components/dialogs/GifSelect' 11 import {GifSquare_Stroke2_Corner0_Rounded as GifIcon} from '#/components/icons/Gif' 12 13 type Props = { 14 onClose: () => void 15 onSelectGif: (gif: Gif) => void 16 disabled?: boolean 17 } 18 19 - export function SelectGifBtn({onClose, onSelectGif, disabled}: Props) { 20 const {_} = useLingui() 21 const ref = useRef<{open: () => void}>(null) 22 const t = useTheme() ··· 46 controlRef={ref} 47 onClose={onClose} 48 onSelectGif={onSelectGif} 49 /> 50 </> 51 )
··· 9 import {Button} from '#/components/Button' 10 import {GifSelectDialog} from '#/components/dialogs/GifSelect' 11 import {GifSquare_Stroke2_Corner0_Rounded as GifIcon} from '#/components/icons/Gif' 12 + import {PortalComponent} from '#/components/Portal' 13 14 type Props = { 15 onClose: () => void 16 onSelectGif: (gif: Gif) => void 17 disabled?: boolean 18 + Portal?: PortalComponent 19 } 20 21 + export function SelectGifBtn({onClose, onSelectGif, disabled, Portal}: Props) { 22 const {_} = useLingui() 23 const ref = useRef<{open: () => void}>(null) 24 const t = useTheme() ··· 48 controlRef={ref} 49 onClose={onClose} 50 onSelectGif={onSelectGif} 51 + Portal={Portal} 52 /> 53 </> 54 )
+9 -5
src/view/com/composer/photos/SelectPhotoBtn.tsx
··· 9 import {ComposerImage, createComposerImage} from '#/state/gallery' 10 import {atoms as a, useTheme} from '#/alf' 11 import {Button} from '#/components/Button' 12 import {Image_Stroke2_Corner0_Rounded as Image} from '#/components/icons/Image' 13 14 type Props = { ··· 21 const {_} = useLingui() 22 const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission() 23 const t = useTheme() 24 25 const onPressSelectPhotos = useCallback(async () => { 26 if (isNative && !(await requestPhotoAccessIfNeeded())) { 27 return 28 } 29 30 - const images = await openPicker({ 31 - selectionLimit: 4 - size, 32 - allowsMultipleSelection: true, 33 - }) 34 35 const results = await Promise.all( 36 images.map(img => createComposerImage(img)), 37 ) 38 39 onAdd(results) 40 - }, [requestPhotoAccessIfNeeded, size, onAdd]) 41 42 return ( 43 <Button
··· 9 import {ComposerImage, createComposerImage} from '#/state/gallery' 10 import {atoms as a, useTheme} from '#/alf' 11 import {Button} from '#/components/Button' 12 + import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper' 13 import {Image_Stroke2_Corner0_Rounded as Image} from '#/components/icons/Image' 14 15 type Props = { ··· 22 const {_} = useLingui() 23 const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission() 24 const t = useTheme() 25 + const sheetWrapper = useSheetWrapper() 26 27 const onPressSelectPhotos = useCallback(async () => { 28 if (isNative && !(await requestPhotoAccessIfNeeded())) { 29 return 30 } 31 32 + const images = await sheetWrapper( 33 + openPicker({ 34 + selectionLimit: 4 - size, 35 + allowsMultipleSelection: true, 36 + }), 37 + ) 38 39 const results = await Promise.all( 40 images.map(img => createComposerImage(img)), 41 ) 42 43 onAdd(results) 44 + }, [requestPhotoAccessIfNeeded, size, onAdd, sheetWrapper]) 45 46 return ( 47 <Button
+5
src/view/com/composer/threadgate/ThreadgateBtn.tsx
··· 13 import {PostInteractionSettingsControlledDialog} from '#/components/dialogs/PostInteractionSettingsDialog' 14 import {Earth_Stroke2_Corner0_Rounded as Earth} from '#/components/icons/Globe' 15 import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group' 16 17 export function ThreadgateBtn({ 18 postgate, ··· 20 threadgateAllowUISettings, 21 onChangeThreadgateAllowUISettings, 22 style, 23 }: { 24 postgate: AppBskyFeedPostgate.Record 25 onChangePostgate: (v: AppBskyFeedPostgate.Record) => void ··· 28 onChangeThreadgateAllowUISettings: (v: ThreadgateAllowUISetting[]) => void 29 30 style?: StyleProp<AnimatedStyle<ViewStyle>> 31 }) { 32 const {_} = useLingui() 33 const t = useTheme() ··· 77 onChangePostgate={onChangePostgate} 78 threadgateAllowUISettings={threadgateAllowUISettings} 79 onChangeThreadgateAllowUISettings={onChangeThreadgateAllowUISettings} 80 /> 81 </> 82 )
··· 13 import {PostInteractionSettingsControlledDialog} from '#/components/dialogs/PostInteractionSettingsDialog' 14 import {Earth_Stroke2_Corner0_Rounded as Earth} from '#/components/icons/Globe' 15 import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group' 16 + import {PortalComponent} from '#/components/Portal' 17 18 export function ThreadgateBtn({ 19 postgate, ··· 21 threadgateAllowUISettings, 22 onChangeThreadgateAllowUISettings, 23 style, 24 + Portal, 25 }: { 26 postgate: AppBskyFeedPostgate.Record 27 onChangePostgate: (v: AppBskyFeedPostgate.Record) => void ··· 30 onChangeThreadgateAllowUISettings: (v: ThreadgateAllowUISetting[]) => void 31 32 style?: StyleProp<AnimatedStyle<ViewStyle>> 33 + 34 + Portal: PortalComponent 35 }) { 36 const {_} = useLingui() 37 const t = useTheme() ··· 81 onChangePostgate={onChangePostgate} 82 threadgateAllowUISettings={threadgateAllowUISettings} 83 onChangeThreadgateAllowUISettings={onChangeThreadgateAllowUISettings} 84 + Portal={Portal} 85 /> 86 </> 87 )
+4 -4
src/view/com/composer/videos/SubtitleDialog.tsx
··· 7 import {MAX_ALT_TEXT} from '#/lib/constants' 8 import {useEnforceMaxGraphemeCount} from '#/lib/strings/helpers' 9 import {LANGUAGES} from '#/locale/languages' 10 - import {isAndroid, isWeb} from '#/platform/detection' 11 import {useLanguagePrefs} from '#/state/preferences' 12 import {atoms as a, useTheme, web} from '#/alf' 13 import {Button, ButtonIcon, ButtonText} from '#/components/Button' ··· 17 import {PageText_Stroke2_Corner0_Rounded as PageTextIcon} from '#/components/icons/PageText' 18 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' 19 import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' 20 import {Text} from '#/components/Typography' 21 import {SubtitleFilePicker} from './SubtitleFilePicker' 22 ··· 29 captions: CaptionsTrack[] 30 saveAltText: (altText: string) => void 31 setCaptions: (updater: (prev: CaptionsTrack[]) => CaptionsTrack[]) => void 32 } 33 34 export function SubtitleDialogBtn(props: Props) { ··· 56 {isWeb ? <Trans>Captions & alt text</Trans> : <Trans>Alt text</Trans>} 57 </ButtonText> 58 </Button> 59 - <Dialog.Outer 60 - control={control} 61 - nativeOptions={isAndroid ? {sheet: {snapPoints: ['60%']}} : {}}> 62 <Dialog.Handle /> 63 <SubtitleDialogInner {...props} /> 64 </Dialog.Outer>
··· 7 import {MAX_ALT_TEXT} from '#/lib/constants' 8 import {useEnforceMaxGraphemeCount} from '#/lib/strings/helpers' 9 import {LANGUAGES} from '#/locale/languages' 10 + import {isWeb} from '#/platform/detection' 11 import {useLanguagePrefs} from '#/state/preferences' 12 import {atoms as a, useTheme, web} from '#/alf' 13 import {Button, ButtonIcon, ButtonText} from '#/components/Button' ··· 17 import {PageText_Stroke2_Corner0_Rounded as PageTextIcon} from '#/components/icons/PageText' 18 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' 19 import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' 20 + import {PortalComponent} from '#/components/Portal' 21 import {Text} from '#/components/Typography' 22 import {SubtitleFilePicker} from './SubtitleFilePicker' 23 ··· 30 captions: CaptionsTrack[] 31 saveAltText: (altText: string) => void 32 setCaptions: (updater: (prev: CaptionsTrack[]) => CaptionsTrack[]) => void 33 + Portal: PortalComponent 34 } 35 36 export function SubtitleDialogBtn(props: Props) { ··· 58 {isWeb ? <Trans>Captions & alt text</Trans> : <Trans>Alt text</Trans>} 59 </ButtonText> 60 </Button> 61 + <Dialog.Outer control={control} Portal={props.Portal}> 62 <Dialog.Handle /> 63 <SubtitleDialogInner {...props} /> 64 </Dialog.Outer>
+8 -4
src/view/com/util/UserAvatar.tsx
··· 20 import {precacheProfile} from '#/state/queries/profile' 21 import {HighPriorityImage} from '#/view/com/util/images/Image' 22 import {tokens, useTheme} from '#/alf' 23 import { 24 Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled, 25 Camera_Stroke2_Corner0_Rounded as Camera, ··· 271 const {_} = useLingui() 272 const {requestCameraAccessIfNeeded} = useCameraPermission() 273 const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission() 274 275 const aviStyle = useMemo(() => { 276 if (type === 'algo' || type === 'list') { ··· 306 return 307 } 308 309 - const items = await openPicker({ 310 - aspect: [1, 1], 311 - }) 312 const item = items[0] 313 if (!item) { 314 return ··· 332 logger.error('Failed to crop banner', {error: e}) 333 } 334 } 335 - }, [onSelectNewAvatar, requestPhotoAccessIfNeeded]) 336 337 const onRemoveAvatar = React.useCallback(() => { 338 onSelectNewAvatar(null)
··· 20 import {precacheProfile} from '#/state/queries/profile' 21 import {HighPriorityImage} from '#/view/com/util/images/Image' 22 import {tokens, useTheme} from '#/alf' 23 + import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper' 24 import { 25 Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled, 26 Camera_Stroke2_Corner0_Rounded as Camera, ··· 272 const {_} = useLingui() 273 const {requestCameraAccessIfNeeded} = useCameraPermission() 274 const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission() 275 + const sheetWrapper = useSheetWrapper() 276 277 const aviStyle = useMemo(() => { 278 if (type === 'algo' || type === 'list') { ··· 308 return 309 } 310 311 + const items = await sheetWrapper( 312 + openPicker({ 313 + aspect: [1, 1], 314 + }), 315 + ) 316 const item = items[0] 317 if (!item) { 318 return ··· 336 logger.error('Failed to crop banner', {error: e}) 337 } 338 } 339 + }, [onSelectNewAvatar, requestPhotoAccessIfNeeded, sheetWrapper]) 340 341 const onRemoveAvatar = React.useCallback(() => { 342 onSelectNewAvatar(null)
+4 -2
src/view/com/util/UserBanner.tsx
··· 17 import {isAndroid, isNative} from '#/platform/detection' 18 import {EventStopper} from '#/view/com/util/EventStopper' 19 import {tokens, useTheme as useAlfTheme} from '#/alf' 20 import { 21 Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled, 22 Camera_Stroke2_Corner0_Rounded as Camera, ··· 43 const {_} = useLingui() 44 const {requestCameraAccessIfNeeded} = useCameraPermission() 45 const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission() 46 47 const onOpenCamera = React.useCallback(async () => { 48 if (!(await requestCameraAccessIfNeeded())) { ··· 60 if (!(await requestPhotoAccessIfNeeded())) { 61 return 62 } 63 - const items = await openPicker() 64 if (!items[0]) { 65 return 66 } ··· 80 logger.error('Failed to crop banner', {error: e}) 81 } 82 } 83 - }, [onSelectNewBanner, requestPhotoAccessIfNeeded]) 84 85 const onRemoveBanner = React.useCallback(() => { 86 onSelectNewBanner?.(null)
··· 17 import {isAndroid, isNative} from '#/platform/detection' 18 import {EventStopper} from '#/view/com/util/EventStopper' 19 import {tokens, useTheme as useAlfTheme} from '#/alf' 20 + import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper' 21 import { 22 Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled, 23 Camera_Stroke2_Corner0_Rounded as Camera, ··· 44 const {_} = useLingui() 45 const {requestCameraAccessIfNeeded} = useCameraPermission() 46 const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission() 47 + const sheetWrapper = useSheetWrapper() 48 49 const onOpenCamera = React.useCallback(async () => { 50 if (!(await requestCameraAccessIfNeeded())) { ··· 62 if (!(await requestPhotoAccessIfNeeded())) { 63 return 64 } 65 + const items = await sheetWrapper(openPicker()) 66 if (!items[0]) { 67 return 68 } ··· 82 logger.error('Failed to crop banner', {error: e}) 83 } 84 } 85 + }, [onSelectNewBanner, requestPhotoAccessIfNeeded, sheetWrapper]) 86 87 const onRemoveBanner = React.useCallback(() => { 88 onSelectNewBanner?.(null)
+9 -7
src/view/com/util/forms/PostDropdownBtn.tsx
··· 240 Toast.show(_(msg`Copied to clipboard`), 'clipboard-check') 241 }, [_, richText]) 242 243 - const onPressTranslate = React.useCallback(() => { 244 - openLink(translatorUrl) 245 }, [openLink, translatorUrl]) 246 247 const onHidePost = React.useCallback(() => { ··· 439 <Menu.Item 440 testID="postDropdownSendViaDMBtn" 441 label={_(msg`Send via direct message`)} 442 - onPress={sendViaChatControl.open}> 443 <Menu.ItemText> 444 <Trans>Send via direct message</Trans> 445 </Menu.ItemText> ··· 467 <Menu.Item 468 testID="postDropdownEmbedBtn" 469 label={_(msg`Embed post`)} 470 - onPress={embedPostControl.open}> 471 <Menu.ItemText>{_(msg`Embed post`)}</Menu.ItemText> 472 <Menu.ItemIcon icon={CodeBrackets} position="right" /> 473 </Menu.Item> ··· 542 ? _(msg`Hide reply for me`) 543 : _(msg`Hide post for me`) 544 } 545 - onPress={hidePromptControl.open}> 546 <Menu.ItemText> 547 {isReply 548 ? _(msg`Hide reply for me`) ··· 630 <Menu.Item 631 testID="postDropdownEditPostInteractions" 632 label={_(msg`Edit interaction settings`)} 633 - onPress={postInteractionSettingsDialogControl.open} 634 {...(isAuthor 635 ? Platform.select({ 636 web: { ··· 649 <Menu.Item 650 testID="postDropdownDeleteBtn" 651 label={_(msg`Delete post`)} 652 - onPress={deletePromptControl.open}> 653 <Menu.ItemText>{_(msg`Delete post`)}</Menu.ItemText> 654 <Menu.ItemIcon icon={Trash} position="right" /> 655 </Menu.Item>
··· 240 Toast.show(_(msg`Copied to clipboard`), 'clipboard-check') 241 }, [_, richText]) 242 243 + const onPressTranslate = React.useCallback(async () => { 244 + await openLink(translatorUrl) 245 }, [openLink, translatorUrl]) 246 247 const onHidePost = React.useCallback(() => { ··· 439 <Menu.Item 440 testID="postDropdownSendViaDMBtn" 441 label={_(msg`Send via direct message`)} 442 + onPress={() => sendViaChatControl.open()}> 443 <Menu.ItemText> 444 <Trans>Send via direct message</Trans> 445 </Menu.ItemText> ··· 467 <Menu.Item 468 testID="postDropdownEmbedBtn" 469 label={_(msg`Embed post`)} 470 + onPress={() => embedPostControl.open()}> 471 <Menu.ItemText>{_(msg`Embed post`)}</Menu.ItemText> 472 <Menu.ItemIcon icon={CodeBrackets} position="right" /> 473 </Menu.Item> ··· 542 ? _(msg`Hide reply for me`) 543 : _(msg`Hide post for me`) 544 } 545 + onPress={() => hidePromptControl.open()}> 546 <Menu.ItemText> 547 {isReply 548 ? _(msg`Hide reply for me`) ··· 630 <Menu.Item 631 testID="postDropdownEditPostInteractions" 632 label={_(msg`Edit interaction settings`)} 633 + onPress={() => 634 + postInteractionSettingsDialogControl.open() 635 + } 636 {...(isAuthor 637 ? Platform.select({ 638 web: { ··· 651 <Menu.Item 652 testID="postDropdownDeleteBtn" 653 label={_(msg`Delete post`)} 654 + onPress={() => deletePromptControl.open()}> 655 <Menu.ItemText>{_(msg`Delete post`)}</Menu.ItemText> 656 <Menu.ItemIcon icon={Trash} position="right" /> 657 </Menu.Item>
+3 -2
src/view/com/util/post-ctrls/RepostButton.tsx
··· 86 </Text> 87 ) : undefined} 88 </Button> 89 - <Dialog.Outer control={dialogControl}> 90 <Dialog.Handle /> 91 <Dialog.Inner label={_(msg`Repost or quote post`)}> 92 <View style={a.gap_xl}> ··· 155 </View> 156 <Button 157 label={_(msg`Cancel quote post`)} 158 - onAccessibilityEscape={close} 159 onPress={close} 160 size="large" 161 variant="solid"
··· 86 </Text> 87 ) : undefined} 88 </Button> 89 + <Dialog.Outer 90 + control={dialogControl} 91 + nativeOptions={{preventExpansion: true}}> 92 <Dialog.Handle /> 93 <Dialog.Inner label={_(msg`Repost or quote post`)}> 94 <View style={a.gap_xl}> ··· 157 </View> 158 <Button 159 label={_(msg`Cancel quote post`)} 160 onPress={close} 161 size="large" 162 variant="solid"
-1
src/view/screens/Settings/DisableEmail2FADialog.tsx
··· 79 return ( 80 <Dialog.Outer control={control}> 81 <Dialog.Handle /> 82 - 83 <Dialog.ScrollableInner 84 accessibilityDescribedBy="dialog-description" 85 accessibilityLabelledBy="dialog-title">
··· 79 return ( 80 <Dialog.Outer control={control}> 81 <Dialog.Handle /> 82 <Dialog.ScrollableInner 83 accessibilityDescribedBy="dialog-description" 84 accessibilityLabelledBy="dialog-title">
-1
src/view/screens/Settings/ExportCarDialog.tsx
··· 53 return ( 54 <Dialog.Outer control={control}> 55 <Dialog.Handle /> 56 - 57 <Dialog.ScrollableInner 58 accessibilityDescribedBy="dialog-description" 59 accessibilityLabelledBy="dialog-title">
··· 53 return ( 54 <Dialog.Outer control={control}> 55 <Dialog.Handle /> 56 <Dialog.ScrollableInner 57 accessibilityDescribedBy="dialog-description" 58 accessibilityLabelledBy="dialog-title">
+2 -12
src/view/screens/Storybook/Dialogs.tsx
··· 2 import {View} from 'react-native' 3 import {useNavigation} from '@react-navigation/native' 4 5 import {useDialogStateControlContext} from '#/state/dialogs' 6 - import {NavigationProp} from 'lib/routes/types' 7 import {atoms as a} from '#/alf' 8 import {Button, ButtonText} from '#/components/Button' 9 import * as Dialog from '#/components/Dialog' ··· 179 </Prompt.Outer> 180 181 <Dialog.Outer control={basic}> 182 - <Dialog.Handle /> 183 - 184 <Dialog.Inner label="test"> 185 <H3 nativeID="dialog-title">Dialog</H3> 186 <P nativeID="dialog-description">A basic dialog</P> 187 </Dialog.Inner> 188 </Dialog.Outer> 189 190 - <Dialog.Outer 191 - control={scrollable} 192 - nativeOptions={{sheet: {snapPoints: ['100%']}}}> 193 - <Dialog.Handle /> 194 - 195 <Dialog.ScrollableInner 196 accessibilityDescribedBy="dialog-description" 197 accessibilityLabelledBy="dialog-title"> ··· 230 </Dialog.Outer> 231 232 <Dialog.Outer control={testDialog}> 233 - <Dialog.Handle /> 234 - 235 <Dialog.ScrollableInner 236 accessibilityDescribedBy="dialog-description" 237 accessibilityLabelledBy="dialog-title"> ··· 356 357 {shouldRenderUnmountTest && ( 358 <Dialog.Outer control={unmountTestDialog}> 359 - <Dialog.Handle /> 360 - 361 <Dialog.Inner label="test"> 362 <H3 nativeID="dialog-title">Unmount Test Dialog</H3> 363 <P nativeID="dialog-description">Will unmount in about 5 seconds</P>
··· 2 import {View} from 'react-native' 3 import {useNavigation} from '@react-navigation/native' 4 5 + import {NavigationProp} from '#/lib/routes/types' 6 import {useDialogStateControlContext} from '#/state/dialogs' 7 import {atoms as a} from '#/alf' 8 import {Button, ButtonText} from '#/components/Button' 9 import * as Dialog from '#/components/Dialog' ··· 179 </Prompt.Outer> 180 181 <Dialog.Outer control={basic}> 182 <Dialog.Inner label="test"> 183 <H3 nativeID="dialog-title">Dialog</H3> 184 <P nativeID="dialog-description">A basic dialog</P> 185 </Dialog.Inner> 186 </Dialog.Outer> 187 188 + <Dialog.Outer control={scrollable}> 189 <Dialog.ScrollableInner 190 accessibilityDescribedBy="dialog-description" 191 accessibilityLabelledBy="dialog-title"> ··· 224 </Dialog.Outer> 225 226 <Dialog.Outer control={testDialog}> 227 <Dialog.ScrollableInner 228 accessibilityDescribedBy="dialog-description" 229 accessibilityLabelledBy="dialog-title"> ··· 348 349 {shouldRenderUnmountTest && ( 350 <Dialog.Outer control={unmountTestDialog}> 351 <Dialog.Inner label="test"> 352 <H3 nativeID="dialog-title">Unmount Test Dialog</H3> 353 <P nativeID="dialog-description">Will unmount in about 5 seconds</P>
+24 -53
src/view/shell/Composer.ios.tsx
··· 1 - import React, {useLayoutEffect} from 'react' 2 import {Modal, View} from 'react-native' 3 - import {StatusBar} from 'expo-status-bar' 4 - import * as SystemUI from 'expo-system-ui' 5 6 import {useComposerState} from '#/state/shell/composer' 7 import {atoms as a, useTheme} from '#/alf' 8 - import {getBackgroundColor, useThemeName} from '#/alf/util/useColorModeTheme' 9 import {ComposePost, useComposerCancelRef} from '../com/composer/Composer' 10 11 export function Composer({}: {winHeight: number}) { 12 const t = useTheme() 13 const state = useComposerState() 14 const ref = useComposerCancelRef() 15 16 const open = !!state 17 18 return ( 19 <Modal ··· 24 animationType="slide" 25 onRequestClose={() => ref.current?.onPressCancel()}> 26 <View style={[t.atoms.bg, a.flex_1]}> 27 - <Providers open={open}> 28 - <ComposePost 29 - cancelRef={ref} 30 - replyTo={state?.replyTo} 31 - onPost={state?.onPost} 32 - quote={state?.quote} 33 - quoteCount={state?.quoteCount} 34 - mention={state?.mention} 35 - text={state?.text} 36 - imageUris={state?.imageUris} 37 - videoUri={state?.videoUri} 38 - /> 39 - </Providers> 40 </View> 41 </Modal> 42 ) 43 } 44 - 45 - function Providers({ 46 - children, 47 - open, 48 - }: { 49 - children: React.ReactNode 50 - open: boolean 51 - }) { 52 - // on iOS, it's a native formSheet. We use FullWindowOverlay to make 53 - // the dialogs appear over it 54 - return ( 55 - <> 56 - {children} 57 - <IOSModalBackground active={open} /> 58 - </> 59 - ) 60 - } 61 - 62 - // Generally, the backdrop of the app is the theme color, but when this is open 63 - // we want it to be black due to the modal being a form sheet. 64 - function IOSModalBackground({active}: {active: boolean}) { 65 - const theme = useThemeName() 66 - 67 - useLayoutEffect(() => { 68 - SystemUI.setBackgroundColorAsync('black') 69 - 70 - return () => { 71 - SystemUI.setBackgroundColorAsync(getBackgroundColor(theme)) 72 - } 73 - }, [theme]) 74 - 75 - // Set the status bar to light - however, only if the modal is active 76 - // If we rely on this component being mounted to set this, 77 - // there'll be a delay before it switches back to default. 78 - return active ? <StatusBar style="light" animated /> : null 79 - }
··· 1 + import React from 'react' 2 import {Modal, View} from 'react-native' 3 4 + import {useDialogStateControlContext} from '#/state/dialogs' 5 import {useComposerState} from '#/state/shell/composer' 6 import {atoms as a, useTheme} from '#/alf' 7 import {ComposePost, useComposerCancelRef} from '../com/composer/Composer' 8 9 export function Composer({}: {winHeight: number}) { 10 + const {setFullyExpandedCount} = useDialogStateControlContext() 11 const t = useTheme() 12 const state = useComposerState() 13 const ref = useComposerCancelRef() 14 15 const open = !!state 16 + const prevOpen = React.useRef(open) 17 + 18 + React.useEffect(() => { 19 + if (open && !prevOpen.current) { 20 + setFullyExpandedCount(c => c + 1) 21 + } else if (!open && prevOpen.current) { 22 + setFullyExpandedCount(c => c - 1) 23 + } 24 + prevOpen.current = open 25 + }, [open, setFullyExpandedCount]) 26 27 return ( 28 <Modal ··· 33 animationType="slide" 34 onRequestClose={() => ref.current?.onPressCancel()}> 35 <View style={[t.atoms.bg, a.flex_1]}> 36 + <ComposePost 37 + cancelRef={ref} 38 + replyTo={state?.replyTo} 39 + onPost={state?.onPost} 40 + quote={state?.quote} 41 + quoteCount={state?.quoteCount} 42 + mention={state?.mention} 43 + text={state?.text} 44 + imageUris={state?.imageUris} 45 + videoUri={state?.videoUri} 46 + /> 47 </View> 48 </Modal> 49 ) 50 }
+21 -16
src/view/shell/index.tsx
··· 13 import {StatusBar} from 'expo-status-bar' 14 import {useNavigation, useNavigationState} from '@react-navigation/native' 15 16 import {useSession} from '#/state/session' 17 import { 18 useIsDrawerOpen, ··· 20 useSetDrawerOpen, 21 } from '#/state/shell' 22 import {useCloseAnyActiveElement} from '#/state/util' 23 - import {useDedupe} from 'lib/hooks/useDedupe' 24 - import {useNotificationsHandler} from 'lib/hooks/useNotificationHandler' 25 - import {usePalette} from 'lib/hooks/usePalette' 26 - import {useNotificationsRegistration} from 'lib/notifications/notifications' 27 - import {isStateAtTabRoot} from 'lib/routes/helpers' 28 - import {useTheme} from 'lib/ThemeContext' 29 - import {isAndroid} from 'platform/detection' 30 - import {useDialogStateContext} from 'state/dialogs' 31 - import {Lightbox} from 'view/com/lightbox/Lightbox' 32 - import {ModalsContainer} from 'view/com/modals/Modal' 33 - import {ErrorBoundary} from 'view/com/util/ErrorBoundary' 34 import {MutedWordsDialog} from '#/components/dialogs/MutedWords' 35 import {SigninDialog} from '#/components/dialogs/Signin' 36 import {Outlet as PortalOutlet} from '#/components/Portal' ··· 61 const canGoBack = useNavigationState(state => !isStateAtTabRoot(state)) 62 const {hasSession} = useSession() 63 const closeAnyActiveElement = useCloseAnyActiveElement() 64 - const {importantForAccessibility} = useDialogStateContext() 65 66 useNotificationsRegistration() 67 useNotificationsHandler() ··· 101 102 return ( 103 <> 104 - <Animated.View 105 - style={containerPadding} 106 - importantForAccessibility={importantForAccessibility}> 107 <ErrorBoundary> 108 <Drawer 109 renderDrawerContent={renderDrawerContent} ··· 127 } 128 129 export const Shell: React.FC = function ShellImpl() { 130 const pal = usePalette('default') 131 const theme = useTheme() 132 React.useEffect(() => { ··· 140 }, [theme]) 141 return ( 142 <View testID="mobileShellView" style={[styles.outerContainer, pal.view]}> 143 - <StatusBar style={theme.colorScheme === 'dark' ? 'light' : 'dark'} /> 144 <RoutesContainer> 145 <ShellInner /> 146 </RoutesContainer>
··· 13 import {StatusBar} from 'expo-status-bar' 14 import {useNavigation, useNavigationState} from '@react-navigation/native' 15 16 + import {useDedupe} from '#/lib/hooks/useDedupe' 17 + import {useNotificationsHandler} from '#/lib/hooks/useNotificationHandler' 18 + import {usePalette} from '#/lib/hooks/usePalette' 19 + import {useNotificationsRegistration} from '#/lib/notifications/notifications' 20 + import {isStateAtTabRoot} from '#/lib/routes/helpers' 21 + import {useTheme} from '#/lib/ThemeContext' 22 + import {isAndroid, isIOS} from '#/platform/detection' 23 + import {useDialogStateControlContext} from '#/state/dialogs' 24 import {useSession} from '#/state/session' 25 import { 26 useIsDrawerOpen, ··· 28 useSetDrawerOpen, 29 } from '#/state/shell' 30 import {useCloseAnyActiveElement} from '#/state/util' 31 + import {Lightbox} from '#/view/com/lightbox/Lightbox' 32 + import {ModalsContainer} from '#/view/com/modals/Modal' 33 + import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' 34 import {MutedWordsDialog} from '#/components/dialogs/MutedWords' 35 import {SigninDialog} from '#/components/dialogs/Signin' 36 import {Outlet as PortalOutlet} from '#/components/Portal' ··· 61 const canGoBack = useNavigationState(state => !isStateAtTabRoot(state)) 62 const {hasSession} = useSession() 63 const closeAnyActiveElement = useCloseAnyActiveElement() 64 65 useNotificationsRegistration() 66 useNotificationsHandler() ··· 100 101 return ( 102 <> 103 + <Animated.View style={containerPadding}> 104 <ErrorBoundary> 105 <Drawer 106 renderDrawerContent={renderDrawerContent} ··· 124 } 125 126 export const Shell: React.FC = function ShellImpl() { 127 + const {fullyExpandedCount} = useDialogStateControlContext() 128 const pal = usePalette('default') 129 const theme = useTheme() 130 React.useEffect(() => { ··· 138 }, [theme]) 139 return ( 140 <View testID="mobileShellView" style={[styles.outerContainer, pal.view]}> 141 + <StatusBar 142 + style={ 143 + theme.colorScheme === 'dark' || (isIOS && fullyExpandedCount > 0) 144 + ? 'light' 145 + : 'dark' 146 + } 147 + animated 148 + /> 149 <RoutesContainer> 150 <ShellInner /> 151 </RoutesContainer>
+8 -17
yarn.lock
··· 11450 resolved "https://registry.yarnpkg.com/expo-structured-headers/-/expo-structured-headers-3.8.0.tgz#11797a4c3a7a6770b21126cecffcda148030e361" 11451 integrity sha512-R+gFGn0x5CWl4OVlk2j1bJTJIz4KO8mPoCHpRHmfqMjmrMvrOM0qQSY3V5NHXwp1yT/L2v8aUmFQsBRIdvi1XA== 11452 11453 - expo-system-ui@~3.0.4: 11454 - version "3.0.4" 11455 - resolved "https://registry.yarnpkg.com/expo-system-ui/-/expo-system-ui-3.0.4.tgz#5ace49d38eb03c09a8041b3b82c581a6b974741a" 11456 - integrity sha512-v1n6hBO30k9qw6RE8/au4yNoovs71ExGuXizJUlR5KSo4Ruogpb+0/2q3uRZMDIYWWCANvms8L0UOh6fQJ5TXg== 11457 - dependencies: 11458 - "@react-native/normalize-colors" "~0.74.83" 11459 - debug "^4.3.2" 11460 - 11461 expo-task-manager@~11.8.1: 11462 version "11.8.1" 11463 resolved "https://registry.yarnpkg.com/expo-task-manager/-/expo-task-manager-11.8.1.tgz#33089e78ee3fbd83327fb403bce12d69baf7d21b" ··· 18057 dependencies: 18058 use-latest-callback "^0.1.9" 18059 18060 - react-native-gesture-handler@~2.16.2: 18061 - version "2.16.2" 18062 - resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.16.2.tgz#032bd2a07334292d7f6cff1dc9d1ec928f72e26d" 18063 - integrity sha512-vGFlrDKlmyI+BT+FemqVxmvO7nqxU33cgXVsn6IKAFishvlG3oV2Ds67D5nPkHMea8T+s1IcuMm0bF8ntZtAyg== 18064 dependencies: 18065 "@egjs/hammerjs" "^2.0.17" 18066 hoist-non-react-statics "^3.3.0" 18067 invariant "^2.2.4" 18068 - lodash "^4.17.21" 18069 prop-types "^15.7.2" 18070 18071 react-native-get-random-values@^1.6.0: ··· 18094 dependencies: 18095 "@dominicstop/ts-event-emitter" "^1.1.0" 18096 18097 - react-native-keyboard-controller@^1.12.1: 18098 - version "1.12.1" 18099 - resolved "https://registry.yarnpkg.com/react-native-keyboard-controller/-/react-native-keyboard-controller-1.12.1.tgz#6de22ed4d060528a0dd25621eeaa7f71772ce35f" 18100 - integrity sha512-2OpQcesiYsMilrTzgcTafSGexd9UryRQRuHudIcOn0YaqvvzNpnhVZMVuJMH93fJv/iaZYp3138rgUKOdHhtSw== 18101 18102 react-native-mmkv@^2.12.2: 18103 version "2.12.2"
··· 11450 resolved "https://registry.yarnpkg.com/expo-structured-headers/-/expo-structured-headers-3.8.0.tgz#11797a4c3a7a6770b21126cecffcda148030e361" 11451 integrity sha512-R+gFGn0x5CWl4OVlk2j1bJTJIz4KO8mPoCHpRHmfqMjmrMvrOM0qQSY3V5NHXwp1yT/L2v8aUmFQsBRIdvi1XA== 11452 11453 expo-task-manager@~11.8.1: 11454 version "11.8.1" 11455 resolved "https://registry.yarnpkg.com/expo-task-manager/-/expo-task-manager-11.8.1.tgz#33089e78ee3fbd83327fb403bce12d69baf7d21b" ··· 18049 dependencies: 18050 use-latest-callback "^0.1.9" 18051 18052 + react-native-gesture-handler@2.20.0: 18053 + version "2.20.0" 18054 + resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.20.0.tgz#2d9ec4e9bd22619ebe36269dda3ecb1173928276" 18055 + integrity sha512-rFKqgHRfxQ7uSAivk8vxCiW4SB3G0U7jnv7kZD4Y90K5kp6YrU8Q3tWhxe3Rx55BIvSd3mBe9ZWbWVJ0FsSHPA== 18056 dependencies: 18057 "@egjs/hammerjs" "^2.0.17" 18058 hoist-non-react-statics "^3.3.0" 18059 invariant "^2.2.4" 18060 prop-types "^15.7.2" 18061 18062 react-native-get-random-values@^1.6.0: ··· 18085 dependencies: 18086 "@dominicstop/ts-event-emitter" "^1.1.0" 18087 18088 + react-native-keyboard-controller@^1.14.0: 18089 + version "1.14.0" 18090 + resolved "https://registry.yarnpkg.com/react-native-keyboard-controller/-/react-native-keyboard-controller-1.14.0.tgz#f6faaa12b3736a10f4eec4236ed5b0343508b9a1" 18091 + integrity sha512-JW9k2fehFXOpvLWh1YcgyubLodg/HPi6bR11sCZB/BOawf1tnbGnqk967B8XkxDOKHH6mg+z82quCvv8ALh1rg== 18092 18093 react-native-mmkv@^2.12.2: 18094 version "2.12.2"