Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d40413fdcb | |||
| 584c29649f | |||
| 7620b51cf0 | |||
| 54ebc4036e | |||
| fba3cce81e | |||
| 9c309bb579 | |||
| f5b2fe7558 | |||
| 5e41b503ad | |||
| 1b7e66f700 | |||
| cc81342256 | |||
| a32f58753d | |||
| f51d42e44f | |||
| 89ac12516d | |||
| 058ddb0c53 | |||
| 52a8e4d616 | |||
| 14e3567663 | |||
| 79996ac952 | |||
| 4466f68b81 | |||
| c49182009a | |||
| ecf7215a34 | |||
| 0ed5d48e02 | |||
| ede5d0155b | |||
| 0037353528 | |||
| 5b08aad014 | |||
| a2e8f3cbc4 | |||
| 64c9d86488 |
@@ -96,6 +96,11 @@ jest.mock('expo-modules-core', () => ({
|
||||
getIsReducedMotionEnabled: () => false,
|
||||
}
|
||||
}
|
||||
if (moduleName === 'BottomSheet') {
|
||||
return {
|
||||
dismissAll: () => {},
|
||||
}
|
||||
}
|
||||
}),
|
||||
requireNativeViewManager: jest.fn().mockImplementation(moduleName => {
|
||||
return () => null
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
group = 'expo.modules.bottomsheet'
|
||||
version = '0.1.0'
|
||||
|
||||
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
||||
apply from: expoModulesCorePlugin
|
||||
applyKotlinExpoModulesCorePlugin()
|
||||
useCoreDependencies()
|
||||
useExpoPublishing()
|
||||
|
||||
// If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
|
||||
// The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
|
||||
// Most of the time, you may like to manage the Android SDK versions yourself.
|
||||
def useManagedAndroidSdkVersions = false
|
||||
if (useManagedAndroidSdkVersions) {
|
||||
useDefaultAndroidSdkVersions()
|
||||
} else {
|
||||
buildscript {
|
||||
// Simple helper that allows the root project to override versions declared by this library.
|
||||
ext.safeExtGet = { prop, fallback ->
|
||||
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
||||
}
|
||||
}
|
||||
project.android {
|
||||
compileSdkVersion safeExtGet("compileSdkVersion", 34)
|
||||
defaultConfig {
|
||||
minSdkVersion safeExtGet("minSdkVersion", 21)
|
||||
targetSdkVersion safeExtGet("targetSdkVersion", 34)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace "expo.modules.bottomsheet"
|
||||
defaultConfig {
|
||||
versionCode 1
|
||||
versionName "0.1.0"
|
||||
}
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
buildFeatures {
|
||||
compose true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.5.8"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':expo-modules-core')
|
||||
implementation "androidx.compose.material3:material3:1.3.0"
|
||||
implementation "androidx.compose.material:material:1.7.2"
|
||||
implementation "com.facebook.react:react-native:+"
|
||||
implementation("com.composables:core:1.12.0")
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
<manifest>
|
||||
</manifest>
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package expo.modules.bottomsheet
|
||||
|
||||
import android.graphics.Color
|
||||
import expo.modules.kotlin.modules.Module
|
||||
import expo.modules.kotlin.modules.ModuleDefinition
|
||||
|
||||
class BottomSheetModule : Module() {
|
||||
override fun definition() =
|
||||
ModuleDefinition {
|
||||
Name("BottomSheet")
|
||||
|
||||
AsyncFunction("dismissAll") {
|
||||
SheetManager.dismissAll()
|
||||
}
|
||||
|
||||
View(BottomSheetView::class) {
|
||||
Events(
|
||||
arrayOf(
|
||||
"onAttemptDismiss",
|
||||
"onSnapPointChange",
|
||||
"onStateChange",
|
||||
),
|
||||
)
|
||||
|
||||
AsyncFunction("dismiss") { view: BottomSheetView ->
|
||||
view.dismiss()
|
||||
}
|
||||
|
||||
AsyncFunction("updateLayout") { view: BottomSheetView ->
|
||||
view.updateLayout()
|
||||
}
|
||||
|
||||
Prop("containerBackgroundColor") { view: BottomSheetView, prop: String ->
|
||||
view.sheetState.value.containerBackgroundColor = Color.parseColor(prop)
|
||||
}
|
||||
|
||||
Prop("cornerRadius") { view: BottomSheetView, prop: Float ->
|
||||
view.sheetState.value.cornerRadius = prop
|
||||
}
|
||||
|
||||
Prop("minHeight") { view: BottomSheetView, prop: Float ->
|
||||
view.minHeight = prop
|
||||
}
|
||||
|
||||
Prop("maxHeight") { view: BottomSheetView, prop: Float ->
|
||||
view.maxHeight = prop
|
||||
}
|
||||
|
||||
Prop("preventDismiss") { view: BottomSheetView, prop: Boolean ->
|
||||
view.preventDismiss = prop
|
||||
}
|
||||
|
||||
Prop("preventExpansion") { view: BottomSheetView, prop: Boolean ->
|
||||
view.sheetState.value.preventExpansion = prop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package expo.modules.bottomsheet
|
||||
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
class SheetManager {
|
||||
companion object {
|
||||
private val sheets = mutableSetOf<WeakReference<BottomSheetView>>()
|
||||
|
||||
fun add(view: BottomSheetView) {
|
||||
sheets.add(WeakReference(view))
|
||||
}
|
||||
|
||||
fun remove(view: BottomSheetView) {
|
||||
sheets.forEach {
|
||||
if (it.get() == view) {
|
||||
sheets.remove(it)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun dismissAll() {
|
||||
sheets.forEach {
|
||||
it.get()?.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package expo.modules.bottomsheet
|
||||
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.scrollable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.SheetValue
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import com.composables.core.BottomSheet
|
||||
import com.composables.core.SheetDetent
|
||||
import com.composables.core.SheetDetent.Companion.FullyExpanded
|
||||
import com.composables.core.SheetDetent.Companion.Hidden
|
||||
import com.composables.core.rememberBottomSheetState
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.annotations.jvm.Mutable
|
||||
|
||||
data class SheetState(
|
||||
var isOpen: Boolean = false,
|
||||
var cornerRadius: Float? = null,
|
||||
var containerBackgroundColor: Int? = null,
|
||||
var preventExpansion: Boolean = false,
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SheetView(
|
||||
state: MutableState<SheetState>,
|
||||
openState: MutableState<Boolean>,
|
||||
innerView: View,
|
||||
contentHeight: Float,
|
||||
onDismissRequest: () -> Unit,
|
||||
onExpanded: () -> Unit,
|
||||
onHidden: () -> Unit,
|
||||
) {
|
||||
val Peek = SheetDetent(identifier = "peek") { containerHeight, sheetHeight ->
|
||||
containerHeight*0.6f
|
||||
}
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val sheetState = rememberBottomSheetState(
|
||||
initialDetent = Hidden,
|
||||
detents = listOf(Hidden, Peek, FullyExpanded)
|
||||
)
|
||||
|
||||
BottomSheet(
|
||||
state = sheetState,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.shadow(4.dp)
|
||||
.clip(RoundedCornerShape(topStart = state.value.cornerRadius ?: 0f, topEnd = state.value.cornerRadius ?: 0f))
|
||||
.background(Color(state.value.containerBackgroundColor ?: android.graphics.Color.TRANSPARENT))
|
||||
.imePadding()
|
||||
// onDismissRequest = onDismissRequest,
|
||||
// shape =
|
||||
// RoundedCornerShape(
|
||||
// topStart = state.value.cornerRadius ?: 0f,
|
||||
// topEnd = state.value.cornerRadius ?: 0f,
|
||||
// ),
|
||||
// containerColor = Color(state.value.containerBackgroundColor ?: android.graphics.Color.TRANSPARENT),
|
||||
) {
|
||||
AndroidView(
|
||||
factory = { innerView },
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(openState) {
|
||||
Log.d("SheetView", "openState: ${openState.value}")
|
||||
if (openState.value) {
|
||||
sheetState.currentDetent = Peek
|
||||
} else {
|
||||
sheetState.currentDetent = Hidden
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(sheetState.currentDetent) {
|
||||
if (sheetState.currentDetent == Peek || sheetState.currentDetent == FullyExpanded) {
|
||||
onExpanded()
|
||||
} else if (sheetState.currentDetent == Hidden) {
|
||||
onHidden()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"platforms": ["ios", "android"],
|
||||
"ios": {
|
||||
"modules": ["BottomSheetModule"]
|
||||
},
|
||||
"android": {
|
||||
"modules": ["expo.modules.bottomsheet.BottomSheetModule"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import {BottomSheet} from './src/BottomSheet'
|
||||
import {
|
||||
BottomSheetSnapPoint,
|
||||
BottomSheetState,
|
||||
BottomSheetViewProps,
|
||||
} from './src/BottomSheet.types'
|
||||
|
||||
export {
|
||||
BottomSheet,
|
||||
BottomSheetSnapPoint,
|
||||
type BottomSheetState,
|
||||
type BottomSheetViewProps,
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'BottomSheet'
|
||||
s.version = '1.0.0'
|
||||
s.summary = 'A bottom sheet for use in Bluesky'
|
||||
s.description = 'A bottom sheet for use in Bluesky'
|
||||
s.author = ''
|
||||
s.homepage = 'https://github.com/bluesky-social/social-app'
|
||||
s.platforms = { :ios => '15.0', :tvos => '15.0' }
|
||||
s.source = { git: '' }
|
||||
s.static_framework = true
|
||||
|
||||
s.dependency 'ExpoModulesCore'
|
||||
|
||||
# Swift/Objective-C compatibility
|
||||
s.pod_target_xcconfig = {
|
||||
'DEFINES_MODULE' => 'YES',
|
||||
'SWIFT_COMPILATION_MODE' => 'wholemodule'
|
||||
}
|
||||
|
||||
s.source_files = "**/*.{h,m,swift}"
|
||||
end
|
||||
@@ -0,0 +1,47 @@
|
||||
import ExpoModulesCore
|
||||
|
||||
public class BottomSheetModule: Module {
|
||||
public func definition() -> ModuleDefinition {
|
||||
Name("BottomSheet")
|
||||
|
||||
AsyncFunction("dismissAll") {
|
||||
SheetManager.shared.dismissAll()
|
||||
}
|
||||
|
||||
View(SheetView.self) {
|
||||
Events([
|
||||
"onAttemptDismiss",
|
||||
"onSnapPointChange",
|
||||
"onStateChange",
|
||||
])
|
||||
|
||||
AsyncFunction("dismiss") { (view: SheetView) in
|
||||
view.dismiss()
|
||||
}
|
||||
|
||||
AsyncFunction("updateLayout") { (view: SheetView) in
|
||||
view.updateLayout()
|
||||
}
|
||||
|
||||
Prop("cornerRadius") { (view: SheetView, prop: Float) in
|
||||
view.cornerRadius = CGFloat(prop)
|
||||
}
|
||||
|
||||
Prop("minHeight") { (view: SheetView, prop: Double) in
|
||||
view.minHeight = prop
|
||||
}
|
||||
|
||||
Prop("maxHeight") { (view: SheetView, prop: Double) in
|
||||
view.maxHeight = prop
|
||||
}
|
||||
|
||||
Prop("preventDismiss") { (view: SheetView, prop: Bool) in
|
||||
view.preventDismiss = prop
|
||||
}
|
||||
|
||||
Prop("preventExpansion") { (view: SheetView, prop: Bool) in
|
||||
view.preventExpansion = prop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// SheetManager.swift
|
||||
// Pods
|
||||
//
|
||||
// Created by Hailey on 10/1/24.
|
||||
//
|
||||
|
||||
import ExpoModulesCore
|
||||
|
||||
class SheetManager {
|
||||
static let shared = SheetManager()
|
||||
|
||||
private var sheetViews = NSHashTable<SheetView>(options: .weakMemory)
|
||||
|
||||
func add(_ view: SheetView) {
|
||||
sheetViews.add(view)
|
||||
}
|
||||
|
||||
func remove(_ view: SheetView) {
|
||||
sheetViews.remove(view)
|
||||
}
|
||||
|
||||
func dismissAll() {
|
||||
sheetViews.allObjects.forEach { sheetView in
|
||||
sheetView.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
import ExpoModulesCore
|
||||
import UIKit
|
||||
|
||||
class SheetView: ExpoView, UISheetPresentationControllerDelegate {
|
||||
// Views
|
||||
private var sheetVc: SheetViewController?
|
||||
private var innerView: UIView?
|
||||
|
||||
// Events
|
||||
private let onAttemptDismiss = EventDispatcher()
|
||||
private let onSnapPointChange = EventDispatcher()
|
||||
private let onStateChange = EventDispatcher()
|
||||
|
||||
// Open event firing
|
||||
private var isOpen: Bool = false {
|
||||
didSet {
|
||||
onStateChange([
|
||||
"state": isOpen ? "open" : "closed"
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
// React view props
|
||||
var preventDismiss = false
|
||||
var preventExpansion = false
|
||||
var cornerRadius: CGFloat?
|
||||
var minHeight = 0.0
|
||||
var maxHeight: CGFloat! {
|
||||
didSet {
|
||||
let screenHeight = Util.getScreenHeight() ?? 0
|
||||
if maxHeight > screenHeight {
|
||||
maxHeight = screenHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var isOpening = false {
|
||||
didSet {
|
||||
if isOpening {
|
||||
onStateChange([
|
||||
"state": "opening"
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
private var isClosing = false {
|
||||
didSet {
|
||||
if isClosing {
|
||||
onStateChange([
|
||||
"state": "closing"
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
private var selectedDetentIdentifier: UISheetPresentationController.Detent.Identifier? {
|
||||
didSet {
|
||||
if selectedDetentIdentifier == .large {
|
||||
onSnapPointChange([
|
||||
"snapPoint": 2
|
||||
])
|
||||
} else {
|
||||
onSnapPointChange([
|
||||
"snapPoint": 1
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
required init (appContext: AppContext? = nil) {
|
||||
super.init(appContext: appContext)
|
||||
self.maxHeight = Util.getScreenHeight()
|
||||
SheetManager.shared.add(self)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.destroy()
|
||||
}
|
||||
|
||||
// We don't want this view to actually get added to the tree, so we'll simply store it for adding
|
||||
// to the SheetViewController
|
||||
override func insertReactSubview(_ subview: UIView!, at atIndex: Int) {
|
||||
self.innerView = subview
|
||||
}
|
||||
|
||||
// We'll grab the content height from here so we know the initial detent to set
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
guard let innerView = self.innerView else {
|
||||
return
|
||||
}
|
||||
|
||||
if innerView.subviews.count != 1 {
|
||||
return
|
||||
}
|
||||
|
||||
self.present()
|
||||
}
|
||||
|
||||
private func destroy() {
|
||||
self.isClosing = false
|
||||
self.isOpen = false
|
||||
self.sheetVc = nil
|
||||
self.innerView = nil
|
||||
SheetManager.shared.remove(self)
|
||||
}
|
||||
|
||||
// MARK: - Presentation
|
||||
|
||||
func present() {
|
||||
guard !self.isOpen,
|
||||
let innerView = self.innerView,
|
||||
let contentHeight = innerView.subviews.first?.frame.height,
|
||||
let rvc = self.reactViewController() else {
|
||||
return
|
||||
}
|
||||
|
||||
let sheetVc = SheetViewController()
|
||||
sheetVc.setDetents(contentHeight: self.clampHeight(contentHeight), preventExpansion: self.preventExpansion)
|
||||
if let sheet = sheetVc.sheetPresentationController {
|
||||
sheet.delegate = self
|
||||
sheet.preferredCornerRadius = self.cornerRadius
|
||||
if sheet.detents.count == 1 {
|
||||
sheet.selectedDetentIdentifier = .large
|
||||
} else {
|
||||
self.selectedDetentIdentifier = sheet.selectedDetentIdentifier
|
||||
}
|
||||
}
|
||||
sheetVc.view.addSubview(innerView)
|
||||
|
||||
self.sheetVc = sheetVc
|
||||
self.isOpening = true
|
||||
|
||||
rvc.present(sheetVc, animated: true) { [weak self] in
|
||||
self?.isOpening = false
|
||||
self?.isOpen = true
|
||||
}
|
||||
}
|
||||
|
||||
func updateLayout() {
|
||||
if let contentHeight = self.innerView?.subviews.first?.frame.size.height {
|
||||
self.sheetVc?.updateDetents(contentHeight: self.clampHeight(contentHeight),
|
||||
preventExpansion: self.preventExpansion)
|
||||
}
|
||||
}
|
||||
|
||||
func dismiss() {
|
||||
self.isClosing = true
|
||||
self.sheetVc?.dismiss(animated: true) { [weak self] in
|
||||
self?.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Utils
|
||||
|
||||
private func clampHeight(_ height: CGFloat) -> CGFloat {
|
||||
if height < self.minHeight {
|
||||
return self.minHeight
|
||||
} else if height > self.maxHeight {
|
||||
return self.maxHeight
|
||||
}
|
||||
return height
|
||||
}
|
||||
|
||||
// MARK: - UISheetPresentationControllerDelegate
|
||||
|
||||
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
|
||||
self.onAttemptDismiss()
|
||||
return !self.preventDismiss
|
||||
}
|
||||
|
||||
func presentationControllerWillDismiss(_ presentationController: UIPresentationController) {
|
||||
self.isClosing = true
|
||||
}
|
||||
|
||||
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
|
||||
self.destroy()
|
||||
}
|
||||
|
||||
func sheetPresentationControllerDidChangeSelectedDetentIdentifier(_ sheetPresentationController: UISheetPresentationController) {
|
||||
self.selectedDetentIdentifier = sheetPresentationController.selectedDetentIdentifier
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
//
|
||||
// SheetViewController.swift
|
||||
// Pods
|
||||
//
|
||||
// Created by Hailey on 9/30/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
class SheetViewController: UIViewController {
|
||||
init() {
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
self.modalPresentationStyle = .formSheet
|
||||
self.isModalInPresentation = false
|
||||
|
||||
if let sheet = self.sheetPresentationController {
|
||||
sheet.prefersGrabberVisible = true
|
||||
}
|
||||
}
|
||||
|
||||
func setDetents(contentHeight: CGFloat, preventExpansion: Bool) {
|
||||
guard let sheet = self.sheetPresentationController,
|
||||
let screenHeight = Util.getScreenHeight()
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
if contentHeight > screenHeight {
|
||||
sheet.detents = [
|
||||
.large()
|
||||
]
|
||||
} else {
|
||||
if #available(iOS 16.0, *) {
|
||||
sheet.detents = [
|
||||
.custom { _ in
|
||||
return contentHeight
|
||||
}
|
||||
]
|
||||
} else {
|
||||
sheet.detents = [
|
||||
.medium()
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
if !preventExpansion {
|
||||
sheet.detents.append(.large())
|
||||
}
|
||||
}
|
||||
|
||||
func updateDetents(contentHeight: CGFloat, preventExpansion: Bool) {
|
||||
if let sheet = self.sheetPresentationController {
|
||||
sheet.animateChanges {
|
||||
self.setDetents(contentHeight: contentHeight, preventExpansion: preventExpansion)
|
||||
if #available(iOS 16.0, *) {
|
||||
sheet.invalidateDetents()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// Util.swift
|
||||
// Pods
|
||||
//
|
||||
// Created by Hailey on 10/2/24.
|
||||
//
|
||||
|
||||
class Util {
|
||||
static func getScreenHeight() -> CGFloat? {
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let window = windowScene.windows.first {
|
||||
let safeAreaInsets = window.safeAreaInsets
|
||||
let fullScreenHeight = UIScreen.main.bounds.height
|
||||
return fullScreenHeight - (safeAreaInsets.top + safeAreaInsets.bottom)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import * as React from 'react'
|
||||
import {
|
||||
ColorValue,
|
||||
Dimensions,
|
||||
NativeSyntheticEvent,
|
||||
StyleProp,
|
||||
StyleSheet,
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
import {requireNativeModule} from 'expo'
|
||||
import {requireNativeViewManager} from 'expo-modules-core'
|
||||
|
||||
import {BottomSheetState, BottomSheetViewProps} from './BottomSheet.types'
|
||||
|
||||
const screenHeight = Dimensions.get('screen').height
|
||||
|
||||
const NativeView: React.ComponentType<
|
||||
BottomSheetViewProps & {
|
||||
ref: React.RefObject<any>
|
||||
style: StyleProp<ViewStyle>
|
||||
}
|
||||
> = requireNativeViewManager('BottomSheet')
|
||||
|
||||
const NativeModule = requireNativeModule('BottomSheet')
|
||||
|
||||
export class BottomSheet extends React.Component<
|
||||
BottomSheetViewProps,
|
||||
{
|
||||
open: boolean
|
||||
}
|
||||
> {
|
||||
ref = React.createRef<any>()
|
||||
|
||||
constructor(props: BottomSheetViewProps) {
|
||||
super(props)
|
||||
this.state = {
|
||||
open: false,
|
||||
}
|
||||
}
|
||||
|
||||
present() {
|
||||
this.setState({open: true})
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
this.ref.current?.dismiss()
|
||||
}
|
||||
|
||||
private onStateChange = (
|
||||
event: NativeSyntheticEvent<{state: BottomSheetState}>,
|
||||
) => {
|
||||
const {state} = event.nativeEvent
|
||||
const isOpen = state !== 'closed'
|
||||
this.setState({open: isOpen})
|
||||
this.props.onStateChange?.(event)
|
||||
}
|
||||
|
||||
private getBackgroundColor = (): ColorValue | undefined => {
|
||||
const parent = React.Children.toArray(
|
||||
this.props.children,
|
||||
)[0] as React.ReactElement
|
||||
if (parent?.props?.style) {
|
||||
const parentStyle = StyleSheet.flatten(parent.props.style) as ViewStyle
|
||||
return parentStyle.backgroundColor ?? 'transparent'
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
private updateLayout = () => {
|
||||
this.ref.current?.updateLayout()
|
||||
}
|
||||
|
||||
static dismissAll = async () => {
|
||||
await NativeModule.dismissAll()
|
||||
}
|
||||
|
||||
render() {
|
||||
const {children, ...rest} = this.props
|
||||
const topInset = rest.topInset ?? 0
|
||||
const bottomInset = rest.bottomInset ?? 0
|
||||
|
||||
if (!this.state.open) {
|
||||
return null
|
||||
}
|
||||
|
||||
const backgroundColor = this.getBackgroundColor()
|
||||
return (
|
||||
<NativeView
|
||||
{...rest}
|
||||
onStateChange={this.onStateChange}
|
||||
ref={this.ref}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
height: screenHeight - topInset - bottomInset,
|
||||
width: '100%',
|
||||
}}
|
||||
containerBackgroundColor={backgroundColor}>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor,
|
||||
paddingTop: topInset,
|
||||
paddingBottom: bottomInset,
|
||||
}}>
|
||||
<View onLayout={this.updateLayout}>{children}</View>
|
||||
</View>
|
||||
</NativeView>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from 'react'
|
||||
import {ColorValue, NativeSyntheticEvent} from 'react-native'
|
||||
|
||||
export type BottomSheetState = 'closed' | 'closing' | 'open' | 'opening'
|
||||
|
||||
export enum BottomSheetSnapPoint {
|
||||
Hidden,
|
||||
Partial,
|
||||
Full,
|
||||
}
|
||||
|
||||
export interface BottomSheetViewProps {
|
||||
children: React.ReactNode
|
||||
cornerRadius?: number
|
||||
preventDismiss?: boolean
|
||||
preventExpansion?: boolean
|
||||
containerBackgroundColor?: ColorValue
|
||||
topInset?: number
|
||||
bottomInset?: number
|
||||
|
||||
minHeight?: number
|
||||
maxHeight?: number
|
||||
|
||||
onAttemptDismiss?: (event: NativeSyntheticEvent<object>) => void
|
||||
onSnapPointChange?: (
|
||||
event: NativeSyntheticEvent<{snapPoint: BottomSheetSnapPoint}>,
|
||||
) => void
|
||||
onStateChange?: (
|
||||
event: NativeSyntheticEvent<{state: BottomSheetState}>,
|
||||
) => void
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import {BottomSheetViewProps} from './BottomSheet.types'
|
||||
|
||||
export function BottomSheet(_: BottomSheetViewProps) {
|
||||
throw new Error('BottomSheetView is not available on web')
|
||||
}
|
||||
+2
-2
@@ -171,11 +171,11 @@
|
||||
"react-native-compressor": "^1.8.24",
|
||||
"react-native-date-picker": "^4.4.2",
|
||||
"react-native-drawer-layout": "^4.0.0-alpha.3",
|
||||
"react-native-gesture-handler": "~2.16.2",
|
||||
"react-native-gesture-handler": "2.20.0",
|
||||
"react-native-get-random-values": "~1.11.0",
|
||||
"react-native-image-crop-picker": "0.41.2",
|
||||
"react-native-ios-context-menu": "^1.15.3",
|
||||
"react-native-keyboard-controller": "^1.12.1",
|
||||
"react-native-keyboard-controller": "^1.14.0",
|
||||
"react-native-mmkv": "^2.12.2",
|
||||
"react-native-pager-view": "6.2.3",
|
||||
"react-native-picker-select": "^9.1.3",
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import React from 'react'
|
||||
import {StackActions, useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
import {flatten, useTheme} from '#/alf'
|
||||
import {useDialogContext} from '#/components/Dialog'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {InlineLinkProps, useLink} from '#/components/Link'
|
||||
import {NormalizedRNGHPressable} from '#/components/NormalizedRNGHPressable'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {router} from '#/routes'
|
||||
|
||||
export function BottomSheetInlineLinkText({
|
||||
children,
|
||||
to,
|
||||
action = 'push',
|
||||
disableMismatchWarning,
|
||||
style,
|
||||
onPress: outerOnPress,
|
||||
label,
|
||||
shareOnLongPress,
|
||||
disableUnderline,
|
||||
...rest
|
||||
}: InlineLinkProps) {
|
||||
const t = useTheme()
|
||||
const stringChildren = typeof children === 'string'
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const dialog = useDialogContext()
|
||||
|
||||
const {href, isExternal, onLongPress} = useLink({
|
||||
to,
|
||||
displayText: stringChildren ? children : '',
|
||||
action,
|
||||
disableMismatchWarning,
|
||||
onPress: outerOnPress,
|
||||
shareOnLongPress,
|
||||
})
|
||||
const {
|
||||
state: pressed,
|
||||
onIn: onPressIn,
|
||||
onOut: onPressOut,
|
||||
} = useInteractionState()
|
||||
|
||||
const onPress = () => {
|
||||
if (isExternal) {
|
||||
return
|
||||
}
|
||||
|
||||
dialog.close()
|
||||
|
||||
if (action === 'push') {
|
||||
navigation.dispatch(StackActions.push(...router.matchPath(href)))
|
||||
} else if (action === 'replace') {
|
||||
navigation.dispatch(StackActions.replace(...router.matchPath(href)))
|
||||
} else if (action === 'navigate') {
|
||||
// @ts-ignore
|
||||
navigation.navigate(...router.matchPath(href))
|
||||
} else {
|
||||
throw Error('Unsupported navigator action.')
|
||||
}
|
||||
}
|
||||
|
||||
const flattenedStyle = flatten(style) || {}
|
||||
|
||||
// eslint-disable-next-line bsky-internal/avoid-unwrapped-text
|
||||
return (
|
||||
<NormalizedRNGHPressable
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
onPressIn={onPressIn}
|
||||
onPressOut={onPressOut}
|
||||
role="link"
|
||||
accessibilityLabel={label}
|
||||
accessibilityHint=""
|
||||
style={{flexDirection: 'row'}}>
|
||||
<Text
|
||||
{...rest}
|
||||
style={[
|
||||
{color: t.palette.primary_500},
|
||||
pressed &&
|
||||
!disableUnderline && {
|
||||
textDecorationLine: 'underline',
|
||||
textDecorationColor:
|
||||
flattenedStyle.color ?? t.palette.primary_500,
|
||||
},
|
||||
flattenedStyle,
|
||||
]}>
|
||||
{children}
|
||||
</Text>
|
||||
</NormalizedRNGHPressable>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export {Link as BottomSheetInlineLinkText} from './Link'
|
||||
@@ -15,7 +15,9 @@ import {
|
||||
import {LinearGradient} from 'expo-linear-gradient'
|
||||
|
||||
import {atoms as a, flatten, select, tokens, useTheme, web} from '#/alf'
|
||||
import {useDialogContext} from '#/components/Dialog'
|
||||
import {Props as SVGIconProps} from '#/components/icons/common'
|
||||
import {NormalizedRNGHPressable} from '#/components/NormalizedRNGHPressable'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'gradient'
|
||||
@@ -87,6 +89,7 @@ export type ButtonProps = Pick<
|
||||
style?: StyleProp<ViewStyle>
|
||||
hoverStyle?: StyleProp<ViewStyle>
|
||||
children: NonTextElements | ((context: ButtonContext) => NonTextElements)
|
||||
PressableComponent?: React.ComponentType<PressableProps>
|
||||
}
|
||||
|
||||
export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean}
|
||||
@@ -114,10 +117,24 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
disabled = false,
|
||||
style,
|
||||
hoverStyle: hoverStyleProp,
|
||||
PressableComponent,
|
||||
...rest
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
/*
|
||||
* This will pick the correct default pressable to use. If we are inside a
|
||||
* native dialog, we need to use the RNGH pressable.
|
||||
*/
|
||||
const {isNativeDialog} = useDialogContext()
|
||||
if (!PressableComponent) {
|
||||
if (isNativeDialog) {
|
||||
PressableComponent = NormalizedRNGHPressable
|
||||
} else {
|
||||
PressableComponent = Pressable
|
||||
}
|
||||
}
|
||||
|
||||
const t = useTheme()
|
||||
const [state, setState] = React.useState({
|
||||
pressed: false,
|
||||
@@ -449,10 +466,11 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
const flattenedBaseStyles = flatten([baseStyles, style])
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
<PressableComponent
|
||||
role="button"
|
||||
accessibilityHint={undefined} // optional
|
||||
{...rest}
|
||||
// @ts-ignore - this will always be a pressable
|
||||
ref={ref}
|
||||
aria-label={label}
|
||||
aria-pressed={state.pressed}
|
||||
@@ -500,7 +518,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
<Context.Provider value={context}>
|
||||
{typeof children === 'function' ? children(context) : children}
|
||||
</Context.Provider>
|
||||
</Pressable>
|
||||
</PressableComponent>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -6,9 +6,12 @@ import {
|
||||
DialogControlRefProps,
|
||||
DialogOuterProps,
|
||||
} from '#/components/Dialog/types'
|
||||
import {BottomSheetSnapPoint} from '../../../modules/bottom-sheet/src/BottomSheet.types'
|
||||
|
||||
export const Context = React.createContext<DialogContextProps>({
|
||||
close: () => {},
|
||||
isNativeDialog: false,
|
||||
nativeSnapPoint: BottomSheetSnapPoint.Hidden,
|
||||
})
|
||||
|
||||
export function useDialogContext() {
|
||||
|
||||
+77
-214
@@ -1,86 +1,33 @@
|
||||
import React, {useImperativeHandle} from 'react'
|
||||
import {StyleProp, View, ViewStyle} from 'react-native'
|
||||
import {
|
||||
Dimensions,
|
||||
Keyboard,
|
||||
Pressable,
|
||||
StyleProp,
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
import Animated, {useAnimatedStyle} from 'react-native-reanimated'
|
||||
GestureHandlerRootView,
|
||||
ScrollView,
|
||||
TextInput as RNGHTextInput,
|
||||
} from 'react-native-gesture-handler'
|
||||
import {KeyboardAwareScrollView} from 'react-native-keyboard-controller'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import BottomSheet, {
|
||||
BottomSheetBackdropProps,
|
||||
BottomSheetFlatList,
|
||||
BottomSheetFlatListMethods,
|
||||
BottomSheetScrollView,
|
||||
BottomSheetScrollViewMethods,
|
||||
BottomSheetTextInput,
|
||||
BottomSheetView,
|
||||
useBottomSheet,
|
||||
WINDOW_HEIGHT,
|
||||
} from '@discord/bottom-sheet/src'
|
||||
import {BottomSheetFlatListProps} from '@discord/bottom-sheet/src/components/bottomSheetScrollable/types'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {isIOS} from '#/platform/detection'
|
||||
import {useDialogStateControlContext} from '#/state/dialogs'
|
||||
import {List, ListMethods, ListProps} from '#/view/com/util/List'
|
||||
import {atoms as a, flatten, useTheme} from '#/alf'
|
||||
import {Context} from '#/components/Dialog/context'
|
||||
import {Context, useDialogContext} from '#/components/Dialog/context'
|
||||
import {
|
||||
DialogControlProps,
|
||||
DialogInnerProps,
|
||||
DialogOuterProps,
|
||||
} from '#/components/Dialog/types'
|
||||
import {createInput} from '#/components/forms/TextField'
|
||||
import {FullWindowOverlay} from '#/components/FullWindowOverlay'
|
||||
import {Portal} from '#/components/Portal'
|
||||
import {Portal as DefaultPortal} from '#/components/Portal'
|
||||
import {BottomSheet, BottomSheetSnapPoint} from '../../../modules/bottom-sheet'
|
||||
|
||||
export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
|
||||
export * from '#/components/Dialog/types'
|
||||
export * from '#/components/Dialog/utils'
|
||||
// @ts-ignore
|
||||
export const Input = createInput(BottomSheetTextInput)
|
||||
|
||||
function Backdrop(props: BottomSheetBackdropProps) {
|
||||
const t = useTheme()
|
||||
const bottomSheet = useBottomSheet()
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => {
|
||||
const opacity =
|
||||
(Math.abs(WINDOW_HEIGHT - props.animatedPosition.value) - 50) / 1000
|
||||
|
||||
return {
|
||||
opacity: Math.min(Math.max(opacity, 0), 0.55),
|
||||
}
|
||||
})
|
||||
|
||||
const onPress = React.useCallback(() => {
|
||||
bottomSheet.close()
|
||||
}, [bottomSheet])
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[
|
||||
t.atoms.bg_contrast_300,
|
||||
{
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
position: 'absolute',
|
||||
},
|
||||
animatedStyle,
|
||||
]}>
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Dialog backdrop"
|
||||
accessibilityHint="Press the backdrop to close the dialog"
|
||||
style={{flex: 1}}
|
||||
onPress={onPress}
|
||||
/>
|
||||
</Animated.View>
|
||||
)
|
||||
}
|
||||
export const Input = createInput(RNGHTextInput)
|
||||
|
||||
export function Outer({
|
||||
children,
|
||||
@@ -88,24 +35,17 @@ export function Outer({
|
||||
onClose,
|
||||
nativeOptions,
|
||||
testID,
|
||||
Portal = DefaultPortal,
|
||||
}: React.PropsWithChildren<DialogOuterProps>) {
|
||||
const t = useTheme()
|
||||
const sheet = React.useRef<BottomSheet>(null)
|
||||
const sheetOptions = nativeOptions?.sheet || {}
|
||||
const hasSnapPoints = !!sheetOptions.snapPoints
|
||||
const ref = React.useRef<BottomSheet>(null)
|
||||
const insets = useSafeAreaInsets()
|
||||
const closeCallbacks = React.useRef<(() => void)[]>([])
|
||||
const {setDialogIsOpen} = useDialogStateControlContext()
|
||||
|
||||
/*
|
||||
* Used to manage open/closed, but index is otherwise handled internally by `BottomSheet`
|
||||
*/
|
||||
const [openIndex, setOpenIndex] = React.useState(-1)
|
||||
|
||||
/*
|
||||
* `openIndex` is the index of the snap point to open the bottom sheet to. If >0, the bottom sheet is open.
|
||||
*/
|
||||
const isOpen = openIndex > -1
|
||||
const [snapPoint, setSnapPoint] = React.useState<BottomSheetSnapPoint>(
|
||||
BottomSheetSnapPoint.Partial,
|
||||
)
|
||||
|
||||
const callQueuedCallbacks = React.useCallback(() => {
|
||||
for (const cb of closeCallbacks.current) {
|
||||
@@ -119,25 +59,19 @@ export function Outer({
|
||||
closeCallbacks.current = []
|
||||
}, [])
|
||||
|
||||
const open = React.useCallback<DialogControlProps['open']>(
|
||||
({index} = {}) => {
|
||||
// Run any leftover callbacks that might have been queued up before calling `.open()`
|
||||
callQueuedCallbacks()
|
||||
|
||||
setDialogIsOpen(control.id, true)
|
||||
// can be set to any index of `snapPoints`, but `0` is the first i.e. "open"
|
||||
setOpenIndex(index || 0)
|
||||
sheet.current?.snapToIndex(index || 0)
|
||||
},
|
||||
[setDialogIsOpen, control.id, callQueuedCallbacks],
|
||||
)
|
||||
const open = React.useCallback<DialogControlProps['open']>(() => {
|
||||
// Run any leftover callbacks that might have been queued up before calling `.open()`
|
||||
callQueuedCallbacks()
|
||||
setDialogIsOpen(control.id, true)
|
||||
ref.current?.present()
|
||||
}, [setDialogIsOpen, control.id, callQueuedCallbacks])
|
||||
|
||||
// This is the function that we call when we want to dismiss the dialog.
|
||||
const close = React.useCallback<DialogControlProps['close']>(cb => {
|
||||
if (typeof cb === 'function') {
|
||||
closeCallbacks.current.push(cb)
|
||||
}
|
||||
sheet.current?.close()
|
||||
ref.current?.dismiss()
|
||||
}, [])
|
||||
|
||||
// This is the actual thing we are doing once we "confirm" the dialog. We want the dialog's close animation to
|
||||
@@ -146,8 +80,6 @@ export function Outer({
|
||||
// This removes the dialog from our list of stored dialogs. Not super necessary on iOS, but on Android this
|
||||
// tells us that we need to toggle the accessibility overlay setting
|
||||
setDialogIsOpen(control.id, false)
|
||||
setOpenIndex(-1)
|
||||
|
||||
callQueuedCallbacks()
|
||||
onClose?.()
|
||||
}, [callQueuedCallbacks, control.id, onClose, setDialogIsOpen])
|
||||
@@ -161,120 +93,82 @@ export function Outer({
|
||||
[open, close],
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
setDialogIsOpen(control.id, false)
|
||||
}
|
||||
}, [control.id, setDialogIsOpen])
|
||||
const context = React.useMemo(
|
||||
() => ({close, isNativeDialog: true, nativeSnapPoint: snapPoint}),
|
||||
[close, snapPoint],
|
||||
)
|
||||
|
||||
const context = React.useMemo(() => ({close}), [close])
|
||||
const Wrapper = isIOS ? View : GestureHandlerRootView
|
||||
|
||||
return (
|
||||
isOpen && (
|
||||
<Portal>
|
||||
<FullWindowOverlay>
|
||||
<View
|
||||
// iOS
|
||||
accessibilityViewIsModal
|
||||
// Android
|
||||
importantForAccessibility="yes"
|
||||
style={[a.absolute, a.inset_0]}
|
||||
testID={testID}
|
||||
onTouchMove={() => Keyboard.dismiss()}>
|
||||
<BottomSheet
|
||||
enableDynamicSizing={!hasSnapPoints}
|
||||
enablePanDownToClose
|
||||
keyboardBehavior="interactive"
|
||||
android_keyboardInputMode="adjustResize"
|
||||
keyboardBlurBehavior="restore"
|
||||
topInset={insets.top}
|
||||
{...sheetOptions}
|
||||
snapPoints={sheetOptions.snapPoints || ['100%']}
|
||||
ref={sheet}
|
||||
index={openIndex}
|
||||
backgroundStyle={{backgroundColor: 'transparent'}}
|
||||
backdropComponent={Backdrop}
|
||||
handleIndicatorStyle={{backgroundColor: t.palette.primary_500}}
|
||||
handleStyle={{display: 'none'}}
|
||||
onClose={onCloseAnimationComplete}>
|
||||
<Context.Provider value={context}>
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
t.atoms.bg,
|
||||
{
|
||||
borderTopLeftRadius: 40,
|
||||
borderTopRightRadius: 40,
|
||||
height: Dimensions.get('window').height * 2,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{children}
|
||||
</Context.Provider>
|
||||
</BottomSheet>
|
||||
</View>
|
||||
</FullWindowOverlay>
|
||||
</Portal>
|
||||
)
|
||||
<Portal>
|
||||
<Context.Provider value={context}>
|
||||
<BottomSheet
|
||||
ref={ref}
|
||||
topInset={30}
|
||||
bottomInset={insets.bottom}
|
||||
onSnapPointChange={e => {
|
||||
setSnapPoint(e.nativeEvent.snapPoint)
|
||||
}}
|
||||
onStateChange={e => {
|
||||
if (e.nativeEvent.state === 'closed') {
|
||||
onCloseAnimationComplete()
|
||||
}
|
||||
}}
|
||||
cornerRadius={20}
|
||||
{...nativeOptions}>
|
||||
<Wrapper testID={testID} style={[t.atoms.bg]}>
|
||||
{children}
|
||||
</Wrapper>
|
||||
</BottomSheet>
|
||||
</Context.Provider>
|
||||
</Portal>
|
||||
)
|
||||
}
|
||||
|
||||
export function Inner({children, style}: DialogInnerProps) {
|
||||
const insets = useSafeAreaInsets()
|
||||
return (
|
||||
<BottomSheetView
|
||||
<View
|
||||
style={[
|
||||
a.py_xl,
|
||||
a.px_xl,
|
||||
{
|
||||
paddingTop: 40,
|
||||
borderTopLeftRadius: 40,
|
||||
borderTopRightRadius: 40,
|
||||
paddingBottom: insets.bottom + a.pb_5xl.paddingBottom,
|
||||
},
|
||||
flatten(style),
|
||||
style,
|
||||
]}>
|
||||
{children}
|
||||
</BottomSheetView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export const ScrollableInner = React.forwardRef<
|
||||
BottomSheetScrollViewMethods,
|
||||
DialogInnerProps
|
||||
>(function ScrollableInner({children, style}, ref) {
|
||||
const insets = useSafeAreaInsets()
|
||||
return (
|
||||
<BottomSheetScrollView
|
||||
keyboardShouldPersistTaps="handled"
|
||||
style={[
|
||||
a.flex_1, // main diff is this
|
||||
a.p_xl,
|
||||
a.h_full,
|
||||
{
|
||||
paddingTop: 40,
|
||||
borderTopLeftRadius: 40,
|
||||
borderTopRightRadius: 40,
|
||||
},
|
||||
style,
|
||||
]}
|
||||
contentContainerStyle={a.pb_4xl}
|
||||
ref={ref}>
|
||||
{children}
|
||||
<View style={{height: insets.bottom + a.pt_5xl.paddingTop}} />
|
||||
</BottomSheetScrollView>
|
||||
)
|
||||
})
|
||||
export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>(
|
||||
function ScrollableInner({children, style, ...props}, ref) {
|
||||
const insets = useSafeAreaInsets()
|
||||
const {nativeSnapPoint} = useDialogContext()
|
||||
return (
|
||||
<KeyboardAwareScrollView
|
||||
style={[a.px_xl, style]}
|
||||
ref={ref}
|
||||
{...props}
|
||||
bounces={nativeSnapPoint === BottomSheetSnapPoint.Full}
|
||||
nestedScrollEnabled={true}
|
||||
bottomOffset={30}
|
||||
ScrollViewComponent={ScrollView}>
|
||||
{children}
|
||||
<View style={{height: insets.bottom + a.pt_5xl.paddingTop}} />
|
||||
</KeyboardAwareScrollView>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export const InnerFlatList = React.forwardRef<
|
||||
BottomSheetFlatListMethods,
|
||||
BottomSheetFlatListProps<any> & {webInnerStyle?: StyleProp<ViewStyle>}
|
||||
ListMethods,
|
||||
ListProps<any> & {webInnerStyle?: StyleProp<ViewStyle>}
|
||||
>(function InnerFlatList({style, contentContainerStyle, ...props}, ref) {
|
||||
const insets = useSafeAreaInsets()
|
||||
|
||||
return (
|
||||
<BottomSheetFlatList
|
||||
<List
|
||||
keyboardShouldPersistTaps="handled"
|
||||
contentContainerStyle={[a.pb_4xl, flatten(contentContainerStyle)]}
|
||||
ListFooterComponent={
|
||||
@@ -282,42 +176,11 @@ export const InnerFlatList = React.forwardRef<
|
||||
}
|
||||
ref={ref}
|
||||
{...props}
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.p_xl,
|
||||
a.pt_0,
|
||||
a.h_full,
|
||||
{
|
||||
marginTop: 40,
|
||||
},
|
||||
flatten(style),
|
||||
]}
|
||||
style={style}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
export function Handle() {
|
||||
const t = useTheme()
|
||||
|
||||
return (
|
||||
<View style={[a.absolute, a.w_full, a.align_center, a.z_10, {height: 40}]}>
|
||||
<View
|
||||
style={[
|
||||
a.rounded_sm,
|
||||
{
|
||||
top: a.pt_lg.paddingTop,
|
||||
width: 35,
|
||||
height: 4,
|
||||
alignSelf: 'center',
|
||||
backgroundColor: t.palette.contrast_900,
|
||||
opacity: 0.5,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function Close() {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -103,6 +103,8 @@ export function Outer({
|
||||
const context = React.useMemo(
|
||||
() => ({
|
||||
close,
|
||||
isNativeDialog: false,
|
||||
nativeSnapPoint: 0,
|
||||
}),
|
||||
[close],
|
||||
)
|
||||
@@ -229,10 +231,6 @@ export const InnerFlatList = React.forwardRef<
|
||||
)
|
||||
})
|
||||
|
||||
export function Handle() {
|
||||
return null
|
||||
}
|
||||
|
||||
export function Close() {
|
||||
const {_} = useLingui()
|
||||
const {close} = React.useContext(Context)
|
||||
|
||||
@@ -4,9 +4,11 @@ import type {
|
||||
GestureResponderEvent,
|
||||
ScrollViewProps,
|
||||
} from 'react-native'
|
||||
import {BottomSheetProps} from '@discord/bottom-sheet/src'
|
||||
|
||||
import {ViewStyleProp} from '#/alf'
|
||||
import {PortalComponent} from '#/components/Portal'
|
||||
import {BottomSheetViewProps} from '../../../modules/bottom-sheet'
|
||||
import {BottomSheetSnapPoint} from '../../../modules/bottom-sheet/src/BottomSheet.types'
|
||||
|
||||
type A11yProps = Required<AccessibilityProps>
|
||||
|
||||
@@ -37,6 +39,8 @@ export type DialogControlProps = DialogControlRefProps & {
|
||||
|
||||
export type DialogContextProps = {
|
||||
close: DialogControlProps['close']
|
||||
isNativeDialog: boolean
|
||||
nativeSnapPoint: BottomSheetSnapPoint
|
||||
}
|
||||
|
||||
export type DialogControlOpenOptions = {
|
||||
@@ -52,11 +56,10 @@ export type DialogControlOpenOptions = {
|
||||
export type DialogOuterProps = {
|
||||
control: DialogControlProps
|
||||
onClose?: () => void
|
||||
nativeOptions?: {
|
||||
sheet?: Omit<BottomSheetProps, 'children'>
|
||||
}
|
||||
nativeOptions?: Omit<BottomSheetViewProps, 'children'>
|
||||
webOptions?: {}
|
||||
testID?: string
|
||||
Portal?: PortalComponent
|
||||
}
|
||||
|
||||
type DialogInnerPropsBase<T> = React.PropsWithChildren<ViewStyleProp> & T
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import React from 'react'
|
||||
import {useKeyboardHandler} from 'react-native-keyboard-controller'
|
||||
import Animated, {
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
} from 'react-native-reanimated'
|
||||
|
||||
export function KeyboardControllerPadding({maxHeight}: {maxHeight?: number}) {
|
||||
const keyboardHeight = useSharedValue(0)
|
||||
|
||||
useKeyboardHandler(
|
||||
{
|
||||
onMove: e => {
|
||||
'worklet'
|
||||
|
||||
if (maxHeight && e.height > maxHeight) {
|
||||
keyboardHeight.value = maxHeight
|
||||
} else {
|
||||
keyboardHeight.value = e.height
|
||||
}
|
||||
},
|
||||
},
|
||||
[maxHeight],
|
||||
)
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
height: keyboardHeight.value,
|
||||
}))
|
||||
|
||||
return <Animated.View style={animatedStyle} />
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
export function KeyboardControllerPadding({
|
||||
maxHeight: _,
|
||||
}: {
|
||||
maxHeight?: number
|
||||
}) {
|
||||
return null
|
||||
}
|
||||
@@ -1,20 +1,19 @@
|
||||
import React, {useMemo, useCallback} from 'react'
|
||||
import React, {useCallback, useMemo} from 'react'
|
||||
import {ActivityIndicator, FlatList, View} from 'react-native'
|
||||
import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
|
||||
|
||||
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
|
||||
import {useLikedByQuery} from '#/state/queries/post-liked-by'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {logger} from '#/logger'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Text} from '#/components/Typography'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
|
||||
import {useLikedByQuery} from '#/state/queries/post-liked-by'
|
||||
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
|
||||
import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
|
||||
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
interface LikesDialogProps {
|
||||
control: Dialog.DialogOuterProps['control']
|
||||
@@ -24,8 +23,6 @@ interface LikesDialogProps {
|
||||
export function LikesDialog(props: LikesDialogProps) {
|
||||
return (
|
||||
<Dialog.Outer control={props.control}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<LikesDialogInner {...props} />
|
||||
</Dialog.Outer>
|
||||
)
|
||||
|
||||
+43
-11
@@ -5,6 +5,7 @@ import {
|
||||
StyleProp,
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
import {TouchableWithoutFeedback} from 'react-native-gesture-handler'
|
||||
import {sanitizeUrl} from '@braintree/sanitize-url'
|
||||
import {StackActions, useLinkProps} from '@react-navigation/native'
|
||||
|
||||
@@ -24,6 +25,7 @@ import {useModalControls} from '#/state/modals'
|
||||
import {useOpenLink} from '#/state/preferences/in-app-browser'
|
||||
import {atoms as a, flatten, TextStyleProp, useTheme, web} from '#/alf'
|
||||
import {Button, ButtonProps} from '#/components/Button'
|
||||
import {useDialogContext} from '#/components/Dialog'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {Text, TextProps} from '#/components/Typography'
|
||||
import {router} from '#/routes'
|
||||
@@ -103,17 +105,17 @@ export function useLink({
|
||||
linkRequiresWarning(href, displayText),
|
||||
)
|
||||
|
||||
if (requiresWarning) {
|
||||
if (isWeb) {
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
if (requiresWarning) {
|
||||
openModal({
|
||||
name: 'link-warning',
|
||||
text: displayText,
|
||||
href: href,
|
||||
})
|
||||
} else {
|
||||
e.preventDefault()
|
||||
|
||||
if (isExternal) {
|
||||
openLink(href)
|
||||
} else {
|
||||
@@ -285,13 +287,49 @@ export function InlineLinkText({
|
||||
onOut: onPressOut,
|
||||
} = useInteractionState()
|
||||
const flattenedStyle = flatten(style) || {}
|
||||
const {isNativeDialog} = useDialogContext()
|
||||
|
||||
const pressableProps = {
|
||||
accessibilityLabel: label,
|
||||
accessibilityHint: '',
|
||||
accessibilityRole: 'link',
|
||||
role: 'link',
|
||||
onPress: download ? undefined : onPress,
|
||||
onLongPress: onLongPress,
|
||||
onPressIn: onPressIn,
|
||||
onPressOut: onPressOut,
|
||||
} as const
|
||||
|
||||
if (isNativeDialog) {
|
||||
/* eslint-disable-next-line bsky-internal/avoid-unwrapped-text */
|
||||
return (
|
||||
<TouchableWithoutFeedback {...pressableProps}>
|
||||
<Text
|
||||
{...rest}
|
||||
selectable={selectable}
|
||||
style={[
|
||||
{color: t.palette.primary_500},
|
||||
(hovered || focused || pressed) &&
|
||||
!disableUnderline && {
|
||||
...web({outline: 0}),
|
||||
textDecorationLine: 'underline',
|
||||
textDecorationColor:
|
||||
flattenedStyle.color ?? t.palette.primary_500,
|
||||
},
|
||||
flattenedStyle,
|
||||
{lineHeight: 20},
|
||||
]}>
|
||||
{children}
|
||||
</Text>
|
||||
</TouchableWithoutFeedback>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Text
|
||||
selectable={selectable}
|
||||
accessibilityHint=""
|
||||
accessibilityLabel={label}
|
||||
{...rest}
|
||||
{...pressableProps}
|
||||
style={[
|
||||
{color: t.palette.primary_500},
|
||||
(hovered || focused || pressed) &&
|
||||
@@ -302,16 +340,10 @@ export function InlineLinkText({
|
||||
},
|
||||
flattenedStyle,
|
||||
]}
|
||||
role="link"
|
||||
onPress={download ? undefined : onPress}
|
||||
onLongPress={onLongPress}
|
||||
onPressIn={onPressIn}
|
||||
onPressOut={onPressOut}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
onMouseEnter={onHoverIn}
|
||||
onMouseLeave={onHoverOut}
|
||||
accessibilityRole="link"
|
||||
href={href}
|
||||
{...web({
|
||||
hrefAttrs: {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react'
|
||||
import {Pressable, StyleProp, View, ViewStyle} from 'react-native'
|
||||
import {StyleProp, View, ViewStyle} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import flattenReactChildren from 'react-keyed-flatten-children'
|
||||
|
||||
import {isNative} from 'platform/detection'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
ItemTextProps,
|
||||
TriggerProps,
|
||||
} from '#/components/Menu/types'
|
||||
import {NormalizedRNGHPressable} from '#/components/NormalizedRNGHPressable'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export {
|
||||
@@ -84,9 +85,9 @@ export function Outer({
|
||||
const context = React.useContext(Context)
|
||||
|
||||
return (
|
||||
<Dialog.Outer control={context.control}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.Outer
|
||||
control={context.control}
|
||||
nativeOptions={{preventExpansion: true}}>
|
||||
{/* Re-wrap with context since Dialogs are portal-ed to root */}
|
||||
<Context.Provider value={context}>
|
||||
<Dialog.ScrollableInner label="Menu TODO">
|
||||
@@ -112,19 +113,20 @@ export function Item({children, label, style, onPress, ...rest}: ItemProps) {
|
||||
} = useInteractionState()
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
<NormalizedRNGHPressable
|
||||
{...rest}
|
||||
accessibilityHint=""
|
||||
accessibilityLabel={label}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
onPress={e => {
|
||||
control?.close()
|
||||
onPress(e)
|
||||
|
||||
if (!e.defaultPrevented) {
|
||||
control?.close()
|
||||
}
|
||||
}}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
onPressIn={e => {
|
||||
onPressIn()
|
||||
rest.onPressIn?.(e)
|
||||
@@ -149,7 +151,7 @@ export function Item({children, label, style, onPress, ...rest}: ItemProps) {
|
||||
<ItemContext.Provider value={{disabled: Boolean(rest.disabled)}}>
|
||||
{children}
|
||||
</ItemContext.Provider>
|
||||
</Pressable>
|
||||
</NormalizedRNGHPressable>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,12 @@ import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {differenceInSeconds} from 'date-fns'
|
||||
|
||||
import {HITSLOP_10} from '#/lib/constants'
|
||||
import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
|
||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {HITSLOP_10} from 'lib/constants'
|
||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||
import {useSession} from 'state/session'
|
||||
import {useSession} from '#/state/session'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
@@ -78,7 +78,6 @@ export function NewskieDialog({
|
||||
</Button>
|
||||
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Handle />
|
||||
<Dialog.ScrollableInner
|
||||
label={_(msg`New user info dialog`)}
|
||||
style={[{width: 'auto', maxWidth: 400, minWidth: 200}]}>
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
GestureResponderEvent,
|
||||
MeasureOnSuccessCallback,
|
||||
NativeMouseEvent,
|
||||
NativeSyntheticEvent,
|
||||
PressableProps,
|
||||
} from 'react-native'
|
||||
import {Pressable as BSPressable} from 'react-native-gesture-handler'
|
||||
import {PressableEvent} from 'react-native-gesture-handler/lib/typescript/components/Pressable/PressableProps'
|
||||
|
||||
function pressableEventToGestureResponderEvent(
|
||||
event: PressableEvent,
|
||||
target: NormalizedRNGHPressable,
|
||||
): GestureResponderEvent {
|
||||
return {
|
||||
nativeEvent: {
|
||||
...event.nativeEvent,
|
||||
touches: [],
|
||||
changedTouches: [],
|
||||
identifier: event.nativeEvent.identifier.toString(),
|
||||
target: event.nativeEvent.target.toString(),
|
||||
},
|
||||
// @ts-expect-error
|
||||
target: target,
|
||||
// @ts-expect-error
|
||||
currentTarget: target,
|
||||
preventDefault() {},
|
||||
stopPropagation() {},
|
||||
cancelable: false,
|
||||
defaultPrevented: false,
|
||||
eventPhase: 0,
|
||||
isTrusted: false,
|
||||
bubbles: false,
|
||||
timeStamp: event.nativeEvent.timestamp,
|
||||
isDefaultPrevented(): boolean {
|
||||
return false
|
||||
},
|
||||
isPropagationStopped(): boolean {
|
||||
return false
|
||||
},
|
||||
persist() {},
|
||||
type: 'press',
|
||||
}
|
||||
}
|
||||
|
||||
function pressableEventToMouseEvent(
|
||||
event: PressableEvent,
|
||||
target: NormalizedRNGHPressable,
|
||||
): MouseEvent & NativeSyntheticEvent<NativeMouseEvent> {
|
||||
return {
|
||||
...event.nativeEvent,
|
||||
// @ts-expect-error
|
||||
target: target,
|
||||
// @ts-expect-error
|
||||
currentTarget: target,
|
||||
preventDefault() {},
|
||||
stopPropagation() {},
|
||||
cancelable: false,
|
||||
defaultPrevented: false,
|
||||
eventPhase: 0,
|
||||
isTrusted: false,
|
||||
bubbles: false,
|
||||
timeStamp: event.nativeEvent.timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
export class NormalizedRNGHPressable extends React.Component<PressableProps> {
|
||||
static displayName = 'Pressable'
|
||||
|
||||
measure = (_: MeasureOnSuccessCallback) => {}
|
||||
|
||||
measureLayout = (_: number) => {}
|
||||
|
||||
measureInWindow = (
|
||||
_: (x: number, y: number, width: number, height: number) => void,
|
||||
) => {}
|
||||
|
||||
setNativeProps = (_: PressableProps) => {}
|
||||
|
||||
focus = () => {}
|
||||
|
||||
blur = () => {}
|
||||
|
||||
onPress = (event: PressableEvent) => {
|
||||
if (!this.props.onPress) return
|
||||
this.props.onPress(pressableEventToGestureResponderEvent(event, this))
|
||||
}
|
||||
|
||||
onLongPress = (event: PressableEvent) => {
|
||||
if (!this.props.onLongPress) return
|
||||
this.props.onLongPress(pressableEventToGestureResponderEvent(event, this))
|
||||
}
|
||||
|
||||
onPressIn = (event: PressableEvent) => {
|
||||
if (!this.props.onPressIn) return
|
||||
this.props.onPressIn(pressableEventToGestureResponderEvent(event, this))
|
||||
}
|
||||
|
||||
onPressOut = (event: PressableEvent) => {
|
||||
if (!this.props.onPressOut) return
|
||||
this.props.onPressOut(pressableEventToGestureResponderEvent(event, this))
|
||||
}
|
||||
|
||||
onHoverIn = (event: PressableEvent) => {
|
||||
if (!this.props.onHoverIn) return
|
||||
this.props.onHoverIn(pressableEventToMouseEvent(event, this))
|
||||
}
|
||||
|
||||
onHoverOut = (event: PressableEvent) => {
|
||||
if (!this.props.onHoverOut) return
|
||||
this.props.onHoverOut(pressableEventToMouseEvent(event, this))
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<BSPressable
|
||||
accessible={true}
|
||||
accessibilityRole="button"
|
||||
{...this.props}
|
||||
onPress={this.onPress}
|
||||
onLongPress={this.onLongPress}
|
||||
onPressIn={this.onPressIn}
|
||||
onPressOut={this.onPressOut}
|
||||
onHoverIn={this.onHoverIn}
|
||||
onHoverOut={this.onHoverOut}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import {Pressable as NormalizedRNGHPressable} from 'react-native'
|
||||
|
||||
export {NormalizedRNGHPressable}
|
||||
@@ -12,6 +12,8 @@ type ComponentMap = {
|
||||
[id: string]: Component
|
||||
}
|
||||
|
||||
export type PortalComponent = ({children}: {children?: React.ReactNode}) => null
|
||||
|
||||
export function createPortalGroup() {
|
||||
const Context = React.createContext<ContextType>({
|
||||
outlet: null,
|
||||
|
||||
@@ -4,8 +4,9 @@ import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {Button, ButtonColor, ButtonProps, ButtonText} from '#/components/Button'
|
||||
import {Button, ButtonColor, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {PortalComponent} from '#/components/Portal'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export {
|
||||
@@ -25,9 +26,11 @@ export function Outer({
|
||||
children,
|
||||
control,
|
||||
testID,
|
||||
Portal,
|
||||
}: React.PropsWithChildren<{
|
||||
control: Dialog.DialogControlProps
|
||||
testID?: string
|
||||
Portal?: PortalComponent
|
||||
}>) {
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const titleId = React.useId()
|
||||
@@ -39,10 +42,8 @@ export function Outer({
|
||||
)
|
||||
|
||||
return (
|
||||
<Dialog.Outer control={control} testID={testID}>
|
||||
<Dialog.Outer control={control} testID={testID} Portal={Portal}>
|
||||
<Context.Provider value={context}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.ScrollableInner
|
||||
accessibilityLabelledBy={titleId}
|
||||
accessibilityDescribedBy={descriptionId}
|
||||
@@ -141,7 +142,7 @@ export function Action({
|
||||
* Note: The dialog will close automatically when the action is pressed, you
|
||||
* should NOT close the dialog as a side effect of this method.
|
||||
*/
|
||||
onPress: ButtonProps['onPress']
|
||||
onPress: (e: GestureResponderEvent) => void
|
||||
color?: ButtonColor
|
||||
/**
|
||||
* Optional i18n string. If undefined, it will default to "Confirm".
|
||||
@@ -181,6 +182,7 @@ export function Basic({
|
||||
onConfirm,
|
||||
confirmButtonColor,
|
||||
showCancel = true,
|
||||
Portal,
|
||||
}: React.PropsWithChildren<{
|
||||
control: Dialog.DialogOuterProps['control']
|
||||
title: string
|
||||
@@ -194,12 +196,13 @@ export function Basic({
|
||||
* Note: The dialog will close automatically when the action is pressed, you
|
||||
* should NOT close the dialog as a side effect of this method.
|
||||
*/
|
||||
onConfirm: ButtonProps['onPress']
|
||||
onConfirm: (e: GestureResponderEvent) => void
|
||||
confirmButtonColor?: ButtonColor
|
||||
showCancel?: boolean
|
||||
Portal?: PortalComponent
|
||||
}>) {
|
||||
return (
|
||||
<Outer control={control} testID="confirmModal">
|
||||
<Outer control={control} testID="confirmModal" Portal={Portal}>
|
||||
<TitleText>{title}</TitleText>
|
||||
<DescriptionText>{description}</DescriptionText>
|
||||
<Actions>
|
||||
|
||||
@@ -4,7 +4,6 @@ import {AppBskyLabelerDefs} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
export {useDialogControl as useReportDialogControl} from '#/components/Dialog'
|
||||
import {getLabelingServiceTitle} from '#/lib/moderation'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {Button, useButtonContext} from '#/components/Button'
|
||||
|
||||
@@ -16,6 +16,7 @@ import * as Toggle from '#/components/forms/Toggle'
|
||||
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {NormalizedRNGHPressable} from '#/components/NormalizedRNGHPressable'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {ReportDialogProps} from './types'
|
||||
|
||||
@@ -153,7 +154,8 @@ export function SubmitView({
|
||||
<Toggle.Item
|
||||
key={labeler.creator.did}
|
||||
name={labeler.creator.did}
|
||||
label={title}>
|
||||
label={title}
|
||||
PressableComponent={NormalizedRNGHPressable}>
|
||||
<LabelerToggle title={title} />
|
||||
</Toggle.Item>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react'
|
||||
import {Pressable, View} from 'react-native'
|
||||
import {ScrollView} from 'react-native-gesture-handler'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
@@ -8,12 +9,10 @@ import {useMyLabelersQuery} from '#/state/queries/preferences'
|
||||
export {useDialogControl as useReportDialogControl} from '#/components/Dialog'
|
||||
|
||||
import {AppBskyLabelerDefs} from '@atproto/api'
|
||||
import {BottomSheetScrollViewMethods} from '@discord/bottom-sheet/src'
|
||||
|
||||
import {atoms as a} from '#/alf'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {useDelayedLoading} from '#/components/hooks/useDelayedLoading'
|
||||
import {useOnKeyboardDidShow} from '#/components/hooks/useOnKeyboard'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {SelectLabelerView} from './SelectLabelerView'
|
||||
@@ -24,8 +23,6 @@ import {ReportDialogProps} from './types'
|
||||
export function ReportDialog(props: ReportDialogProps) {
|
||||
return (
|
||||
<Dialog.Outer control={props.control}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<ReportDialogInner {...props} />
|
||||
</Dialog.Outer>
|
||||
)
|
||||
@@ -40,10 +37,7 @@ function ReportDialogInner(props: ReportDialogProps) {
|
||||
} = useMyLabelersQuery()
|
||||
const isLoading = useDelayedLoading(500, isLabelerLoading)
|
||||
|
||||
const ref = React.useRef<BottomSheetScrollViewMethods>(null)
|
||||
useOnKeyboardDidShow(() => {
|
||||
ref.current?.scrollToEnd({animated: true})
|
||||
})
|
||||
const ref = React.useRef<ScrollView>(null)
|
||||
|
||||
return (
|
||||
<Dialog.ScrollableInner label={_(msg`Report dialog`)} ref={ref}>
|
||||
|
||||
@@ -149,7 +149,6 @@ export function QrCodeDialog({
|
||||
|
||||
return (
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Handle />
|
||||
<Dialog.ScrollableInner
|
||||
label={_(msg`Create a QR code for a starter pack`)}>
|
||||
<View style={[a.flex_1, a.align_center, a.gap_5xl]}>
|
||||
|
||||
@@ -6,14 +6,14 @@ import {AppBskyGraphDefs} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {saveImageToMediaLibrary} from '#/lib/media/manip'
|
||||
import {shareUrl} from '#/lib/sharing'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {getStarterPackOgCard} from '#/lib/strings/starter-pack'
|
||||
import {logger} from '#/logger'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {saveImageToMediaLibrary} from 'lib/media/manip'
|
||||
import {shareUrl} from 'lib/sharing'
|
||||
import {logEvent} from 'lib/statsig/statsig'
|
||||
import {getStarterPackOgCard} from 'lib/strings/starter-pack'
|
||||
import {isNative, isWeb} from 'platform/detection'
|
||||
import * as Toast from 'view/com/util/Toast'
|
||||
import {isNative, isWeb} from '#/platform/detection'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import {DialogControlProps} from '#/components/Dialog'
|
||||
@@ -84,7 +84,6 @@ function ShareDialogInner({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog.Handle />
|
||||
<Dialog.ScrollableInner label={_(msg`Share link dialog`)}>
|
||||
{!imageLoaded || !link ? (
|
||||
<View style={[a.p_xl, a.align_center]}>
|
||||
|
||||
@@ -3,13 +3,13 @@ import type {ListRenderItemInfo} from 'react-native'
|
||||
import {View} from 'react-native'
|
||||
import {AppBskyActorDefs, ModerationOpts} from '@atproto/api'
|
||||
import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
|
||||
import {BottomSheetFlatListMethods} from '@discord/bottom-sheet'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {useSession} from 'state/session'
|
||||
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useSession} from '#/state/session'
|
||||
import {ListMethods} from '#/view/com/util/List'
|
||||
import {WizardAction, WizardState} from '#/screens/StarterPack/Wizard/State'
|
||||
import {atoms as a, native, useTheme, web} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
@@ -45,7 +45,7 @@ export function WizardEditListDialog({
|
||||
const {currentAccount} = useSession()
|
||||
const initialNumToRender = useInitialNumToRender()
|
||||
|
||||
const listRef = useRef<BottomSheetFlatListMethods>(null)
|
||||
const listRef = useRef<ListMethods>(null)
|
||||
|
||||
const getData = () => {
|
||||
if (state.currentStep === 'Feeds') return state.feeds
|
||||
@@ -76,11 +76,7 @@ export function WizardEditListDialog({
|
||||
)
|
||||
|
||||
return (
|
||||
<Dialog.Outer
|
||||
control={control}
|
||||
testID="newChatDialog"
|
||||
nativeOptions={{sheet: {snapPoints: ['95%']}}}>
|
||||
<Dialog.Handle />
|
||||
<Dialog.Outer control={control} testID="newChatDialog">
|
||||
<Dialog.InnerFlatList
|
||||
ref={listRef}
|
||||
data={getData()}
|
||||
@@ -103,13 +99,7 @@ export function WizardEditListDialog({
|
||||
height: 48,
|
||||
},
|
||||
]
|
||||
: [
|
||||
a.pb_sm,
|
||||
a.align_end,
|
||||
{
|
||||
height: 68,
|
||||
},
|
||||
],
|
||||
: [a.pb_sm, a.align_end],
|
||||
]}>
|
||||
<View style={{width: 60}} />
|
||||
<Text style={[a.font_bold, a.text_xl]}>
|
||||
@@ -143,8 +133,6 @@ export function WizardEditListDialog({
|
||||
paddingHorizontal: 0,
|
||||
marginTop: 0,
|
||||
paddingTop: 0,
|
||||
borderTopLeftRadius: 40,
|
||||
borderTopRightRadius: 40,
|
||||
}),
|
||||
]}
|
||||
webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]}
|
||||
|
||||
@@ -84,8 +84,6 @@ export function TagMenu({
|
||||
{children}
|
||||
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.Inner label={_(msg`Tag menu: ${displayTag}`)}>
|
||||
{isPreferencesLoading ? (
|
||||
<View style={[a.w_full, a.align_center]}>
|
||||
|
||||
@@ -170,7 +170,6 @@ function WhoCanReplyDialog({
|
||||
const {_} = useLingui()
|
||||
return (
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Handle />
|
||||
<Dialog.ScrollableInner
|
||||
label={_(msg`Dialog: adjust who can interact with this post`)}
|
||||
style={[{width: 'auto', maxWidth: 400, minWidth: 200}]}>
|
||||
|
||||
@@ -30,8 +30,6 @@ export function BirthDateSettingsDialog({
|
||||
|
||||
return (
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.ScrollableInner label={_(msg`My Birthday`)}>
|
||||
<View style={[a.gap_sm, a.pb_lg]}>
|
||||
<Text style={[a.text_2xl, a.font_bold]}>
|
||||
|
||||
@@ -27,7 +27,6 @@ type EmbedDialogProps = {
|
||||
let EmbedDialog = ({control, ...rest}: EmbedDialogProps): React.ReactNode => {
|
||||
return (
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Handle />
|
||||
<EmbedDialogInner {...rest} />
|
||||
</Dialog.Outer>
|
||||
)
|
||||
|
||||
@@ -49,8 +49,6 @@ export function EmbedConsentDialog({
|
||||
|
||||
return (
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.ScrollableInner
|
||||
label={_(msg`External Media`)}
|
||||
style={[gtMobile ? {width: 'auto', maxWidth: 400} : a.w_full]}>
|
||||
|
||||
@@ -22,7 +22,6 @@ import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import * as TextField from '#/components/forms/TextField'
|
||||
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
|
||||
import {Button, ButtonText} from '../Button'
|
||||
import {Handle} from '../Dialog'
|
||||
import {useThrottledValue} from '../hooks/useThrottledValue'
|
||||
import {ListFooter, ListMaybePlaceholder} from '../Lists'
|
||||
import {GifPreview} from './GifSelect.shared'
|
||||
@@ -70,7 +69,6 @@ export function GifSelectDialog({
|
||||
aria-modal
|
||||
accessibilityViewIsModal>
|
||||
<View style={[a.flex_1, t.atoms.bg]}>
|
||||
<Handle />
|
||||
<ErrorBoundary renderError={renderErrorBoundary}>
|
||||
<GifList onSelectGif={onSelectGif} close={close} />
|
||||
</ErrorBoundary>
|
||||
|
||||
@@ -6,7 +6,6 @@ import React, {
|
||||
useState,
|
||||
} from 'react'
|
||||
import {TextInput, View} from 'react-native'
|
||||
import {BottomSheetFlatListMethods} from '@discord/bottom-sheet'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
@@ -19,6 +18,7 @@ import {
|
||||
} from '#/state/queries/tenor'
|
||||
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
|
||||
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
|
||||
import {ListMethods} from '#/view/com/util/List'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import * as TextField from '#/components/forms/TextField'
|
||||
@@ -57,11 +57,7 @@ export function GifSelectDialog({
|
||||
)
|
||||
|
||||
return (
|
||||
<Dialog.Outer
|
||||
control={control}
|
||||
nativeOptions={{sheet: {snapPoints: ['100%']}}}
|
||||
onClose={onClose}>
|
||||
<Dialog.Handle />
|
||||
<Dialog.Outer control={control} onClose={onClose}>
|
||||
<ErrorBoundary renderError={renderErrorBoundary}>
|
||||
<GifList control={control} onSelectGif={onSelectGif} />
|
||||
</ErrorBoundary>
|
||||
@@ -80,7 +76,7 @@ function GifList({
|
||||
const t = useTheme()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const textInputRef = useRef<TextInput>(null)
|
||||
const listRef = useRef<BottomSheetFlatListMethods>(null)
|
||||
const listRef = useRef<ListMethods>(null)
|
||||
const [undeferredSearch, setSearch] = useState('')
|
||||
const search = useThrottledValue(undeferredSearch, 500)
|
||||
|
||||
|
||||
@@ -30,16 +30,19 @@ import {PageText_Stroke2_Corner0_Rounded as PageText} from '#/components/icons/P
|
||||
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {NormalizedRNGHPressable} from '#/components/NormalizedRNGHPressable'
|
||||
import {createPortalGroup} from '#/components/Portal'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
const ONE_DAY = 24 * 60 * 60 * 1000
|
||||
|
||||
const Portal = createPortalGroup()
|
||||
|
||||
export function MutedWordsDialog() {
|
||||
const {mutedWordsDialogControl: control} = useGlobalDialogsControlContext()
|
||||
return (
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Handle />
|
||||
<MutedWordsInner />
|
||||
</Dialog.Outer>
|
||||
)
|
||||
@@ -105,307 +108,356 @@ function MutedWordsInner() {
|
||||
}, [_, field, targets, addMutedWord, setField, durations, excludeFollowing])
|
||||
|
||||
return (
|
||||
<Dialog.ScrollableInner label={_(msg`Manage your muted words and tags`)}>
|
||||
<View>
|
||||
<Text
|
||||
style={[a.text_md, a.font_bold, a.pb_sm, t.atoms.text_contrast_high]}>
|
||||
<Trans>Add muted words and tags</Trans>
|
||||
</Text>
|
||||
<Text style={[a.pb_lg, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||
<Trans>
|
||||
Posts can be muted based on their text, their tags, or both. We
|
||||
recommend avoiding common words that appear in many posts, since it
|
||||
can result in no posts being shown.
|
||||
</Trans>
|
||||
</Text>
|
||||
|
||||
<View style={[a.pb_sm]}>
|
||||
<Dialog.Input
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
autoComplete="off"
|
||||
label={_(msg`Enter a word or tag`)}
|
||||
placeholder={_(msg`Enter a word or tag`)}
|
||||
value={field}
|
||||
onChangeText={value => {
|
||||
if (error) {
|
||||
setError('')
|
||||
}
|
||||
setField(value)
|
||||
}}
|
||||
onSubmitEditing={submit}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={[a.pb_xl, a.gap_sm]}>
|
||||
<Toggle.Group
|
||||
label={_(msg`Select how long to mute this word for.`)}
|
||||
type="radio"
|
||||
values={durations}
|
||||
onChange={setDurations}>
|
||||
<Text
|
||||
style={[
|
||||
a.pb_xs,
|
||||
a.text_sm,
|
||||
a.font_bold,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
<Trans>Duration:</Trans>
|
||||
</Text>
|
||||
|
||||
<View
|
||||
style={[
|
||||
gtMobile && [a.flex_row, a.align_center, a.justify_start],
|
||||
a.gap_sm,
|
||||
]}>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.flex_row,
|
||||
a.justify_start,
|
||||
a.align_center,
|
||||
a.gap_sm,
|
||||
]}>
|
||||
<Toggle.Item
|
||||
label={_(msg`Mute this word until you unmute it`)}
|
||||
name="forever"
|
||||
style={[a.flex_1]}>
|
||||
<TargetToggle>
|
||||
<View
|
||||
style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
|
||||
<Toggle.Radio />
|
||||
<Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
|
||||
<Trans>Forever</Trans>
|
||||
</Toggle.LabelText>
|
||||
</View>
|
||||
</TargetToggle>
|
||||
</Toggle.Item>
|
||||
|
||||
<Toggle.Item
|
||||
label={_(msg`Mute this word for 24 hours`)}
|
||||
name="24_hours"
|
||||
style={[a.flex_1]}>
|
||||
<TargetToggle>
|
||||
<View
|
||||
style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
|
||||
<Toggle.Radio />
|
||||
<Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
|
||||
<Trans>24 hours</Trans>
|
||||
</Toggle.LabelText>
|
||||
</View>
|
||||
</TargetToggle>
|
||||
</Toggle.Item>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.flex_row,
|
||||
a.justify_start,
|
||||
a.align_center,
|
||||
a.gap_sm,
|
||||
]}>
|
||||
<Toggle.Item
|
||||
label={_(msg`Mute this word for 7 days`)}
|
||||
name="7_days"
|
||||
style={[a.flex_1]}>
|
||||
<TargetToggle>
|
||||
<View
|
||||
style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
|
||||
<Toggle.Radio />
|
||||
<Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
|
||||
<Trans>7 days</Trans>
|
||||
</Toggle.LabelText>
|
||||
</View>
|
||||
</TargetToggle>
|
||||
</Toggle.Item>
|
||||
|
||||
<Toggle.Item
|
||||
label={_(msg`Mute this word for 30 days`)}
|
||||
name="30_days"
|
||||
style={[a.flex_1]}>
|
||||
<TargetToggle>
|
||||
<View
|
||||
style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
|
||||
<Toggle.Radio />
|
||||
<Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
|
||||
<Trans>30 days</Trans>
|
||||
</Toggle.LabelText>
|
||||
</View>
|
||||
</TargetToggle>
|
||||
</Toggle.Item>
|
||||
</View>
|
||||
</View>
|
||||
</Toggle.Group>
|
||||
|
||||
<Toggle.Group
|
||||
label={_(msg`Select what content this mute word should apply to.`)}
|
||||
type="radio"
|
||||
values={targets}
|
||||
onChange={setTargets}>
|
||||
<Text
|
||||
style={[
|
||||
a.pb_xs,
|
||||
a.text_sm,
|
||||
a.font_bold,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
<Trans>Mute in:</Trans>
|
||||
</Text>
|
||||
|
||||
<View style={[a.flex_row, a.align_center, a.gap_sm, a.flex_wrap]}>
|
||||
<Toggle.Item
|
||||
label={_(msg`Mute this word in post text and tags`)}
|
||||
name="content"
|
||||
style={[a.flex_1]}>
|
||||
<TargetToggle>
|
||||
<View
|
||||
style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
|
||||
<Toggle.Radio />
|
||||
<Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
|
||||
<Trans>Text & tags</Trans>
|
||||
</Toggle.LabelText>
|
||||
</View>
|
||||
<PageText size="sm" />
|
||||
</TargetToggle>
|
||||
</Toggle.Item>
|
||||
|
||||
<Toggle.Item
|
||||
label={_(msg`Mute this word in tags only`)}
|
||||
name="tag"
|
||||
style={[a.flex_1]}>
|
||||
<TargetToggle>
|
||||
<View
|
||||
style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
|
||||
<Toggle.Radio />
|
||||
<Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
|
||||
<Trans>Tags only</Trans>
|
||||
</Toggle.LabelText>
|
||||
</View>
|
||||
<Hashtag size="sm" />
|
||||
</TargetToggle>
|
||||
</Toggle.Item>
|
||||
</View>
|
||||
</Toggle.Group>
|
||||
|
||||
<View>
|
||||
<Text
|
||||
style={[
|
||||
a.pb_xs,
|
||||
a.text_sm,
|
||||
a.font_bold,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
<Trans>Options:</Trans>
|
||||
</Text>
|
||||
<Toggle.Item
|
||||
label={_(msg`Do not apply this mute word to users you follow`)}
|
||||
name="exclude_following"
|
||||
style={[a.flex_row, a.justify_between]}
|
||||
value={excludeFollowing}
|
||||
onChange={setExcludeFollowing}>
|
||||
<TargetToggle>
|
||||
<View style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
|
||||
<Toggle.Checkbox />
|
||||
<Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
|
||||
<Trans>Exclude users you follow</Trans>
|
||||
</Toggle.LabelText>
|
||||
</View>
|
||||
</TargetToggle>
|
||||
</Toggle.Item>
|
||||
</View>
|
||||
|
||||
<View style={[a.pt_xs]}>
|
||||
<Button
|
||||
disabled={isPending || !field}
|
||||
label={_(msg`Add mute word for configured settings`)}
|
||||
size="large"
|
||||
color="primary"
|
||||
variant="solid"
|
||||
style={[]}
|
||||
onPress={submit}>
|
||||
<ButtonText>
|
||||
<Trans>Add</Trans>
|
||||
</ButtonText>
|
||||
<ButtonIcon icon={isPending ? Loader : Plus} position="right" />
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
{error && (
|
||||
<View
|
||||
style={[
|
||||
a.mb_lg,
|
||||
a.flex_row,
|
||||
a.rounded_sm,
|
||||
a.p_md,
|
||||
a.mb_xs,
|
||||
t.atoms.bg_contrast_25,
|
||||
{
|
||||
backgroundColor: t.palette.negative_400,
|
||||
},
|
||||
]}>
|
||||
<Text
|
||||
style={[
|
||||
a.italic,
|
||||
{color: t.palette.white},
|
||||
native({marginTop: 2}),
|
||||
]}>
|
||||
{error}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<Divider />
|
||||
|
||||
<View style={[a.pt_2xl]}>
|
||||
<Portal.Provider>
|
||||
<Dialog.ScrollableInner label={_(msg`Manage your muted words and tags`)}>
|
||||
<View>
|
||||
<Text
|
||||
style={[
|
||||
a.text_md,
|
||||
a.font_bold,
|
||||
a.pb_md,
|
||||
a.pb_sm,
|
||||
t.atoms.text_contrast_high,
|
||||
]}>
|
||||
<Trans>Your muted words</Trans>
|
||||
<Trans>Add muted words and tags</Trans>
|
||||
</Text>
|
||||
<Text style={[a.pb_lg, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||
<Trans>
|
||||
Posts can be muted based on their text, their tags, or both. We
|
||||
recommend avoiding common words that appear in many posts, since
|
||||
it can result in no posts being shown.
|
||||
</Trans>
|
||||
</Text>
|
||||
|
||||
{isPreferencesLoading ? (
|
||||
<Loader />
|
||||
) : preferencesError || !preferences ? (
|
||||
<View
|
||||
style={[a.py_md, a.px_lg, a.rounded_md, t.atoms.bg_contrast_25]}>
|
||||
<Text style={[a.italic, t.atoms.text_contrast_high]}>
|
||||
<Trans>
|
||||
We're sorry, but we weren't able to load your muted words at
|
||||
this time. Please try again.
|
||||
</Trans>
|
||||
<View style={[a.pb_sm]}>
|
||||
<Dialog.Input
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
autoComplete="off"
|
||||
label={_(msg`Enter a word or tag`)}
|
||||
placeholder={_(msg`Enter a word or tag`)}
|
||||
value={field}
|
||||
onChangeText={value => {
|
||||
if (error) {
|
||||
setError('')
|
||||
}
|
||||
setField(value)
|
||||
}}
|
||||
onSubmitEditing={submit}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={[a.pb_xl, a.gap_sm]}>
|
||||
<Toggle.Group
|
||||
label={_(msg`Select how long to mute this word for.`)}
|
||||
type="radio"
|
||||
values={durations}
|
||||
onChange={setDurations}>
|
||||
<Text
|
||||
style={[
|
||||
a.pb_xs,
|
||||
a.text_sm,
|
||||
a.font_bold,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
<Trans>Duration:</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
) : preferences.moderationPrefs.mutedWords.length ? (
|
||||
[...preferences.moderationPrefs.mutedWords]
|
||||
.reverse()
|
||||
.map((word, i) => (
|
||||
<MutedWordRow
|
||||
key={word.value + i}
|
||||
word={word}
|
||||
style={[i % 2 === 0 && t.atoms.bg_contrast_25]}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<View
|
||||
style={[a.py_md, a.px_lg, a.rounded_md, t.atoms.bg_contrast_25]}>
|
||||
<Text style={[a.italic, t.atoms.text_contrast_high]}>
|
||||
<Trans>You haven't muted any words or tags yet</Trans>
|
||||
|
||||
<View
|
||||
style={[
|
||||
gtMobile && [a.flex_row, a.align_center, a.justify_start],
|
||||
a.gap_sm,
|
||||
]}>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.flex_row,
|
||||
a.justify_start,
|
||||
a.align_center,
|
||||
a.gap_sm,
|
||||
]}>
|
||||
<Toggle.Item
|
||||
label={_(msg`Mute this word until you unmute it`)}
|
||||
name="forever"
|
||||
style={[a.flex_1]}
|
||||
PressableComponent={NormalizedRNGHPressable}>
|
||||
<TargetToggle>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.gap_sm,
|
||||
]}>
|
||||
<Toggle.Radio />
|
||||
<Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
|
||||
<Trans>Forever</Trans>
|
||||
</Toggle.LabelText>
|
||||
</View>
|
||||
</TargetToggle>
|
||||
</Toggle.Item>
|
||||
|
||||
<Toggle.Item
|
||||
label={_(msg`Mute this word for 24 hours`)}
|
||||
name="24_hours"
|
||||
style={[a.flex_1]}
|
||||
PressableComponent={NormalizedRNGHPressable}>
|
||||
<TargetToggle>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.gap_sm,
|
||||
]}>
|
||||
<Toggle.Radio />
|
||||
<Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
|
||||
<Trans>24 hours</Trans>
|
||||
</Toggle.LabelText>
|
||||
</View>
|
||||
</TargetToggle>
|
||||
</Toggle.Item>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.flex_row,
|
||||
a.justify_start,
|
||||
a.align_center,
|
||||
a.gap_sm,
|
||||
]}>
|
||||
<Toggle.Item
|
||||
label={_(msg`Mute this word for 7 days`)}
|
||||
name="7_days"
|
||||
style={[a.flex_1]}
|
||||
PressableComponent={NormalizedRNGHPressable}>
|
||||
<TargetToggle>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.gap_sm,
|
||||
]}>
|
||||
<Toggle.Radio />
|
||||
<Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
|
||||
<Trans>7 days</Trans>
|
||||
</Toggle.LabelText>
|
||||
</View>
|
||||
</TargetToggle>
|
||||
</Toggle.Item>
|
||||
|
||||
<Toggle.Item
|
||||
label={_(msg`Mute this word for 30 days`)}
|
||||
name="30_days"
|
||||
style={[a.flex_1]}
|
||||
PressableComponent={NormalizedRNGHPressable}>
|
||||
<TargetToggle>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.gap_sm,
|
||||
]}>
|
||||
<Toggle.Radio />
|
||||
<Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
|
||||
<Trans>30 days</Trans>
|
||||
</Toggle.LabelText>
|
||||
</View>
|
||||
</TargetToggle>
|
||||
</Toggle.Item>
|
||||
</View>
|
||||
</View>
|
||||
</Toggle.Group>
|
||||
|
||||
<Toggle.Group
|
||||
label={_(
|
||||
msg`Select what content this mute word should apply to.`,
|
||||
)}
|
||||
type="radio"
|
||||
values={targets}
|
||||
onChange={setTargets}>
|
||||
<Text
|
||||
style={[
|
||||
a.pb_xs,
|
||||
a.text_sm,
|
||||
a.font_bold,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
<Trans>Mute in:</Trans>
|
||||
</Text>
|
||||
|
||||
<View style={[a.flex_row, a.align_center, a.gap_sm, a.flex_wrap]}>
|
||||
<Toggle.Item
|
||||
label={_(msg`Mute this word in post text and tags`)}
|
||||
name="content"
|
||||
style={[a.flex_1]}
|
||||
PressableComponent={NormalizedRNGHPressable}>
|
||||
<TargetToggle>
|
||||
<View
|
||||
style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
|
||||
<Toggle.Radio />
|
||||
<Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
|
||||
<Trans>Text & tags</Trans>
|
||||
</Toggle.LabelText>
|
||||
</View>
|
||||
<PageText size="sm" />
|
||||
</TargetToggle>
|
||||
</Toggle.Item>
|
||||
|
||||
<Toggle.Item
|
||||
label={_(msg`Mute this word in tags only`)}
|
||||
name="tag"
|
||||
style={[a.flex_1]}
|
||||
PressableComponent={NormalizedRNGHPressable}>
|
||||
<TargetToggle>
|
||||
<View
|
||||
style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
|
||||
<Toggle.Radio />
|
||||
<Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
|
||||
<Trans>Tags only</Trans>
|
||||
</Toggle.LabelText>
|
||||
</View>
|
||||
<Hashtag size="sm" />
|
||||
</TargetToggle>
|
||||
</Toggle.Item>
|
||||
</View>
|
||||
</Toggle.Group>
|
||||
|
||||
<View>
|
||||
<Text
|
||||
style={[
|
||||
a.pb_xs,
|
||||
a.text_sm,
|
||||
a.font_bold,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
<Trans>Options:</Trans>
|
||||
</Text>
|
||||
<Toggle.Item
|
||||
label={_(msg`Do not apply this mute word to users you follow`)}
|
||||
name="exclude_following"
|
||||
style={[a.flex_row, a.justify_between]}
|
||||
value={excludeFollowing}
|
||||
onChange={setExcludeFollowing}
|
||||
PressableComponent={NormalizedRNGHPressable}>
|
||||
<TargetToggle>
|
||||
<View
|
||||
style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
|
||||
<Toggle.Checkbox />
|
||||
<Toggle.LabelText style={[a.flex_1, a.leading_tight]}>
|
||||
<Trans>Exclude users you follow</Trans>
|
||||
</Toggle.LabelText>
|
||||
</View>
|
||||
</TargetToggle>
|
||||
</Toggle.Item>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={[a.pt_xs]}>
|
||||
<Button
|
||||
disabled={isPending || !field}
|
||||
label={_(msg`Add mute word for configured settings`)}
|
||||
size="large"
|
||||
color="primary"
|
||||
variant="solid"
|
||||
style={[]}
|
||||
onPress={submit}>
|
||||
<ButtonText>
|
||||
<Trans>Add</Trans>
|
||||
</ButtonText>
|
||||
<ButtonIcon icon={isPending ? Loader : Plus} position="right" />
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
{error && (
|
||||
<View
|
||||
style={[
|
||||
a.mb_lg,
|
||||
a.flex_row,
|
||||
a.rounded_sm,
|
||||
a.p_md,
|
||||
a.mb_xs,
|
||||
t.atoms.bg_contrast_25,
|
||||
{
|
||||
backgroundColor: t.palette.negative_400,
|
||||
},
|
||||
]}>
|
||||
<Text
|
||||
style={[
|
||||
a.italic,
|
||||
{color: t.palette.white},
|
||||
native({marginTop: 2}),
|
||||
]}>
|
||||
{error}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<Divider />
|
||||
|
||||
<View style={[a.pt_2xl]}>
|
||||
<Text
|
||||
style={[
|
||||
a.text_md,
|
||||
a.font_bold,
|
||||
a.pb_md,
|
||||
t.atoms.text_contrast_high,
|
||||
]}>
|
||||
<Trans>Your muted words</Trans>
|
||||
</Text>
|
||||
|
||||
{isPreferencesLoading ? (
|
||||
<Loader />
|
||||
) : preferencesError || !preferences ? (
|
||||
<View
|
||||
style={[
|
||||
a.py_md,
|
||||
a.px_lg,
|
||||
a.rounded_md,
|
||||
t.atoms.bg_contrast_25,
|
||||
]}>
|
||||
<Text style={[a.italic, t.atoms.text_contrast_high]}>
|
||||
<Trans>
|
||||
We're sorry, but we weren't able to load your muted words at
|
||||
this time. Please try again.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
) : preferences.moderationPrefs.mutedWords.length ? (
|
||||
[...preferences.moderationPrefs.mutedWords]
|
||||
.reverse()
|
||||
.map((word, i) => (
|
||||
<MutedWordRow
|
||||
key={word.value + i}
|
||||
word={word}
|
||||
style={[i % 2 === 0 && t.atoms.bg_contrast_25]}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<View
|
||||
style={[
|
||||
a.py_md,
|
||||
a.px_lg,
|
||||
a.rounded_md,
|
||||
t.atoms.bg_contrast_25,
|
||||
]}>
|
||||
<Text style={[a.italic, t.atoms.text_contrast_high]}>
|
||||
<Trans>You haven't muted any words or tags yet</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{isNative && <View style={{height: 20}} />}
|
||||
</View>
|
||||
|
||||
{isNative && <View style={{height: 20}} />}
|
||||
</View>
|
||||
<Dialog.Close />
|
||||
</Dialog.ScrollableInner>
|
||||
|
||||
<Dialog.Close />
|
||||
</Dialog.ScrollableInner>
|
||||
<Portal.Outlet />
|
||||
</Portal.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -437,6 +489,7 @@ function MutedWordRow({
|
||||
onConfirm={remove}
|
||||
confirmButtonCta={_(msg`Remove`)}
|
||||
confirmButtonColor="negative"
|
||||
Portal={Portal.Portal}
|
||||
/>
|
||||
|
||||
<View
|
||||
|
||||
@@ -37,6 +37,8 @@ import * as Toggle from '#/components/forms/Toggle'
|
||||
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {NormalizedRNGHPressable} from '#/components/NormalizedRNGHPressable'
|
||||
import {PortalComponent} from '#/components/Portal'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export type PostInteractionSettingsFormProps = {
|
||||
@@ -54,14 +56,15 @@ export type PostInteractionSettingsFormProps = {
|
||||
|
||||
export function PostInteractionSettingsControlledDialog({
|
||||
control,
|
||||
Portal,
|
||||
...rest
|
||||
}: PostInteractionSettingsFormProps & {
|
||||
control: Dialog.DialogControlProps
|
||||
Portal?: PortalComponent
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
return (
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Handle />
|
||||
<Dialog.Outer control={control} Portal={Portal}>
|
||||
<Dialog.ScrollableInner
|
||||
label={_(msg`Edit post interaction settings`)}
|
||||
style={[{maxWidth: 500}, a.w_full]}>
|
||||
@@ -96,7 +99,6 @@ export function PostInteractionSettingsDialog(
|
||||
) {
|
||||
return (
|
||||
<Dialog.Outer control={props.control}>
|
||||
<Dialog.Handle />
|
||||
<PostInteractionSettingsDialogControlledInner {...props} />
|
||||
</Dialog.Outer>
|
||||
)
|
||||
@@ -231,7 +233,6 @@ export function PostInteractionSettingsForm({
|
||||
}: PostInteractionSettingsFormProps) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const control = Dialog.useDialogContext()
|
||||
const {data: lists} = useMyListsQuery('curate')
|
||||
const [quotesEnabled, setQuotesEnabled] = React.useState(
|
||||
!(
|
||||
@@ -305,7 +306,8 @@ export function PostInteractionSettingsForm({
|
||||
}
|
||||
value={quotesEnabled}
|
||||
onChange={onChangeQuotesEnabled}
|
||||
style={[, a.justify_between, a.pt_xs]}>
|
||||
style={[, a.justify_between, a.pt_xs]}
|
||||
PressableComponent={NormalizedRNGHPressable}>
|
||||
<Text style={[t.atoms.text_contrast_medium]}>
|
||||
{quotesEnabled ? (
|
||||
<Trans>Quote posts enabled</Trans>
|
||||
@@ -437,7 +439,6 @@ export function PostInteractionSettingsForm({
|
||||
<Button
|
||||
label={_(msg`Save`)}
|
||||
onPress={onSave}
|
||||
onAccessibilityEscape={control.close}
|
||||
color="primary"
|
||||
size="large"
|
||||
variant="solid"
|
||||
|
||||
@@ -18,7 +18,6 @@ export function SigninDialog() {
|
||||
const {signinDialogControl: control} = useGlobalDialogsControlContext()
|
||||
return (
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Handle />
|
||||
<SigninDialogInner control={control} />
|
||||
</Dialog.Outer>
|
||||
)
|
||||
|
||||
@@ -42,8 +42,6 @@ export function SwitchAccountDialog({
|
||||
|
||||
return (
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.ScrollableInner label={_(msg`Switch Account`)}>
|
||||
<View style={[a.gap_lg]}>
|
||||
<Text style={[a.text_2xl, a.font_bold]}>
|
||||
|
||||
@@ -43,8 +43,6 @@ export function NeueTypography() {
|
||||
|
||||
return (
|
||||
<Dialog.Outer control={control} onClose={onClose}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.ScrollableInner label={_(msg`Introducing new font settings`)}>
|
||||
<View style={[a.gap_xl]}>
|
||||
<View style={[a.gap_md]}>
|
||||
|
||||
@@ -136,7 +136,7 @@ let ConvoMenu = ({
|
||||
<Menu.Outer>
|
||||
<Menu.Item
|
||||
label={_(msg`Leave conversation`)}
|
||||
onPress={leaveConvoControl.open}>
|
||||
onPress={() => leaveConvoControl.open()}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Leave conversation</Trans>
|
||||
</Menu.ItemText>
|
||||
@@ -195,7 +195,7 @@ let ConvoMenu = ({
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
label={_(msg`Report conversation`)}
|
||||
onPress={reportControl.open}>
|
||||
onPress={() => reportControl.open()}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Report conversation</Trans>
|
||||
</Menu.ItemText>
|
||||
@@ -206,7 +206,7 @@ let ConvoMenu = ({
|
||||
<Menu.Group>
|
||||
<Menu.Item
|
||||
label={_(msg`Leave conversation`)}
|
||||
onPress={leaveConvoControl.open}>
|
||||
onPress={() => leaveConvoControl.open()}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Leave conversation</Trans>
|
||||
</Menu.ItemText>
|
||||
|
||||
@@ -7,11 +7,11 @@ import {useLingui} from '@lingui/react'
|
||||
|
||||
import {richTextToString} from '#/lib/strings/rich-text-helpers'
|
||||
import {getTranslatorLink} from '#/locale/helpers'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useConvoActive} from '#/state/messages/convo'
|
||||
import {useLanguagePrefs} from '#/state/preferences'
|
||||
import {useOpenLink} from '#/state/preferences/in-app-browser'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {useConvoActive} from 'state/messages/convo'
|
||||
import {useSession} from 'state/session'
|
||||
import {useSession} from '#/state/session'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {ReportDialog} from '#/components/dms/ReportDialog'
|
||||
@@ -120,7 +120,7 @@ export let MessageMenu = ({
|
||||
<Menu.Item
|
||||
testID="messageDropdownDeleteBtn"
|
||||
label={_(msg`Delete message for me`)}
|
||||
onPress={deleteControl.open}>
|
||||
onPress={() => deleteControl.open()}>
|
||||
<Menu.ItemText>{_(msg`Delete for me`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Trash} position="right" />
|
||||
</Menu.Item>
|
||||
@@ -128,7 +128,7 @@ export let MessageMenu = ({
|
||||
<Menu.Item
|
||||
testID="messageDropdownReportBtn"
|
||||
label={_(msg`Report message`)}
|
||||
onPress={reportControl.open}>
|
||||
onPress={() => reportControl.open()}>
|
||||
<Menu.ItemText>{_(msg`Report`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Warning} position="right" />
|
||||
</Menu.Item>
|
||||
|
||||
@@ -10,13 +10,11 @@ import {useLingui} from '@lingui/react'
|
||||
import {useMutation} from '@tanstack/react-query'
|
||||
|
||||
import {ReportOption} from '#/lib/moderation/useReportOptions'
|
||||
import {isAndroid} from '#/platform/detection'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {KeyboardControllerPadding} from '#/components/KeyboardControllerPadding'
|
||||
import {Button, ButtonIcon, ButtonText} from '../Button'
|
||||
import {Divider} from '../Divider'
|
||||
import {ChevronLeft_Stroke2_Corner0_Rounded as Chevron} from '../icons/Chevron'
|
||||
@@ -41,14 +39,10 @@ let ReportDialog = ({
|
||||
}): React.ReactNode => {
|
||||
const {_} = useLingui()
|
||||
return (
|
||||
<Dialog.Outer
|
||||
control={control}
|
||||
nativeOptions={isAndroid ? {sheet: {snapPoints: ['100%']}} : {}}>
|
||||
<Dialog.Handle />
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.ScrollableInner label={_(msg`Report this message`)}>
|
||||
<DialogInner params={params} />
|
||||
<Dialog.Close />
|
||||
<KeyboardControllerPadding />
|
||||
</Dialog.ScrollableInner>
|
||||
</Dialog.Outer>
|
||||
)
|
||||
|
||||
@@ -2,9 +2,9 @@ import React, {useCallback} from 'react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {logger} from '#/logger'
|
||||
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
|
||||
import {logEvent} from 'lib/statsig/statsig'
|
||||
import {FAB} from '#/view/com/util/fab/FAB'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {useTheme} from '#/alf'
|
||||
@@ -55,10 +55,7 @@ export function NewChat({
|
||||
accessibilityHint=""
|
||||
/>
|
||||
|
||||
<Dialog.Outer
|
||||
control={control}
|
||||
testID="newChatDialog"
|
||||
nativeOptions={{sheet: {snapPoints: ['100%']}}}>
|
||||
<Dialog.Outer control={control} testID="newChatDialog">
|
||||
<SearchablePeopleList
|
||||
title={_(msg`Start a new chat`)}
|
||||
onSelectChat={onCreateChat}
|
||||
|
||||
@@ -5,10 +5,8 @@ import React, {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import type {TextInput as TextInputType} from 'react-native'
|
||||
import {View} from 'react-native'
|
||||
import {TextInput, View} from 'react-native'
|
||||
import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
|
||||
import {BottomSheetFlatListMethods} from '@discord/bottom-sheet'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
@@ -16,15 +14,15 @@ import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
|
||||
import {useListConvosQuery} from '#/state/queries/messages/list-converations'
|
||||
import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useActorAutocompleteQuery} from 'state/queries/actor-autocomplete'
|
||||
import {ListMethods} from '#/view/com/util/List'
|
||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a, native, useTheme, web} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {TextInput} from '#/components/dms/dialogs/TextInput'
|
||||
import {canBeMessaged} from '#/components/dms/util'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
|
||||
@@ -66,9 +64,9 @@ export function SearchablePeopleList({
|
||||
const {_} = useLingui()
|
||||
const moderationOpts = useModerationOpts()
|
||||
const control = Dialog.useDialogContext()
|
||||
const listRef = useRef<BottomSheetFlatListMethods>(null)
|
||||
const listRef = useRef<ListMethods>(null)
|
||||
const {currentAccount} = useSession()
|
||||
const inputRef = useRef<TextInputType>(null)
|
||||
const inputRef = useRef<TextInput>(null)
|
||||
|
||||
const [searchText, setSearchText] = useState('')
|
||||
|
||||
@@ -242,13 +240,12 @@ export function SearchablePeopleList({
|
||||
<View
|
||||
style={[
|
||||
a.relative,
|
||||
a.pt_md,
|
||||
web(a.pt_md),
|
||||
a.pb_xs,
|
||||
a.px_lg,
|
||||
a.border_b,
|
||||
t.atoms.border_contrast_low,
|
||||
t.atoms.bg,
|
||||
native([a.pt_lg]),
|
||||
]}>
|
||||
<View
|
||||
style={[
|
||||
@@ -474,7 +471,7 @@ function SearchInput({
|
||||
value: string
|
||||
onChangeText: (text: string) => void
|
||||
onEscape: () => void
|
||||
inputRef: React.RefObject<TextInputType>
|
||||
inputRef: React.RefObject<TextInput>
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
|
||||
@@ -2,9 +2,9 @@ import React, {useCallback} from 'react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {logger} from '#/logger'
|
||||
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
|
||||
import {logEvent} from 'lib/statsig/statsig'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {SearchablePeopleList} from './SearchablePeopleList'
|
||||
@@ -17,10 +17,7 @@ export function SendViaChatDialog({
|
||||
onSelectChat: (chatId: string) => void
|
||||
}) {
|
||||
return (
|
||||
<Dialog.Outer
|
||||
control={control}
|
||||
testID="sendViaChatChatDialog"
|
||||
nativeOptions={{sheet: {snapPoints: ['100%']}}}>
|
||||
<Dialog.Outer control={control} testID="sendViaChatChatDialog">
|
||||
<SendViaChatDialogInner control={control} onSelectChat={onSelectChat} />
|
||||
</Dialog.Outer>
|
||||
)
|
||||
|
||||
@@ -57,7 +57,6 @@ export function DateField({
|
||||
accessibilityHint={accessibilityHint}
|
||||
/>
|
||||
<Dialog.Outer control={control} testID={testID}>
|
||||
<Dialog.Handle />
|
||||
<Dialog.Inner label={label}>
|
||||
<View style={a.gap_lg}>
|
||||
<View style={[a.relative, a.w_full, a.align_center]}>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react'
|
||||
import {Pressable, View, ViewStyle} from 'react-native'
|
||||
import {Pressable, PressableProps, View, ViewStyle} from 'react-native'
|
||||
import Animated, {LinearTransition} from 'react-native-reanimated'
|
||||
|
||||
import {HITSLOP_10} from '#/lib/constants'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {HITSLOP_10} from 'lib/constants'
|
||||
import {
|
||||
atoms as a,
|
||||
flatten,
|
||||
@@ -68,6 +68,7 @@ export type ItemProps = ViewStyleProp & {
|
||||
onChange?: (selected: boolean) => void
|
||||
isInvalid?: boolean
|
||||
children: ((props: ItemState) => React.ReactNode) | React.ReactNode
|
||||
PressableComponent?: React.ComponentType<PressableProps>
|
||||
}
|
||||
|
||||
export function useItemContext() {
|
||||
@@ -159,6 +160,7 @@ export function Item({
|
||||
style,
|
||||
type = 'checkbox',
|
||||
label,
|
||||
PressableComponent = Pressable,
|
||||
...rest
|
||||
}: ItemProps) {
|
||||
const {
|
||||
@@ -206,7 +208,7 @@ export function Item({
|
||||
|
||||
return (
|
||||
<ItemContext.Provider value={state}>
|
||||
<Pressable
|
||||
<PressableComponent
|
||||
accessibilityHint={undefined} // optional
|
||||
hitSlop={HITSLOP_10}
|
||||
{...rest}
|
||||
@@ -231,7 +233,7 @@ export function Item({
|
||||
onBlur={onBlur}
|
||||
style={[a.flex_row, a.align_center, a.gap_sm, flatten(style)]}>
|
||||
{typeof children === 'function' ? children(state) : children}
|
||||
</Pressable>
|
||||
</PressableComponent>
|
||||
</ItemContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ export function VerifyEmailIntentDialog() {
|
||||
|
||||
return (
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Handle />
|
||||
<Inner control={control} />
|
||||
</Dialog.Outer>
|
||||
)
|
||||
|
||||
@@ -13,6 +13,7 @@ import {logger} from '#/logger'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {BottomSheetInlineLinkText} from '#/components/BottomSheetLink'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {InlineLinkText} from '#/components/Link'
|
||||
@@ -31,8 +32,6 @@ export interface LabelsOnMeDialogProps {
|
||||
export function LabelsOnMeDialog(props: LabelsOnMeDialogProps) {
|
||||
return (
|
||||
<Dialog.Outer control={props.control}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<LabelsOnMeDialogInner {...props} />
|
||||
</Dialog.Outer>
|
||||
)
|
||||
@@ -158,23 +157,25 @@ function Label({
|
||||
<Divider />
|
||||
|
||||
<View style={[a.px_md, a.py_sm, t.atoms.bg_contrast_25]}>
|
||||
<Text style={[t.atoms.text_contrast_medium]}>
|
||||
{isSelfLabel ? (
|
||||
{isSelfLabel ? (
|
||||
<Text style={[t.atoms.text_contrast_medium]}>
|
||||
<Trans>This label was applied by you.</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
Source:{' '}
|
||||
<InlineLinkText
|
||||
label={sourceName}
|
||||
to={makeProfileLink(
|
||||
labeler ? labeler.creator : {did: label.src, handle: ''},
|
||||
)}
|
||||
onPress={() => control.close()}>
|
||||
{sourceName}
|
||||
</InlineLinkText>
|
||||
</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</Text>
|
||||
) : (
|
||||
<View style={{flexDirection: 'row'}}>
|
||||
<Text style={[t.atoms.text_contrast_medium]}>
|
||||
<Trans>Source: </Trans>{' '}
|
||||
</Text>
|
||||
<BottomSheetInlineLinkText
|
||||
label={sourceName}
|
||||
to={makeProfileLink(
|
||||
labeler ? labeler.creator : {did: label.src, handle: ''},
|
||||
)}
|
||||
onPress={() => control.close()}>
|
||||
{sourceName}
|
||||
</BottomSheetInlineLinkText>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
|
||||
@@ -10,6 +10,7 @@ import {listUriToHref} from '#/lib/strings/url-helpers'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {useSession} from '#/state/session'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {BottomSheetInlineLinkText} from '#/components/BottomSheetLink'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {Divider} from '#/components/Divider'
|
||||
import {InlineLinkText} from '#/components/Link'
|
||||
@@ -26,7 +27,6 @@ export interface ModerationDetailsDialogProps {
|
||||
export function ModerationDetailsDialog(props: ModerationDetailsDialogProps) {
|
||||
return (
|
||||
<Dialog.Outer control={props.control}>
|
||||
<Dialog.Handle />
|
||||
<ModerationDetailsDialogInner {...props} />
|
||||
</Dialog.Outer>
|
||||
)
|
||||
@@ -141,23 +141,24 @@ function ModerationDetailsDialogInner({
|
||||
{modcause?.type === 'label' && (
|
||||
<View style={[a.pt_lg]}>
|
||||
<Divider />
|
||||
<Text style={[t.atoms.text, a.text_md, a.leading_snug, a.mt_lg]}>
|
||||
{modcause.source.type === 'user' ? (
|
||||
{modcause.source.type === 'user' ? (
|
||||
<Text style={[t.atoms.text, a.text_md, a.leading_snug, a.mt_lg]}>
|
||||
<Trans>This label was applied by the author.</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
This label was applied by{' '}
|
||||
<InlineLinkText
|
||||
label={desc.source || _(msg`an unknown labeler`)}
|
||||
to={makeProfileLink({did: modcause.label.src, handle: ''})}
|
||||
onPress={() => control.close()}
|
||||
style={a.text_md}>
|
||||
{desc.source || _(msg`an unknown labeler`)}
|
||||
</InlineLinkText>
|
||||
.
|
||||
</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</Text>
|
||||
) : (
|
||||
<>
|
||||
<Text style={[t.atoms.text, a.text_md, a.leading_snug, a.mt_lg]}>
|
||||
<Trans>This label was applied by </Trans>
|
||||
</Text>
|
||||
<BottomSheetInlineLinkText
|
||||
label={desc.source || _(msg`an unknown labeler`)}
|
||||
to={makeProfileLink({did: modcause.label.src, handle: ''})}
|
||||
onPress={() => control.close()}
|
||||
style={a.text_md}>
|
||||
{desc.source || _(msg`an unknown labeler`)}
|
||||
</BottomSheetInlineLinkText>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
|
||||
@@ -54,7 +54,6 @@ function AppealDialog() {
|
||||
</Button>
|
||||
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Handle />
|
||||
<DialogInner />
|
||||
</Dialog.Outer>
|
||||
</>
|
||||
|
||||
@@ -286,7 +286,6 @@ export function StepProfile() {
|
||||
</View>
|
||||
|
||||
<Dialog.Outer control={creatorControl}>
|
||||
<Dialog.Handle />
|
||||
<Dialog.Inner
|
||||
label="Avatar creator"
|
||||
style={[
|
||||
|
||||
@@ -15,35 +15,35 @@ import {useNavigation} from '@react-navigation/native'
|
||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {batchedUpdates} from '#/lib/batchedUpdates'
|
||||
import {HITSLOP_20} from '#/lib/constants'
|
||||
import {isBlockedOrBlocking, isMuted} from '#/lib/moderation/blocked-and-muted'
|
||||
import {makeProfileLink, makeStarterPackLink} from '#/lib/routes/links'
|
||||
import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {getStarterPackOgCard} from '#/lib/strings/starter-pack'
|
||||
import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {updateProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {getAllListMembers} from '#/state/queries/list-members'
|
||||
import {useResolvedStarterPackShortLink} from '#/state/queries/resolve-short-link'
|
||||
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
|
||||
import {useShortenLink} from '#/state/queries/shorten-link'
|
||||
import {useDeleteStarterPackMutation} from '#/state/queries/starter-packs'
|
||||
import {useStarterPackQuery} from '#/state/queries/starter-packs'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
||||
import {
|
||||
ProgressGuideAction,
|
||||
useProgressGuideControls,
|
||||
} from '#/state/shell/progress-guide'
|
||||
import {batchedUpdates} from 'lib/batchedUpdates'
|
||||
import {HITSLOP_20} from 'lib/constants'
|
||||
import {isBlockedOrBlocking, isMuted} from 'lib/moderation/blocked-and-muted'
|
||||
import {makeProfileLink, makeStarterPackLink} from 'lib/routes/links'
|
||||
import {CommonNavigatorParams, NavigationProp} from 'lib/routes/types'
|
||||
import {logEvent} from 'lib/statsig/statsig'
|
||||
import {getStarterPackOgCard} from 'lib/strings/starter-pack'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {updateProfileShadow} from 'state/cache/profile-shadow'
|
||||
import {useModerationOpts} from 'state/preferences/moderation-opts'
|
||||
import {getAllListMembers} from 'state/queries/list-members'
|
||||
import {useResolvedStarterPackShortLink} from 'state/queries/resolve-short-link'
|
||||
import {useResolveDidQuery} from 'state/queries/resolve-uri'
|
||||
import {useShortenLink} from 'state/queries/shorten-link'
|
||||
import {useStarterPackQuery} from 'state/queries/starter-packs'
|
||||
import {useAgent, useSession} from 'state/session'
|
||||
import {useLoggedOutViewControls} from 'state/shell/logged-out'
|
||||
import {useSetActiveStarterPack} from 'state/shell/starter-pack'
|
||||
import {useSetActiveStarterPack} from '#/state/shell/starter-pack'
|
||||
import {PagerWithHeader} from '#/view/com/pager/PagerWithHeader'
|
||||
import {ProfileSubpageHeader} from '#/view/com/profile/ProfileSubpageHeader'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
|
||||
import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader'
|
||||
import {CenteredView} from 'view/com/util/Views'
|
||||
import {CenteredView} from '#/view/com/util/Views'
|
||||
import {bulkWriteFollows} from '#/screens/Onboarding/util'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
@@ -591,7 +591,7 @@ function OverflowMenu({
|
||||
|
||||
<Menu.Item
|
||||
label={_(msg`Report starter pack`)}
|
||||
onPress={reportDialogControl.open}>
|
||||
onPress={() => reportDialogControl.open()}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Report starter pack</Trans>
|
||||
</Menu.ItemText>
|
||||
|
||||
+21
-33
@@ -1,8 +1,9 @@
|
||||
import React from 'react'
|
||||
import {SharedValue, useSharedValue} from 'react-native-reanimated'
|
||||
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {DialogControlRefProps} from '#/components/Dialog'
|
||||
import {Provider as GlobalDialogsProvider} from '#/components/dialogs/Context'
|
||||
import {BottomSheet} from '../../../modules/bottom-sheet'
|
||||
|
||||
interface IDialogContext {
|
||||
/**
|
||||
@@ -16,14 +17,6 @@ interface IDialogContext {
|
||||
* `useId`.
|
||||
*/
|
||||
openDialogs: React.MutableRefObject<Set<string>>
|
||||
/**
|
||||
* The counterpart to `accessibilityViewIsModal` for Android. This property
|
||||
* applies to the parent of all non-modal views, and prevents TalkBack from
|
||||
* navigating within content beneath an open dialog.
|
||||
*
|
||||
* @see https://reactnative.dev/docs/accessibility#importantforaccessibility-android
|
||||
*/
|
||||
importantForAccessibility: SharedValue<'auto' | 'no-hide-descendants'>
|
||||
}
|
||||
|
||||
const DialogContext = React.createContext<IDialogContext>({} as IDialogContext)
|
||||
@@ -49,40 +42,35 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
Map<string, React.MutableRefObject<DialogControlRefProps>>
|
||||
>(new Map())
|
||||
const openDialogs = React.useRef<Set<string>>(new Set())
|
||||
const importantForAccessibility = useSharedValue<
|
||||
'auto' | 'no-hide-descendants'
|
||||
>('auto')
|
||||
|
||||
const closeAllDialogs = React.useCallback(() => {
|
||||
openDialogs.current.forEach(id => {
|
||||
const dialog = activeDialogs.current.get(id)
|
||||
if (dialog) dialog.current.close()
|
||||
})
|
||||
return openDialogs.current.size > 0
|
||||
if (isWeb) {
|
||||
openDialogs.current.forEach(id => {
|
||||
const dialog = activeDialogs.current.get(id)
|
||||
if (dialog) dialog.current.close()
|
||||
})
|
||||
|
||||
return openDialogs.current.size > 0
|
||||
} else {
|
||||
BottomSheet.dismissAll()
|
||||
return false
|
||||
}
|
||||
}, [])
|
||||
|
||||
const setDialogIsOpen = React.useCallback(
|
||||
(id: string, isOpen: boolean) => {
|
||||
if (isOpen) {
|
||||
openDialogs.current.add(id)
|
||||
importantForAccessibility.value = 'no-hide-descendants'
|
||||
} else {
|
||||
openDialogs.current.delete(id)
|
||||
if (openDialogs.current.size < 1) {
|
||||
importantForAccessibility.value = 'auto'
|
||||
}
|
||||
}
|
||||
},
|
||||
[importantForAccessibility],
|
||||
)
|
||||
const setDialogIsOpen = React.useCallback((id: string, isOpen: boolean) => {
|
||||
if (isOpen) {
|
||||
openDialogs.current.add(id)
|
||||
} else {
|
||||
openDialogs.current.delete(id)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const context = React.useMemo<IDialogContext>(
|
||||
() => ({
|
||||
activeDialogs,
|
||||
openDialogs,
|
||||
importantForAccessibility,
|
||||
}),
|
||||
[importantForAccessibility, activeDialogs, openDialogs],
|
||||
[activeDialogs, openDialogs],
|
||||
)
|
||||
const controls = React.useMemo(
|
||||
() => ({closeAllDialogs, setDialogIsOpen}),
|
||||
|
||||
@@ -12,6 +12,7 @@ import * as TextField from '#/components/forms/TextField'
|
||||
import * as ToggleButton from '#/components/forms/ToggleButton'
|
||||
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
|
||||
import {InlineLinkText} from '#/components/Link'
|
||||
import {NormalizedRNGHPressable} from '#/components/NormalizedRNGHPressable'
|
||||
import {P, Text} from '#/components/Typography'
|
||||
|
||||
export function ServerInputDialog({
|
||||
@@ -66,12 +67,7 @@ export function ServerInputDialog({
|
||||
])
|
||||
|
||||
return (
|
||||
<Dialog.Outer
|
||||
control={control}
|
||||
nativeOptions={{sheet: {snapPoints: ['100%']}}}
|
||||
onClose={onClose}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.Outer control={control} onClose={onClose}>
|
||||
<Dialog.ScrollableInner
|
||||
accessibilityDescribedBy="dialog-description"
|
||||
accessibilityLabelledBy="dialog-title">
|
||||
@@ -87,7 +83,10 @@ export function ServerInputDialog({
|
||||
label="Preferences"
|
||||
values={fixedOption}
|
||||
onChange={setFixedOption}>
|
||||
<ToggleButton.Button name={BSKY_SERVICE} label={_(msg`Bluesky`)}>
|
||||
<ToggleButton.Button
|
||||
name={BSKY_SERVICE}
|
||||
label={_(msg`Bluesky`)}
|
||||
PressableComponent={NormalizedRNGHPressable}>
|
||||
<ToggleButton.ButtonText>
|
||||
{_(msg`Bluesky`)}
|
||||
</ToggleButton.ButtonText>
|
||||
@@ -95,7 +94,8 @@ export function ServerInputDialog({
|
||||
<ToggleButton.Button
|
||||
testID="customSelectBtn"
|
||||
name="custom"
|
||||
label={_(msg`Custom`)}>
|
||||
label={_(msg`Custom`)}
|
||||
PressableComponent={NormalizedRNGHPressable}>
|
||||
<ToggleButton.ButtonText>
|
||||
{_(msg`Custom`)}
|
||||
</ToggleButton.ButtonText>
|
||||
|
||||
+267
-250
@@ -121,10 +121,13 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
||||
import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||
import {createPortalGroup} from '#/components/Portal'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {Text as NewText} from '#/components/Typography'
|
||||
import {composerReducer, createComposerState} from './state'
|
||||
|
||||
const Portal = createPortalGroup()
|
||||
|
||||
const MAX_IMAGES = 4
|
||||
|
||||
type CancelRef = {
|
||||
@@ -638,271 +641,285 @@ export const ComposePost = ({
|
||||
const keyboardVerticalOffset = useKeyboardVerticalOffset()
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
testID="composePostView"
|
||||
behavior={isIOS ? 'padding' : 'height'}
|
||||
keyboardVerticalOffset={keyboardVerticalOffset}
|
||||
style={a.flex_1}>
|
||||
<View style={[a.flex_1, viewStyles]} aria-modal accessibilityViewIsModal>
|
||||
<Animated.View
|
||||
style={topBarAnimatedStyle}
|
||||
layout={native(LinearTransition)}>
|
||||
<View style={styles.topbarInner}>
|
||||
<Button
|
||||
label={_(msg`Cancel`)}
|
||||
variant="ghost"
|
||||
color="primary"
|
||||
shape="default"
|
||||
size="small"
|
||||
style={[
|
||||
a.rounded_full,
|
||||
a.py_sm,
|
||||
{paddingLeft: 7, paddingRight: 7},
|
||||
]}
|
||||
onPress={onPressCancel}
|
||||
accessibilityHint={_(
|
||||
msg`Closes post composer and discards post draft`,
|
||||
)}>
|
||||
<ButtonText style={[a.text_md]}>
|
||||
<Trans>Cancel</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
<View style={a.flex_1} />
|
||||
{isProcessing ? (
|
||||
<>
|
||||
<Text style={pal.textLight}>{processingState}</Text>
|
||||
<View style={styles.postBtn}>
|
||||
<ActivityIndicator />
|
||||
</View>
|
||||
</>
|
||||
) : (
|
||||
<View style={[styles.postBtnWrapper]}>
|
||||
<LabelsBtn
|
||||
labels={labels}
|
||||
onChange={setLabels}
|
||||
hasMedia={hasMedia}
|
||||
/>
|
||||
{canPost ? (
|
||||
<Button
|
||||
testID="composerPublishBtn"
|
||||
label={
|
||||
replyTo ? _(msg`Publish reply`) : _(msg`Publish post`)
|
||||
}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
shape="default"
|
||||
size="small"
|
||||
style={[a.rounded_full, a.py_sm]}
|
||||
onPress={() => onPressPublish()}
|
||||
disabled={
|
||||
videoUploadState.status !== 'idle' && publishOnUpload
|
||||
}>
|
||||
<ButtonText style={[a.text_md]}>
|
||||
{replyTo ? (
|
||||
<Trans context="action">Reply</Trans>
|
||||
) : (
|
||||
<Trans context="action">Post</Trans>
|
||||
)}
|
||||
</ButtonText>
|
||||
</Button>
|
||||
) : (
|
||||
<View style={[styles.postBtn, pal.btn]}>
|
||||
<Text style={[pal.textLight, s.f16, s.bold]}>
|
||||
<Trans context="action">Post</Trans>
|
||||
</Text>
|
||||
<Portal.Provider>
|
||||
<KeyboardAvoidingView
|
||||
testID="composePostView"
|
||||
behavior={isIOS ? 'padding' : 'height'}
|
||||
keyboardVerticalOffset={keyboardVerticalOffset}
|
||||
style={a.flex_1}>
|
||||
<View
|
||||
style={[a.flex_1, viewStyles]}
|
||||
aria-modal
|
||||
accessibilityViewIsModal>
|
||||
<Animated.View
|
||||
style={topBarAnimatedStyle}
|
||||
layout={native(LinearTransition)}>
|
||||
<View style={styles.topbarInner}>
|
||||
<Button
|
||||
label={_(msg`Cancel`)}
|
||||
variant="ghost"
|
||||
color="primary"
|
||||
shape="default"
|
||||
size="small"
|
||||
style={[
|
||||
a.rounded_full,
|
||||
a.py_sm,
|
||||
{paddingLeft: 7, paddingRight: 7},
|
||||
]}
|
||||
onPress={onPressCancel}
|
||||
accessibilityHint={_(
|
||||
msg`Closes post composer and discards post draft`,
|
||||
)}>
|
||||
<ButtonText style={[a.text_md]}>
|
||||
<Trans>Cancel</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
<View style={a.flex_1} />
|
||||
{isProcessing ? (
|
||||
<>
|
||||
<Text style={pal.textLight}>{processingState}</Text>
|
||||
<View style={styles.postBtn}>
|
||||
<ActivityIndicator />
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<View style={[styles.postBtnWrapper]}>
|
||||
<LabelsBtn
|
||||
labels={labels}
|
||||
onChange={setLabels}
|
||||
hasMedia={hasMedia}
|
||||
/>
|
||||
{canPost ? (
|
||||
<Button
|
||||
testID="composerPublishBtn"
|
||||
label={
|
||||
replyTo ? _(msg`Publish reply`) : _(msg`Publish post`)
|
||||
}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
shape="default"
|
||||
size="small"
|
||||
style={[a.rounded_full, a.py_sm]}
|
||||
onPress={() => onPressPublish()}
|
||||
disabled={
|
||||
videoUploadState.status !== 'idle' && publishOnUpload
|
||||
}>
|
||||
<ButtonText style={[a.text_md]}>
|
||||
{replyTo ? (
|
||||
<Trans context="action">Reply</Trans>
|
||||
) : (
|
||||
<Trans context="action">Post</Trans>
|
||||
)}
|
||||
</ButtonText>
|
||||
</Button>
|
||||
) : (
|
||||
<View style={[styles.postBtn, pal.btn]}>
|
||||
<Text style={[pal.textLight, s.f16, s.bold]}>
|
||||
<Trans context="action">Post</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{isAltTextRequiredAndMissing && (
|
||||
<View style={[styles.reminderLine, pal.viewLight]}>
|
||||
<View style={styles.errorIcon}>
|
||||
<FontAwesomeIcon
|
||||
icon="exclamation"
|
||||
style={{color: colors.red4}}
|
||||
size={10}
|
||||
/>
|
||||
</View>
|
||||
<Text style={[pal.text, a.flex_1]}>
|
||||
<Trans>One or more images is missing alt text.</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<ErrorBanner
|
||||
error={error}
|
||||
videoUploadState={videoUploadState}
|
||||
clearError={() => setError('')}
|
||||
clearVideo={clearVideo}
|
||||
/>
|
||||
</Animated.View>
|
||||
<Animated.ScrollView
|
||||
layout={native(LinearTransition)}
|
||||
onScroll={scrollHandler}
|
||||
style={styles.scrollView}
|
||||
keyboardShouldPersistTaps="always"
|
||||
onContentSizeChange={onScrollViewContentSizeChange}
|
||||
onLayout={onScrollViewLayout}>
|
||||
{replyTo ? <ComposerReplyTo replyTo={replyTo} /> : undefined}
|
||||
|
||||
{isAltTextRequiredAndMissing && (
|
||||
<View style={[styles.reminderLine, pal.viewLight]}>
|
||||
<View style={styles.errorIcon}>
|
||||
<FontAwesomeIcon
|
||||
icon="exclamation"
|
||||
style={{color: colors.red4}}
|
||||
size={10}
|
||||
<View
|
||||
style={[
|
||||
styles.textInputLayout,
|
||||
isNative && styles.textInputLayoutMobile,
|
||||
]}>
|
||||
<UserAvatar
|
||||
avatar={currentProfile?.avatar}
|
||||
size={50}
|
||||
type={currentProfile?.associated?.labeler ? 'labeler' : 'user'}
|
||||
/>
|
||||
<TextInput
|
||||
ref={textInput}
|
||||
richtext={richtext}
|
||||
placeholder={selectTextInputPlaceholder}
|
||||
autoFocus
|
||||
setRichText={setRichText}
|
||||
onPhotoPasted={onPhotoPasted}
|
||||
onPressPublish={() => onPressPublish()}
|
||||
onNewLink={onNewLink}
|
||||
onError={setError}
|
||||
accessible={true}
|
||||
accessibilityLabel={_(msg`Write post`)}
|
||||
accessibilityHint={_(
|
||||
msg`Compose posts up to ${MAX_GRAPHEME_LENGTH} characters in length`,
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Gallery
|
||||
images={images}
|
||||
dispatch={dispatch}
|
||||
Portal={Portal.Portal}
|
||||
/>
|
||||
{images.length === 0 && extLink && (
|
||||
<View style={a.relative}>
|
||||
<ExternalEmbed
|
||||
link={extLink}
|
||||
gif={extGif}
|
||||
onRemove={() => {
|
||||
setExtLink(undefined)
|
||||
setExtGif(undefined)
|
||||
}}
|
||||
/>
|
||||
<GifAltText
|
||||
link={extLink}
|
||||
gif={extGif}
|
||||
onSubmit={handleChangeGifAltText}
|
||||
Portal={Portal.Portal}
|
||||
/>
|
||||
</View>
|
||||
<Text style={[pal.text, a.flex_1]}>
|
||||
<Trans>One or more images is missing alt text.</Trans>
|
||||
</Text>
|
||||
)}
|
||||
<LayoutAnimationConfig skipExiting>
|
||||
{hasVideo && (
|
||||
<Animated.View
|
||||
style={[a.w_full, a.mt_lg]}
|
||||
entering={native(ZoomIn)}
|
||||
exiting={native(ZoomOut)}>
|
||||
{videoUploadState.asset &&
|
||||
(videoUploadState.status === 'compressing' ? (
|
||||
<VideoTranscodeProgress
|
||||
asset={videoUploadState.asset}
|
||||
progress={videoUploadState.progress}
|
||||
clear={clearVideo}
|
||||
/>
|
||||
) : videoUploadState.video ? (
|
||||
<VideoPreview
|
||||
asset={videoUploadState.asset}
|
||||
video={videoUploadState.video}
|
||||
setDimensions={updateVideoDimensions}
|
||||
clear={clearVideo}
|
||||
/>
|
||||
) : null)}
|
||||
<SubtitleDialogBtn
|
||||
defaultAltText={videoAltText}
|
||||
saveAltText={setVideoAltText}
|
||||
captions={captions}
|
||||
setCaptions={setCaptions}
|
||||
Portal={Portal.Portal}
|
||||
/>
|
||||
</Animated.View>
|
||||
)}
|
||||
</LayoutAnimationConfig>
|
||||
<View style={!hasVideo ? [a.mt_md] : []}>
|
||||
{quote ? (
|
||||
<View style={[s.mt5, s.mb2, isWeb && s.mb10]}>
|
||||
<View style={{pointerEvents: 'none'}}>
|
||||
<QuoteEmbed quote={quote} />
|
||||
</View>
|
||||
{quote.uri !== initQuote?.uri && (
|
||||
<QuoteX onRemove={() => setQuote(undefined)} />
|
||||
)}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
)}
|
||||
<ErrorBanner
|
||||
error={error}
|
||||
videoUploadState={videoUploadState}
|
||||
clearError={() => setError('')}
|
||||
clearVideo={clearVideo}
|
||||
/>
|
||||
</Animated.View>
|
||||
<Animated.ScrollView
|
||||
layout={native(LinearTransition)}
|
||||
onScroll={scrollHandler}
|
||||
style={styles.scrollView}
|
||||
keyboardShouldPersistTaps="always"
|
||||
onContentSizeChange={onScrollViewContentSizeChange}
|
||||
onLayout={onScrollViewLayout}>
|
||||
{replyTo ? <ComposerReplyTo replyTo={replyTo} /> : undefined}
|
||||
</Animated.ScrollView>
|
||||
<SuggestedLanguage text={richtext.text} />
|
||||
|
||||
{replyTo ? null : (
|
||||
<ThreadgateBtn
|
||||
postgate={postgate}
|
||||
onChangePostgate={setPostgate}
|
||||
threadgateAllowUISettings={threadgateAllowUISettings}
|
||||
onChangeThreadgateAllowUISettings={
|
||||
onChangeThreadgateAllowUISettings
|
||||
}
|
||||
style={bottomBarAnimatedStyle}
|
||||
Portal={Portal.Portal}
|
||||
/>
|
||||
)}
|
||||
<View
|
||||
style={[
|
||||
styles.textInputLayout,
|
||||
isNative && styles.textInputLayoutMobile,
|
||||
t.atoms.bg,
|
||||
t.atoms.border_contrast_medium,
|
||||
styles.bottomBar,
|
||||
]}>
|
||||
<UserAvatar
|
||||
avatar={currentProfile?.avatar}
|
||||
size={50}
|
||||
type={currentProfile?.associated?.labeler ? 'labeler' : 'user'}
|
||||
/>
|
||||
<TextInput
|
||||
ref={textInput}
|
||||
richtext={richtext}
|
||||
placeholder={selectTextInputPlaceholder}
|
||||
autoFocus
|
||||
setRichText={setRichText}
|
||||
onPhotoPasted={onPhotoPasted}
|
||||
onPressPublish={() => onPressPublish()}
|
||||
onNewLink={onNewLink}
|
||||
onError={setError}
|
||||
accessible={true}
|
||||
accessibilityLabel={_(msg`Write post`)}
|
||||
accessibilityHint={_(
|
||||
msg`Compose posts up to ${MAX_GRAPHEME_LENGTH} characters in length`,
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Gallery images={images} dispatch={dispatch} />
|
||||
{images.length === 0 && extLink && (
|
||||
<View style={a.relative}>
|
||||
<ExternalEmbed
|
||||
link={extLink}
|
||||
gif={extGif}
|
||||
onRemove={() => {
|
||||
setExtLink(undefined)
|
||||
setExtGif(undefined)
|
||||
}}
|
||||
/>
|
||||
<GifAltText
|
||||
link={extLink}
|
||||
gif={extGif}
|
||||
onSubmit={handleChangeGifAltText}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
<LayoutAnimationConfig skipExiting>
|
||||
{hasVideo && (
|
||||
<Animated.View
|
||||
style={[a.w_full, a.mt_lg]}
|
||||
entering={native(ZoomIn)}
|
||||
exiting={native(ZoomOut)}>
|
||||
{videoUploadState.asset &&
|
||||
(videoUploadState.status === 'compressing' ? (
|
||||
<VideoTranscodeProgress
|
||||
asset={videoUploadState.asset}
|
||||
progress={videoUploadState.progress}
|
||||
clear={clearVideo}
|
||||
/>
|
||||
) : videoUploadState.video ? (
|
||||
<VideoPreview
|
||||
asset={videoUploadState.asset}
|
||||
video={videoUploadState.video}
|
||||
setDimensions={updateVideoDimensions}
|
||||
clear={clearVideo}
|
||||
/>
|
||||
) : null)}
|
||||
<SubtitleDialogBtn
|
||||
defaultAltText={videoAltText}
|
||||
saveAltText={setVideoAltText}
|
||||
captions={captions}
|
||||
setCaptions={setCaptions}
|
||||
{videoUploadState.status !== 'idle' &&
|
||||
videoUploadState.status !== 'done' ? (
|
||||
<VideoUploadToolbar state={videoUploadState} />
|
||||
) : (
|
||||
<ToolbarWrapper style={[a.flex_row, a.align_center, a.gap_xs]}>
|
||||
<SelectPhotoBtn
|
||||
size={images.length}
|
||||
disabled={!canSelectImages}
|
||||
onAdd={onImageAdd}
|
||||
/>
|
||||
</Animated.View>
|
||||
<SelectVideoBtn
|
||||
onSelectVideo={selectVideo}
|
||||
disabled={!canSelectImages || images?.length > 0}
|
||||
setError={setError}
|
||||
/>
|
||||
<OpenCameraBtn disabled={!canSelectImages} onAdd={onImageAdd} />
|
||||
<SelectGifBtn
|
||||
onClose={focusTextInput}
|
||||
onSelectGif={onSelectGif}
|
||||
disabled={hasMedia}
|
||||
/>
|
||||
{!isMobile ? (
|
||||
<Button
|
||||
onPress={onEmojiButtonPress}
|
||||
style={a.p_sm}
|
||||
label={_(msg`Open emoji picker`)}
|
||||
accessibilityHint={_(msg`Open emoji picker`)}
|
||||
variant="ghost"
|
||||
shape="round"
|
||||
color="primary">
|
||||
<EmojiSmile size="lg" />
|
||||
</Button>
|
||||
) : null}
|
||||
</ToolbarWrapper>
|
||||
)}
|
||||
</LayoutAnimationConfig>
|
||||
<View style={!hasVideo ? [a.mt_md] : []}>
|
||||
{quote ? (
|
||||
<View style={[s.mt5, s.mb2, isWeb && s.mb10]}>
|
||||
<View style={{pointerEvents: 'none'}}>
|
||||
<QuoteEmbed quote={quote} />
|
||||
</View>
|
||||
{quote.uri !== initQuote?.uri && (
|
||||
<QuoteX onRemove={() => setQuote(undefined)} />
|
||||
)}
|
||||
</View>
|
||||
) : null}
|
||||
<View style={a.flex_1} />
|
||||
<SelectLangBtn />
|
||||
<CharProgress count={graphemeLength} />
|
||||
</View>
|
||||
</Animated.ScrollView>
|
||||
<SuggestedLanguage text={richtext.text} />
|
||||
|
||||
{replyTo ? null : (
|
||||
<ThreadgateBtn
|
||||
postgate={postgate}
|
||||
onChangePostgate={setPostgate}
|
||||
threadgateAllowUISettings={threadgateAllowUISettings}
|
||||
onChangeThreadgateAllowUISettings={
|
||||
onChangeThreadgateAllowUISettings
|
||||
}
|
||||
style={bottomBarAnimatedStyle}
|
||||
/>
|
||||
)}
|
||||
<View
|
||||
style={[
|
||||
t.atoms.bg,
|
||||
t.atoms.border_contrast_medium,
|
||||
styles.bottomBar,
|
||||
]}>
|
||||
{videoUploadState.status !== 'idle' &&
|
||||
videoUploadState.status !== 'done' ? (
|
||||
<VideoUploadToolbar state={videoUploadState} />
|
||||
) : (
|
||||
<ToolbarWrapper style={[a.flex_row, a.align_center, a.gap_xs]}>
|
||||
<SelectPhotoBtn
|
||||
size={images.length}
|
||||
disabled={!canSelectImages}
|
||||
onAdd={onImageAdd}
|
||||
/>
|
||||
<SelectVideoBtn
|
||||
onSelectVideo={selectVideo}
|
||||
disabled={!canSelectImages || images?.length > 0}
|
||||
setError={setError}
|
||||
/>
|
||||
<OpenCameraBtn disabled={!canSelectImages} onAdd={onImageAdd} />
|
||||
<SelectGifBtn
|
||||
onClose={focusTextInput}
|
||||
onSelectGif={onSelectGif}
|
||||
disabled={hasMedia}
|
||||
/>
|
||||
{!isMobile ? (
|
||||
<Button
|
||||
onPress={onEmojiButtonPress}
|
||||
style={a.p_sm}
|
||||
label={_(msg`Open emoji picker`)}
|
||||
accessibilityHint={_(msg`Open emoji picker`)}
|
||||
variant="ghost"
|
||||
shape="round"
|
||||
color="primary">
|
||||
<EmojiSmile size="lg" />
|
||||
</Button>
|
||||
) : null}
|
||||
</ToolbarWrapper>
|
||||
)}
|
||||
<View style={a.flex_1} />
|
||||
<SelectLangBtn />
|
||||
<CharProgress count={graphemeLength} />
|
||||
</View>
|
||||
</View>
|
||||
<Prompt.Basic
|
||||
control={discardPromptControl}
|
||||
title={_(msg`Discard draft?`)}
|
||||
description={_(msg`Are you sure you'd like to discard this draft?`)}
|
||||
onConfirm={onClose}
|
||||
confirmButtonCta={_(msg`Discard`)}
|
||||
confirmButtonColor="negative"
|
||||
/>
|
||||
</KeyboardAvoidingView>
|
||||
<Prompt.Basic
|
||||
control={discardPromptControl}
|
||||
title={_(msg`Discard draft?`)}
|
||||
description={_(msg`Are you sure you'd like to discard this draft?`)}
|
||||
onConfirm={onClose}
|
||||
confirmButtonCta={_(msg`Discard`)}
|
||||
confirmButtonColor="negative"
|
||||
Portal={Portal.Portal}
|
||||
/>
|
||||
</KeyboardAvoidingView>
|
||||
<Portal.Outlet />
|
||||
</Portal.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
parseEmbedPlayerFromUrl,
|
||||
} from '#/lib/strings/embed-player'
|
||||
import {enforceLen} from '#/lib/strings/helpers'
|
||||
import {isAndroid} from '#/platform/detection'
|
||||
import {Gif} from '#/state/queries/tenor'
|
||||
import {atoms as a, native, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
@@ -20,6 +19,7 @@ import * as Dialog from '#/components/Dialog'
|
||||
import * as TextField from '#/components/forms/TextField'
|
||||
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||
import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||
import {PortalComponent} from '#/components/Portal'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {GifEmbed} from '../util/post-embeds/GifEmbed'
|
||||
import {AltTextReminder} from './photos/Gallery'
|
||||
@@ -28,10 +28,12 @@ export function GifAltText({
|
||||
link: linkProp,
|
||||
gif,
|
||||
onSubmit,
|
||||
Portal,
|
||||
}: {
|
||||
link: ExternalEmbedDraft
|
||||
gif?: Gif
|
||||
onSubmit: (alt: string) => void
|
||||
Portal: PortalComponent
|
||||
}) {
|
||||
const control = Dialog.useDialogControl()
|
||||
const {_} = useLingui()
|
||||
@@ -96,10 +98,7 @@ export function GifAltText({
|
||||
|
||||
<AltTextReminder />
|
||||
|
||||
<Dialog.Outer
|
||||
control={control}
|
||||
nativeOptions={isAndroid ? {sheet: {snapPoints: ['100%']}} : {}}>
|
||||
<Dialog.Handle />
|
||||
<Dialog.Outer control={control} Portal={Portal}>
|
||||
<AltTextInner
|
||||
onSubmit={onPressSubmit}
|
||||
link={link}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {ComposerImage, cropImage} from '#/state/gallery'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import {useTheme} from '#/alf'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {PortalComponent} from '#/components/Portal'
|
||||
import {ComposerAction} from '../state'
|
||||
import {EditImageDialog} from './EditImageDialog'
|
||||
import {ImageAltTextDialog} from './ImageAltTextDialog'
|
||||
@@ -30,6 +31,7 @@ const IMAGE_GAP = 8
|
||||
interface GalleryProps {
|
||||
images: ComposerImage[]
|
||||
dispatch: (action: ComposerAction) => void
|
||||
Portal: PortalComponent
|
||||
}
|
||||
|
||||
export let Gallery = (props: GalleryProps): React.ReactNode => {
|
||||
@@ -57,7 +59,12 @@ interface GalleryInnerProps extends GalleryProps {
|
||||
containerInfo: Dimensions
|
||||
}
|
||||
|
||||
const GalleryInner = ({images, containerInfo, dispatch}: GalleryInnerProps) => {
|
||||
const GalleryInner = ({
|
||||
images,
|
||||
containerInfo,
|
||||
dispatch,
|
||||
Portal,
|
||||
}: GalleryInnerProps) => {
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
|
||||
const {altTextControlStyle, imageControlsStyle, imageStyle} =
|
||||
@@ -111,6 +118,7 @@ const GalleryInner = ({images, containerInfo, dispatch}: GalleryInnerProps) => {
|
||||
onRemove={() => {
|
||||
dispatch({type: 'embed_remove_image', image})
|
||||
}}
|
||||
Portal={Portal}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
@@ -127,6 +135,7 @@ type GalleryItemProps = {
|
||||
imageStyle?: ViewStyle
|
||||
onChange: (next: ComposerImage) => void
|
||||
onRemove: () => void
|
||||
Portal: PortalComponent
|
||||
}
|
||||
|
||||
const GalleryItem = ({
|
||||
@@ -136,6 +145,7 @@ const GalleryItem = ({
|
||||
imageStyle,
|
||||
onChange,
|
||||
onRemove,
|
||||
Portal,
|
||||
}: GalleryItemProps): React.ReactNode => {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
@@ -230,6 +240,7 @@ const GalleryItem = ({
|
||||
control={altTextControl}
|
||||
image={image}
|
||||
onChange={onChange}
|
||||
Portal={Portal}
|
||||
/>
|
||||
|
||||
<EditImageDialog
|
||||
|
||||
@@ -11,19 +11,19 @@ import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import * as TextField from '#/components/forms/TextField'
|
||||
import {PortalComponent} from '#/components/Portal'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
type Props = {
|
||||
control: Dialog.DialogOuterProps['control']
|
||||
image: ComposerImage
|
||||
onChange: (next: ComposerImage) => void
|
||||
Portal: PortalComponent
|
||||
}
|
||||
|
||||
export const ImageAltTextDialog = (props: Props): React.ReactNode => {
|
||||
return (
|
||||
<Dialog.Outer control={props.control}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.Outer control={props.control} Portal={props.Portal}>
|
||||
<ImageAltTextInner {...props} />
|
||||
</Dialog.Outer>
|
||||
)
|
||||
|
||||
@@ -13,6 +13,7 @@ import * as Dialog from '#/components/Dialog'
|
||||
import {PostInteractionSettingsControlledDialog} from '#/components/dialogs/PostInteractionSettingsDialog'
|
||||
import {Earth_Stroke2_Corner0_Rounded as Earth} from '#/components/icons/Globe'
|
||||
import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
|
||||
import {PortalComponent} from '#/components/Portal'
|
||||
|
||||
export function ThreadgateBtn({
|
||||
postgate,
|
||||
@@ -20,6 +21,7 @@ export function ThreadgateBtn({
|
||||
threadgateAllowUISettings,
|
||||
onChangeThreadgateAllowUISettings,
|
||||
style,
|
||||
Portal,
|
||||
}: {
|
||||
postgate: AppBskyFeedPostgate.Record
|
||||
onChangePostgate: (v: AppBskyFeedPostgate.Record) => void
|
||||
@@ -28,6 +30,8 @@ export function ThreadgateBtn({
|
||||
onChangeThreadgateAllowUISettings: (v: ThreadgateAllowUISetting[]) => void
|
||||
|
||||
style?: StyleProp<AnimatedStyle<ViewStyle>>
|
||||
|
||||
Portal: PortalComponent
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
@@ -77,6 +81,7 @@ export function ThreadgateBtn({
|
||||
onChangePostgate={onChangePostgate}
|
||||
threadgateAllowUISettings={threadgateAllowUISettings}
|
||||
onChangeThreadgateAllowUISettings={onChangeThreadgateAllowUISettings}
|
||||
Portal={Portal}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ import {useLingui} from '@lingui/react'
|
||||
import {MAX_ALT_TEXT} from '#/lib/constants'
|
||||
import {useEnforceMaxGraphemeCount} from '#/lib/strings/helpers'
|
||||
import {LANGUAGES} from '#/locale/languages'
|
||||
import {isAndroid, isWeb} from '#/platform/detection'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useLanguagePrefs} from '#/state/preferences'
|
||||
import {atoms as a, useTheme, web} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
@@ -17,6 +17,7 @@ import {CC_Stroke2_Corner0_Rounded as CCIcon} from '#/components/icons/CC'
|
||||
import {PageText_Stroke2_Corner0_Rounded as PageTextIcon} from '#/components/icons/PageText'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
|
||||
import {PortalComponent} from '#/components/Portal'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {SubtitleFilePicker} from './SubtitleFilePicker'
|
||||
|
||||
@@ -29,6 +30,7 @@ interface Props {
|
||||
setCaptions: React.Dispatch<
|
||||
React.SetStateAction<{lang: string; file: File}[]>
|
||||
>
|
||||
Portal: PortalComponent
|
||||
}
|
||||
|
||||
export function SubtitleDialogBtn(props: Props) {
|
||||
@@ -56,10 +58,7 @@ export function SubtitleDialogBtn(props: Props) {
|
||||
{isWeb ? <Trans>Captions & alt text</Trans> : <Trans>Alt text</Trans>}
|
||||
</ButtonText>
|
||||
</Button>
|
||||
<Dialog.Outer
|
||||
control={control}
|
||||
nativeOptions={isAndroid ? {sheet: {snapPoints: ['60%']}} : {}}>
|
||||
<Dialog.Handle />
|
||||
<Dialog.Outer control={control} Portal={props.Portal}>
|
||||
<SubtitleDialogInner {...props} />
|
||||
</Dialog.Outer>
|
||||
</View>
|
||||
|
||||
@@ -437,7 +437,7 @@ let PostDropdownBtn = ({
|
||||
<Menu.Item
|
||||
testID="postDropdownSendViaDMBtn"
|
||||
label={_(msg`Send via direct message`)}
|
||||
onPress={sendViaChatControl.open}>
|
||||
onPress={() => sendViaChatControl.open()}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Send via direct message</Trans>
|
||||
</Menu.ItemText>
|
||||
@@ -465,7 +465,7 @@ let PostDropdownBtn = ({
|
||||
<Menu.Item
|
||||
testID="postDropdownEmbedBtn"
|
||||
label={_(msg`Embed post`)}
|
||||
onPress={embedPostControl.open}>
|
||||
onPress={() => embedPostControl.open()}>
|
||||
<Menu.ItemText>{_(msg`Embed post`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={CodeBrackets} position="right" />
|
||||
</Menu.Item>
|
||||
@@ -540,7 +540,7 @@ let PostDropdownBtn = ({
|
||||
? _(msg`Hide reply for me`)
|
||||
: _(msg`Hide post for me`)
|
||||
}
|
||||
onPress={hidePromptControl.open}>
|
||||
onPress={() => hidePromptControl.open()}>
|
||||
<Menu.ItemText>
|
||||
{isReply
|
||||
? _(msg`Hide reply for me`)
|
||||
@@ -628,7 +628,9 @@ let PostDropdownBtn = ({
|
||||
<Menu.Item
|
||||
testID="postDropdownEditPostInteractions"
|
||||
label={_(msg`Edit interaction settings`)}
|
||||
onPress={postInteractionSettingsDialogControl.open}
|
||||
onPress={() =>
|
||||
postInteractionSettingsDialogControl.open()
|
||||
}
|
||||
{...(isAuthor
|
||||
? Platform.select({
|
||||
web: {
|
||||
@@ -647,7 +649,7 @@ let PostDropdownBtn = ({
|
||||
<Menu.Item
|
||||
testID="postDropdownDeleteBtn"
|
||||
label={_(msg`Delete post`)}
|
||||
onPress={deletePromptControl.open}>
|
||||
onPress={() => deletePromptControl.open()}>
|
||||
<Menu.ItemText>{_(msg`Delete post`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Trash} position="right" />
|
||||
</Menu.Item>
|
||||
|
||||
@@ -86,8 +86,9 @@ let RepostButton = ({
|
||||
</Text>
|
||||
) : undefined}
|
||||
</Button>
|
||||
<Dialog.Outer control={dialogControl}>
|
||||
<Dialog.Handle />
|
||||
<Dialog.Outer
|
||||
control={dialogControl}
|
||||
nativeOptions={{preventExpansion: true}}>
|
||||
<Dialog.Inner label={_(msg`Repost or quote post`)}>
|
||||
<View style={a.gap_xl}>
|
||||
<View style={a.gap_xs}>
|
||||
@@ -155,7 +156,6 @@ let RepostButton = ({
|
||||
</View>
|
||||
<Button
|
||||
label={_(msg`Cancel quote post`)}
|
||||
onAccessibilityEscape={close}
|
||||
onPress={close}
|
||||
size="large"
|
||||
variant="solid"
|
||||
|
||||
@@ -78,8 +78,6 @@ export function DisableEmail2FADialog({
|
||||
|
||||
return (
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.ScrollableInner
|
||||
accessibilityDescribedBy="dialog-description"
|
||||
accessibilityLabelledBy="dialog-title">
|
||||
|
||||
@@ -52,8 +52,6 @@ export function ExportCarDialog({
|
||||
|
||||
return (
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.ScrollableInner
|
||||
accessibilityDescribedBy="dialog-description"
|
||||
accessibilityLabelledBy="dialog-title">
|
||||
|
||||
@@ -2,8 +2,8 @@ import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
import {useDialogStateControlContext} from '#/state/dialogs'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
@@ -179,19 +179,13 @@ export function Dialogs() {
|
||||
</Prompt.Outer>
|
||||
|
||||
<Dialog.Outer control={basic}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.Inner label="test">
|
||||
<H3 nativeID="dialog-title">Dialog</H3>
|
||||
<P nativeID="dialog-description">A basic dialog</P>
|
||||
</Dialog.Inner>
|
||||
</Dialog.Outer>
|
||||
|
||||
<Dialog.Outer
|
||||
control={scrollable}
|
||||
nativeOptions={{sheet: {snapPoints: ['100%']}}}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.Outer control={scrollable}>
|
||||
<Dialog.ScrollableInner
|
||||
accessibilityDescribedBy="dialog-description"
|
||||
accessibilityLabelledBy="dialog-title">
|
||||
@@ -230,8 +224,6 @@ export function Dialogs() {
|
||||
</Dialog.Outer>
|
||||
|
||||
<Dialog.Outer control={testDialog}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.ScrollableInner
|
||||
accessibilityDescribedBy="dialog-description"
|
||||
accessibilityLabelledBy="dialog-title">
|
||||
@@ -356,8 +348,6 @@ export function Dialogs() {
|
||||
|
||||
{shouldRenderUnmountTest && (
|
||||
<Dialog.Outer control={unmountTestDialog}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.Inner label="test">
|
||||
<H3 nativeID="dialog-title">Unmount Test Dialog</H3>
|
||||
<P nativeID="dialog-description">Will unmount in about 5 seconds</P>
|
||||
|
||||
+11
-15
@@ -13,6 +13,13 @@ import * as NavigationBar from 'expo-navigation-bar'
|
||||
import {StatusBar} from 'expo-status-bar'
|
||||
import {useNavigation, useNavigationState} from '@react-navigation/native'
|
||||
|
||||
import {useDedupe} from '#/lib/hooks/useDedupe'
|
||||
import {useNotificationsHandler} from '#/lib/hooks/useNotificationHandler'
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {useNotificationsRegistration} from '#/lib/notifications/notifications'
|
||||
import {isStateAtTabRoot} from '#/lib/routes/helpers'
|
||||
import {useTheme} from '#/lib/ThemeContext'
|
||||
import {isAndroid} from '#/platform/detection'
|
||||
import {useSession} from '#/state/session'
|
||||
import {
|
||||
useIsDrawerOpen,
|
||||
@@ -20,17 +27,9 @@ import {
|
||||
useSetDrawerOpen,
|
||||
} from '#/state/shell'
|
||||
import {useCloseAnyActiveElement} from '#/state/util'
|
||||
import {useDedupe} from 'lib/hooks/useDedupe'
|
||||
import {useNotificationsHandler} from 'lib/hooks/useNotificationHandler'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useNotificationsRegistration} from 'lib/notifications/notifications'
|
||||
import {isStateAtTabRoot} from 'lib/routes/helpers'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {isAndroid} from 'platform/detection'
|
||||
import {useDialogStateContext} from 'state/dialogs'
|
||||
import {Lightbox} from 'view/com/lightbox/Lightbox'
|
||||
import {ModalsContainer} from 'view/com/modals/Modal'
|
||||
import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
|
||||
import {Lightbox} from '#/view/com/lightbox/Lightbox'
|
||||
import {ModalsContainer} from '#/view/com/modals/Modal'
|
||||
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
|
||||
import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
|
||||
import {SigninDialog} from '#/components/dialogs/Signin'
|
||||
import {Outlet as PortalOutlet} from '#/components/Portal'
|
||||
@@ -61,7 +60,6 @@ function ShellInner() {
|
||||
const canGoBack = useNavigationState(state => !isStateAtTabRoot(state))
|
||||
const {hasSession} = useSession()
|
||||
const closeAnyActiveElement = useCloseAnyActiveElement()
|
||||
const {importantForAccessibility} = useDialogStateContext()
|
||||
|
||||
useNotificationsRegistration()
|
||||
useNotificationsHandler()
|
||||
@@ -101,9 +99,7 @@ function ShellInner() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Animated.View
|
||||
style={containerPadding}
|
||||
importantForAccessibility={importantForAccessibility}>
|
||||
<Animated.View style={containerPadding}>
|
||||
<ErrorBoundary>
|
||||
<Drawer
|
||||
renderDrawerContent={renderDrawerContent}
|
||||
|
||||
@@ -18063,15 +18063,14 @@ react-native-drawer-layout@^4.0.0-alpha.3:
|
||||
dependencies:
|
||||
use-latest-callback "^0.1.9"
|
||||
|
||||
react-native-gesture-handler@~2.16.2:
|
||||
version "2.16.2"
|
||||
resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.16.2.tgz#032bd2a07334292d7f6cff1dc9d1ec928f72e26d"
|
||||
integrity sha512-vGFlrDKlmyI+BT+FemqVxmvO7nqxU33cgXVsn6IKAFishvlG3oV2Ds67D5nPkHMea8T+s1IcuMm0bF8ntZtAyg==
|
||||
react-native-gesture-handler@2.20.0:
|
||||
version "2.20.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.20.0.tgz#2d9ec4e9bd22619ebe36269dda3ecb1173928276"
|
||||
integrity sha512-rFKqgHRfxQ7uSAivk8vxCiW4SB3G0U7jnv7kZD4Y90K5kp6YrU8Q3tWhxe3Rx55BIvSd3mBe9ZWbWVJ0FsSHPA==
|
||||
dependencies:
|
||||
"@egjs/hammerjs" "^2.0.17"
|
||||
hoist-non-react-statics "^3.3.0"
|
||||
invariant "^2.2.4"
|
||||
lodash "^4.17.21"
|
||||
prop-types "^15.7.2"
|
||||
|
||||
react-native-get-random-values@^1.6.0:
|
||||
@@ -18100,10 +18099,10 @@ react-native-ios-context-menu@^1.15.3:
|
||||
dependencies:
|
||||
"@dominicstop/ts-event-emitter" "^1.1.0"
|
||||
|
||||
react-native-keyboard-controller@^1.12.1:
|
||||
version "1.12.1"
|
||||
resolved "https://registry.yarnpkg.com/react-native-keyboard-controller/-/react-native-keyboard-controller-1.12.1.tgz#6de22ed4d060528a0dd25621eeaa7f71772ce35f"
|
||||
integrity sha512-2OpQcesiYsMilrTzgcTafSGexd9UryRQRuHudIcOn0YaqvvzNpnhVZMVuJMH93fJv/iaZYp3138rgUKOdHhtSw==
|
||||
react-native-keyboard-controller@^1.14.0:
|
||||
version "1.14.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native-keyboard-controller/-/react-native-keyboard-controller-1.14.0.tgz#f6faaa12b3736a10f4eec4236ed5b0343508b9a1"
|
||||
integrity sha512-JW9k2fehFXOpvLWh1YcgyubLodg/HPi6bR11sCZB/BOawf1tnbGnqk967B8XkxDOKHH6mg+z82quCvv8ALh1rg==
|
||||
|
||||
react-native-mmkv@^2.12.2:
|
||||
version "2.12.2"
|
||||
|
||||
Reference in New Issue
Block a user