+5
jest/jestSetup.js
+5
jest/jestSetup.js
+49
modules/bottom-sheet/android/build.gradle
+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
+
}
+53
modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetModule.kt
+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
+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
+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
+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
+9
modules/bottom-sheet/expo-module.config.json
+13
modules/bottom-sheet/index.ts
+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
+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
+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
+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
+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
+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
+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
+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
+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
+5
modules/bottom-sheet/src/BottomSheet.web.tsx
+2
-3
package.json
+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
+2
-2
src/App.native.tsx
+2
-4
src/alf/util/useColorModeTheme.ts
+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
+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
+5
src/components/Dialog/context.ts
···
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
+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
+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
+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
+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
-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
-7
src/components/KeyboardControllerPadding.tsx
+7
-9
src/components/LikesDialog.tsx
+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
+3
-3
src/components/Link.tsx
+11
-10
src/components/Menu/index.tsx
+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
+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
+2
src/components/Portal.tsx
+11
-7
src/components/Prompt.tsx
+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
-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'
+3
src/components/ReportDialog/SubmitView.tsx
+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
+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
-1
src/components/StarterPack/QrCodeDialog.tsx
+8
-18
src/components/StarterPack/Wizard/WizardEditListDialog.tsx
+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
-1
src/components/TagMenu/index.tsx
-1
src/components/dialogs/BirthDateSettings.tsx
-1
src/components/dialogs/BirthDateSettings.tsx
-1
src/components/dialogs/EmbedConsent.tsx
-1
src/components/dialogs/EmbedConsent.tsx
-255
src/components/dialogs/GifSelect.ios.tsx
-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
-
}
···
+66
-9
src/components/dialogs/GifSelect.tsx
+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
+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
+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
-1
src/components/dialogs/SwitchAccount.tsx
-1
src/components/dialogs/nuxs/NeueTypography.tsx
-1
src/components/dialogs/nuxs/NeueTypography.tsx
+3
-3
src/components/dms/ConvoMenu.tsx
+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
+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
+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
+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
+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()
+1
-1
src/components/forms/Toggle.tsx
+1
-1
src/components/forms/Toggle.tsx
+36
-35
src/components/moderation/LabelsOnMeDialog.tsx
+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
+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
+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
+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
+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
+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
+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
+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
+1
-5
src/view/com/auth/server-input/index.tsx
+294
-274
src/view/com/composer/Composer.tsx
+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
+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
+1
src/view/com/composer/photos/EditImageDialog.web.tsx
+12
-1
src/view/com/composer/photos/Gallery.tsx
+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
+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
+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
-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
+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
+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
+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
+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
+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
+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
-1
src/view/screens/Settings/DisableEmail2FADialog.tsx
-1
src/view/screens/Settings/ExportCarDialog.tsx
-1
src/view/screens/Settings/ExportCarDialog.tsx
+2
-12
src/view/screens/Storybook/Dialogs.tsx
+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
+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
+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
+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"