[Threadgate] Tweak threadgate buttons (#9173)
* tweak in-post threadgate button * tweak composer threadgate button * reduce date length slightly * pressed styles * make date length depend on breakpoint * add chevron to label btn * add tiny chevron, special-case button icon width * [Threadgate] Add hint (#9350) * get tooltip working on web * add compatibility layer for working in iOS sheets * add timeout to profile tooltip now that it appears instantly * rm debug code * Update ThreadgateBtn.tsx Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * remeasure when keyboard changes --------- Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * fix types * [Threadgate] Refresh dialog (#9342) * wip new ui * update threadgate dialog with new designs * restore nobody option * relayout android sheet when ratio changes * fix ratio changing case in bottom sheet * timebox reached, use setTimeout * update panel styles * missing imports * extract out Panel * tweak layout animation * fix icon color * use same color mechamism for icon as text * restore the header * refreshed toggle styles (#9343) * [Threadgate] Persist settings (#9341) * add persist toggle to threadgate dialog * move state back down * sort out spacing * wire up query * @surfdude29 tweaks * use tiny chevron in WhoCanReply * wait for prefetch before opening * move Panel into the Toggle namespace * default -> pref * use medium date length * rm hover state from web selects, fix border radius * fix key issue in Selects --------- Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M10.928 18.882a1.95 1.95 0 0 0 2.452-.25l9-9a1.953 1.953 0 0 0-2.76-2.76L12 14.493l-7.62-7.62a1.952 1.952 0 0 0-2.76 2.76l9 9 .308.25Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 237 B |
+6
-1
@@ -243,13 +243,18 @@ class BottomSheetView(
|
|||||||
val bottomSheet = dialog.findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet)
|
val bottomSheet = dialog.findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet)
|
||||||
bottomSheet?.let {
|
bottomSheet?.let {
|
||||||
val behavior = BottomSheetBehavior.from(it)
|
val behavior = BottomSheetBehavior.from(it)
|
||||||
|
val currentState = behavior.state
|
||||||
|
|
||||||
behavior.halfExpandedRatio = getHalfExpandedRatio(contentHeight)
|
val oldRatio = behavior.halfExpandedRatio
|
||||||
|
var newRatio = getHalfExpandedRatio(contentHeight)
|
||||||
|
behavior.halfExpandedRatio = newRatio
|
||||||
|
|
||||||
if (contentHeight > this.safeScreenHeight && behavior.state != BottomSheetBehavior.STATE_EXPANDED) {
|
if (contentHeight > this.safeScreenHeight && behavior.state != BottomSheetBehavior.STATE_EXPANDED) {
|
||||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||||
} else if (contentHeight < this.safeScreenHeight && behavior.state != BottomSheetBehavior.STATE_HALF_EXPANDED) {
|
} else if (contentHeight < this.safeScreenHeight && behavior.state != BottomSheetBehavior.STATE_HALF_EXPANDED) {
|
||||||
behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
|
behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
|
||||||
|
} else if (currentState == BottomSheetBehavior.STATE_HALF_EXPANDED && oldRatio != newRatio) {
|
||||||
|
behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import {BottomSheet} from './src/BottomSheet'
|
import {BottomSheet} from './src/BottomSheet'
|
||||||
import {
|
import {
|
||||||
BottomSheetSnapPoint,
|
BottomSheetSnapPoint,
|
||||||
BottomSheetState,
|
type BottomSheetState,
|
||||||
BottomSheetViewProps,
|
type BottomSheetViewProps,
|
||||||
} from './src/BottomSheet.types'
|
} from './src/BottomSheet.types'
|
||||||
import {BottomSheetNativeComponent} from './src/BottomSheetNativeComponent'
|
import {BottomSheetNativeComponent} from './src/BottomSheetNativeComponent'
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -112,9 +112,21 @@ export class BottomSheetNativeComponent extends React.Component<
|
|||||||
onStateChange={this.onStateChange}
|
onStateChange={this.onStateChange}
|
||||||
extraStyles={extraStyles}
|
extraStyles={extraStyles}
|
||||||
onLayout={e => {
|
onLayout={e => {
|
||||||
|
if (isIOS15) {
|
||||||
const {height} = e.nativeEvent.layout
|
const {height} = e.nativeEvent.layout
|
||||||
this.setState({viewHeight: height})
|
this.setState({viewHeight: height})
|
||||||
|
}
|
||||||
|
if (Platform.OS === 'android') {
|
||||||
|
// TEMP HACKFIX: I had to timebox this, but this is Bad.
|
||||||
|
// On Android, if you run updateLayout() immediately,
|
||||||
|
// it will take ages to actually run on the native side.
|
||||||
|
// However, adding literally any delay will fix this, including
|
||||||
|
// a console.log() - just sending the log to the CLI is enough.
|
||||||
|
// TODO: Get to the bottom of this and fix it properly! -sfn
|
||||||
|
setTimeout(() => this.updateLayout())
|
||||||
|
} else {
|
||||||
this.updateLayout()
|
this.updateLayout()
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Portal>
|
</Portal>
|
||||||
|
|||||||
@@ -798,6 +798,7 @@ export function ButtonIcon({
|
|||||||
* also so that we can calculate transforms.
|
* also so that we can calculate transforms.
|
||||||
*/
|
*/
|
||||||
const iconSize = {
|
const iconSize = {
|
||||||
|
'2xs': 8,
|
||||||
xs: 12,
|
xs: 12,
|
||||||
sm: 16,
|
sm: 16,
|
||||||
md: 18,
|
md: 18,
|
||||||
@@ -842,7 +843,7 @@ export function ButtonIcon({
|
|||||||
style={[
|
style={[
|
||||||
a.z_20,
|
a.z_20,
|
||||||
{
|
{
|
||||||
width: iconContainerSize,
|
width: size === '2xs' ? 10 : iconContainerSize,
|
||||||
height: iconContainerSize,
|
height: iconContainerSize,
|
||||||
marginLeft: iconNegativeMargin,
|
marginLeft: iconNegativeMargin,
|
||||||
marginRight: iconNegativeMargin,
|
marginRight: iconNegativeMargin,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {createContext, forwardRef, useContext, useMemo} from 'react'
|
import {createContext, forwardRef, Fragment, useContext, useMemo} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {Select as RadixSelect} from 'radix-ui'
|
import {Select as RadixSelect} from 'radix-ui'
|
||||||
|
|
||||||
@@ -96,8 +96,7 @@ export function Trigger({children, label}: TriggerProps) {
|
|||||||
style={flatten([
|
style={flatten([
|
||||||
a.flex,
|
a.flex,
|
||||||
a.relative,
|
a.relative,
|
||||||
t.atoms.bg_contrast_25,
|
t.atoms.bg_contrast_50,
|
||||||
a.rounded_sm,
|
|
||||||
a.w_full,
|
a.w_full,
|
||||||
a.align_center,
|
a.align_center,
|
||||||
a.gap_sm,
|
a.gap_sm,
|
||||||
@@ -106,15 +105,14 @@ export function Trigger({children, label}: TriggerProps) {
|
|||||||
a.px_md,
|
a.px_md,
|
||||||
a.pointer,
|
a.pointer,
|
||||||
{
|
{
|
||||||
|
borderRadius: 10,
|
||||||
maxWidth: 400,
|
maxWidth: 400,
|
||||||
outline: 0,
|
outline: 0,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
borderStyle: 'solid',
|
borderStyle: 'solid',
|
||||||
borderColor: focused
|
borderColor: focused
|
||||||
? t.palette.primary_500
|
? t.palette.primary_500
|
||||||
: hovered
|
: t.palette.contrast_50,
|
||||||
? t.palette.contrast_100
|
|
||||||
: t.palette.contrast_25,
|
|
||||||
},
|
},
|
||||||
])}>
|
])}>
|
||||||
{children}
|
{children}
|
||||||
@@ -140,7 +138,11 @@ export function Icon({style}: IconProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Content<T>({items, renderItem}: ContentProps<T>) {
|
export function Content<T>({
|
||||||
|
items,
|
||||||
|
renderItem,
|
||||||
|
valueExtractor = defaultItemValueExtractor,
|
||||||
|
}: ContentProps<T>) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const selectedValue = useContext(SelectedValueContext)
|
const selectedValue = useContext(SelectedValueContext)
|
||||||
|
|
||||||
@@ -198,7 +200,11 @@ export function Content<T>({items, renderItem}: ContentProps<T>) {
|
|||||||
<ChevronUpIcon style={[t.atoms.text]} size="xs" />
|
<ChevronUpIcon style={[t.atoms.text]} size="xs" />
|
||||||
</RadixSelect.ScrollUpButton>
|
</RadixSelect.ScrollUpButton>
|
||||||
<RadixSelect.Viewport style={flatten([a.p_xs])}>
|
<RadixSelect.Viewport style={flatten([a.p_xs])}>
|
||||||
{items.map((item, index) => renderItem(item, index, selectedValue))}
|
{items.map((item, index) => (
|
||||||
|
<Fragment key={valueExtractor(item)}>
|
||||||
|
{renderItem(item, index, selectedValue)}
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
</RadixSelect.Viewport>
|
</RadixSelect.Viewport>
|
||||||
<RadixSelect.ScrollDownButton style={flatten(down)}>
|
<RadixSelect.ScrollDownButton style={flatten(down)}>
|
||||||
<ChevronDownIcon style={[t.atoms.text]} size="xs" />
|
<ChevronDownIcon style={[t.atoms.text]} size="xs" />
|
||||||
@@ -209,6 +215,10 @@ export function Content<T>({items, renderItem}: ContentProps<T>) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function defaultItemValueExtractor(item: any) {
|
||||||
|
return item.value
|
||||||
|
}
|
||||||
|
|
||||||
const ItemContext = createContext<{
|
const ItemContext = createContext<{
|
||||||
hovered: boolean
|
hovered: boolean
|
||||||
focused: boolean
|
focused: boolean
|
||||||
|
|||||||
@@ -12,9 +12,11 @@ import {useWindowDimensions, View} from 'react-native'
|
|||||||
import Animated, {Easing, ZoomIn} from 'react-native-reanimated'
|
import Animated, {Easing, ZoomIn} from 'react-native-reanimated'
|
||||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
|
|
||||||
|
import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible'
|
||||||
|
import {GlobalGestureEventsProvider} from '#/state/global-gesture-events'
|
||||||
import {atoms as a, select, useTheme} from '#/alf'
|
import {atoms as a, select, useTheme} from '#/alf'
|
||||||
import {useOnGesture} from '#/components/hooks/useOnGesture'
|
import {useOnGesture} from '#/components/hooks/useOnGesture'
|
||||||
import {Portal} from '#/components/Portal'
|
import {createPortalGroup, Portal as RootPortal} from '#/components/Portal'
|
||||||
import {
|
import {
|
||||||
ARROW_HALF_SIZE,
|
ARROW_HALF_SIZE,
|
||||||
ARROW_SIZE,
|
ARROW_SIZE,
|
||||||
@@ -23,6 +25,33 @@ import {
|
|||||||
} from '#/components/Tooltip/const'
|
} from '#/components/Tooltip/const'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
|
const TooltipPortal = createPortalGroup()
|
||||||
|
const TooltipProviderContext =
|
||||||
|
createContext<React.RefObject<View | null> | null>(null)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider for Tooltip component. Only needed when you need to position the tooltip relative to a container,
|
||||||
|
* such as in the composer sheet.
|
||||||
|
*
|
||||||
|
* Only really necessary on iOS but can work on Android.
|
||||||
|
*/
|
||||||
|
export function SheetCompatProvider({children}: {children: React.ReactNode}) {
|
||||||
|
const ref = useRef<View | null>(null)
|
||||||
|
return (
|
||||||
|
<GlobalGestureEventsProvider style={[a.flex_1]}>
|
||||||
|
<TooltipPortal.Provider>
|
||||||
|
<View ref={ref} collapsable={false} style={[a.flex_1]}>
|
||||||
|
<TooltipProviderContext value={ref}>
|
||||||
|
{children}
|
||||||
|
</TooltipProviderContext>
|
||||||
|
</View>
|
||||||
|
<TooltipPortal.Outlet />
|
||||||
|
</TooltipPortal.Provider>
|
||||||
|
</GlobalGestureEventsProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SheetCompatProvider.displayName = 'TooltipSheetCompatProvider'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* These are native specific values, not shared with web
|
* These are native specific values, not shared with web
|
||||||
*/
|
*/
|
||||||
@@ -120,22 +149,46 @@ export function Outer({
|
|||||||
|
|
||||||
export function Target({children}: {children: React.ReactNode}) {
|
export function Target({children}: {children: React.ReactNode}) {
|
||||||
const {shouldMeasure, setTargetMeasurements} = useContext(TargetContext)
|
const {shouldMeasure, setTargetMeasurements} = useContext(TargetContext)
|
||||||
|
const [hasLayedOut, setHasLayedOut] = useState(false)
|
||||||
const targetRef = useRef<View>(null)
|
const targetRef = useRef<View>(null)
|
||||||
|
const containerRef = useContext(TooltipProviderContext)
|
||||||
|
const keyboardIsOpen = useIsKeyboardVisible()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!shouldMeasure) return
|
if (!shouldMeasure || !hasLayedOut) return
|
||||||
/*
|
/*
|
||||||
* Once opened, measure the dimensions and position of the target
|
* Once opened, measure the dimensions and position of the target
|
||||||
*/
|
*/
|
||||||
targetRef.current?.measure((_x, _y, width, height, pageX, pageY) => {
|
|
||||||
if (pageX !== undefined && pageY !== undefined && width && height) {
|
if (containerRef?.current) {
|
||||||
setTargetMeasurements({x: pageX, y: pageY, width, height})
|
targetRef.current?.measureLayout(
|
||||||
|
containerRef.current,
|
||||||
|
(x, y, width, height) => {
|
||||||
|
if (x !== undefined && y !== undefined && width && height) {
|
||||||
|
setTargetMeasurements({x, y, width, height})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
targetRef.current?.measure((_x, _y, width, height, x, y) => {
|
||||||
|
if (x !== undefined && y !== undefined && width && height) {
|
||||||
|
setTargetMeasurements({x, y, width, height})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [shouldMeasure, setTargetMeasurements])
|
}
|
||||||
|
}, [
|
||||||
|
shouldMeasure,
|
||||||
|
setTargetMeasurements,
|
||||||
|
hasLayedOut,
|
||||||
|
containerRef,
|
||||||
|
keyboardIsOpen,
|
||||||
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View collapsable={false} ref={targetRef}>
|
<View
|
||||||
|
collapsable={false}
|
||||||
|
ref={targetRef}
|
||||||
|
onLayout={() => setHasLayedOut(true)}>
|
||||||
{children}
|
{children}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
@@ -150,12 +203,15 @@ export function Content({
|
|||||||
}) {
|
}) {
|
||||||
const {position, visible, onVisibleChange} = useContext(TooltipContext)
|
const {position, visible, onVisibleChange} = useContext(TooltipContext)
|
||||||
const {targetMeasurements} = useContext(TargetContext)
|
const {targetMeasurements} = useContext(TargetContext)
|
||||||
|
const isWithinProvider = !!useContext(TooltipProviderContext)
|
||||||
const requestClose = useCallback(() => {
|
const requestClose = useCallback(() => {
|
||||||
onVisibleChange(false)
|
onVisibleChange(false)
|
||||||
}, [onVisibleChange])
|
}, [onVisibleChange])
|
||||||
|
|
||||||
if (!visible || !targetMeasurements) return null
|
if (!visible || !targetMeasurements) return null
|
||||||
|
|
||||||
|
const Portal = isWithinProvider ? TooltipPortal.Portal : RootPortal
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Portal>
|
<Portal>
|
||||||
<Bubble
|
<Bubble
|
||||||
|
|||||||
@@ -11,14 +11,19 @@ import {
|
|||||||
} from '#/components/Tooltip/const'
|
} from '#/components/Tooltip/const'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
|
// Portal Provider on native, but we actually don't need to do anything here
|
||||||
|
export function Provider({children}: {children: React.ReactNode}) {
|
||||||
|
return <>{children}</>
|
||||||
|
}
|
||||||
|
Provider.displayName = 'TooltipProvider'
|
||||||
|
|
||||||
type TooltipContextType = {
|
type TooltipContextType = {
|
||||||
position: 'top' | 'bottom'
|
position: 'top' | 'bottom'
|
||||||
onVisibleChange: (open: boolean) => void
|
onVisibleChange: (open: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const TooltipContext = createContext<TooltipContextType>({
|
const TooltipContext = createContext<Pick<TooltipContextType, 'position'>>({
|
||||||
position: 'bottom',
|
position: 'bottom',
|
||||||
onVisibleChange: () => {},
|
|
||||||
})
|
})
|
||||||
TooltipContext.displayName = 'TooltipContext'
|
TooltipContext.displayName = 'TooltipContext'
|
||||||
|
|
||||||
@@ -33,10 +38,7 @@ export function Outer({
|
|||||||
visible: boolean
|
visible: boolean
|
||||||
onVisibleChange: (visible: boolean) => void
|
onVisibleChange: (visible: boolean) => void
|
||||||
}) {
|
}) {
|
||||||
const ctx = useMemo(
|
const ctx = useMemo(() => ({position}), [position])
|
||||||
() => ({position, onVisibleChange}),
|
|
||||||
[position, onVisibleChange],
|
|
||||||
)
|
|
||||||
return (
|
return (
|
||||||
<Popover.Root open={visible} onOpenChange={onVisibleChange}>
|
<Popover.Root open={visible} onOpenChange={onVisibleChange}>
|
||||||
<TooltipContext.Provider value={ctx}>{children}</TooltipContext.Provider>
|
<TooltipContext.Provider value={ctx}>{children}</TooltipContext.Provider>
|
||||||
@@ -60,7 +62,7 @@ export function Content({
|
|||||||
label: string
|
label: string
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {position, onVisibleChange} = useContext(TooltipContext)
|
const {position} = useContext(TooltipContext)
|
||||||
return (
|
return (
|
||||||
<Popover.Portal>
|
<Popover.Portal>
|
||||||
<Popover.Content
|
<Popover.Content
|
||||||
@@ -69,7 +71,11 @@ export function Content({
|
|||||||
side={position}
|
side={position}
|
||||||
sideOffset={4}
|
sideOffset={4}
|
||||||
collisionPadding={MIN_EDGE_SPACE}
|
collisionPadding={MIN_EDGE_SPACE}
|
||||||
onInteractOutside={() => onVisibleChange(false)}
|
onInteractOutside={evt => {
|
||||||
|
if (evt.type === 'dismissableLayer.focusOutside') {
|
||||||
|
evt.preventDefault()
|
||||||
|
}
|
||||||
|
}}
|
||||||
style={flatten([
|
style={flatten([
|
||||||
a.rounded_sm,
|
a.rounded_sm,
|
||||||
select(t.name, {
|
select(t.name, {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {Fragment, useMemo} from 'react'
|
import {Fragment, useMemo, useRef} from 'react'
|
||||||
import {
|
import {
|
||||||
Keyboard,
|
Keyboard,
|
||||||
Platform,
|
Platform,
|
||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
type ThreadgateAllowUISetting,
|
type ThreadgateAllowUISetting,
|
||||||
threadgateViewToAllowUISetting,
|
threadgateViewToAllowUISetting,
|
||||||
} from '#/state/queries/threadgate'
|
} from '#/state/queries/threadgate'
|
||||||
import {atoms as a, useTheme, web} from '#/alf'
|
import {atoms as a, native, useTheme, web} from '#/alf'
|
||||||
import {Button, ButtonText} from '#/components/Button'
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import {useDialogControl} from '#/components/Dialog'
|
import {useDialogControl} from '#/components/Dialog'
|
||||||
@@ -30,13 +30,13 @@ import {
|
|||||||
PostInteractionSettingsDialog,
|
PostInteractionSettingsDialog,
|
||||||
usePrefetchPostInteractionSettings,
|
usePrefetchPostInteractionSettings,
|
||||||
} from '#/components/dialogs/PostInteractionSettingsDialog'
|
} from '#/components/dialogs/PostInteractionSettingsDialog'
|
||||||
import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign'
|
import {TinyChevronBottom_Stroke2_Corner0_Rounded as TinyChevronDownIcon} from '#/components/icons/Chevron'
|
||||||
import {Earth_Stroke2_Corner0_Rounded as Earth} from '#/components/icons/Globe'
|
import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSignIcon} from '#/components/icons/CircleBanSign'
|
||||||
import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
|
import {Earth_Stroke2_Corner0_Rounded as EarthIcon} from '#/components/icons/Globe'
|
||||||
|
import {Group3_Stroke2_Corner0_Rounded as GroupIcon} from '#/components/icons/Group'
|
||||||
import {InlineLinkText} from '#/components/Link'
|
import {InlineLinkText} from '#/components/Link'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import * as bsky from '#/types/bsky'
|
import * as bsky from '#/types/bsky'
|
||||||
import {PencilLine_Stroke2_Corner0_Rounded as PencilLine} from './icons/Pencil'
|
|
||||||
|
|
||||||
interface WhoCanReplyProps {
|
interface WhoCanReplyProps {
|
||||||
post: AppBskyFeedDefs.PostView
|
post: AppBskyFeedDefs.PostView
|
||||||
@@ -69,6 +69,11 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) {
|
|||||||
postUri: post.uri,
|
postUri: post.uri,
|
||||||
rootPostUri: rootUri,
|
rootPostUri: rootUri,
|
||||||
})
|
})
|
||||||
|
const prefetchPromise = useRef<Promise<void>>(Promise.resolve())
|
||||||
|
|
||||||
|
const prefetch = () => {
|
||||||
|
prefetchPromise.current = prefetchPostInteractionSettings()
|
||||||
|
}
|
||||||
|
|
||||||
const anyoneCanReply =
|
const anyoneCanReply =
|
||||||
settings.length === 1 && settings[0].type === 'everybody'
|
settings.length === 1 && settings[0].type === 'everybody'
|
||||||
@@ -84,7 +89,14 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) {
|
|||||||
Keyboard.dismiss()
|
Keyboard.dismiss()
|
||||||
}
|
}
|
||||||
if (isThreadAuthor) {
|
if (isThreadAuthor) {
|
||||||
|
// wait on prefetch if it manages to resolve in under 200ms
|
||||||
|
// otherwise, proceed immediately and show the spinner -sfn
|
||||||
|
Promise.race([
|
||||||
|
prefetchPromise.current,
|
||||||
|
new Promise(res => setTimeout(res, 200)),
|
||||||
|
]).finally(() => {
|
||||||
editDialogControl.open()
|
editDialogControl.open()
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
infoDialogControl.open()
|
infoDialogControl.open()
|
||||||
}
|
}
|
||||||
@@ -100,18 +112,27 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) {
|
|||||||
{...(isThreadAuthor
|
{...(isThreadAuthor
|
||||||
? Platform.select({
|
? Platform.select({
|
||||||
web: {
|
web: {
|
||||||
onHoverIn: prefetchPostInteractionSettings,
|
onHoverIn: prefetch,
|
||||||
},
|
},
|
||||||
native: {
|
native: {
|
||||||
onPressIn: prefetchPostInteractionSettings,
|
onPressIn: prefetch,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
: {})}
|
: {})}
|
||||||
hitSlop={HITSLOP_10}>
|
hitSlop={HITSLOP_10}>
|
||||||
{({hovered}) => (
|
{({hovered, focused, pressed}) => (
|
||||||
<View style={[a.flex_row, a.align_center, a.gap_xs, style]}>
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.gap_xs,
|
||||||
|
(hovered || focused || pressed) && native({opacity: 0.5}),
|
||||||
|
style,
|
||||||
|
]}>
|
||||||
<Icon
|
<Icon
|
||||||
color={t.palette.contrast_400}
|
color={
|
||||||
|
isThreadAuthor ? t.palette.primary_500 : t.palette.contrast_400
|
||||||
|
}
|
||||||
width={16}
|
width={16}
|
||||||
settings={settings}
|
settings={settings}
|
||||||
/>
|
/>
|
||||||
@@ -119,14 +140,16 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) {
|
|||||||
style={[
|
style={[
|
||||||
a.text_sm,
|
a.text_sm,
|
||||||
a.leading_tight,
|
a.leading_tight,
|
||||||
t.atoms.text_contrast_medium,
|
isThreadAuthor
|
||||||
hovered && a.underline,
|
? {color: t.palette.primary_500}
|
||||||
|
: t.atoms.text_contrast_medium,
|
||||||
|
(hovered || focused || pressed) && web(a.underline),
|
||||||
]}>
|
]}>
|
||||||
{description}
|
{description}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{isThreadAuthor && (
|
{isThreadAuthor && (
|
||||||
<PencilLine width={12} fill={t.palette.primary_500} />
|
<TinyChevronDownIcon width={8} fill={t.palette.primary_500} />
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
@@ -164,7 +187,11 @@ function Icon({
|
|||||||
settings.length === 0 ||
|
settings.length === 0 ||
|
||||||
settings.every(setting => setting.type === 'everybody')
|
settings.every(setting => setting.type === 'everybody')
|
||||||
const isNobody = !!settings.find(gate => gate.type === 'nobody')
|
const isNobody = !!settings.find(gate => gate.type === 'nobody')
|
||||||
const IconComponent = isEverybody ? Earth : isNobody ? CircleBanSign : Group
|
const IconComponent = isEverybody
|
||||||
|
? EarthIcon
|
||||||
|
: isNobody
|
||||||
|
? CircleBanSignIcon
|
||||||
|
: GroupIcon
|
||||||
return <IconComponent fill={color} width={width} />
|
return <IconComponent fill={color} width={width} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {useCallback} from 'react'
|
import {useCallback, useEffect, useState} from 'react'
|
||||||
import {type ModerationOpts} from '@atproto/api'
|
import {type ModerationOpts} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
@@ -27,8 +27,21 @@ export function SubscribeProfileButton({
|
|||||||
const subscribeDialogControl = useDialogControl()
|
const subscribeDialogControl = useDialogControl()
|
||||||
const [activitySubscriptionsNudged, setActivitySubscriptionsNudged] =
|
const [activitySubscriptionsNudged, setActivitySubscriptionsNudged] =
|
||||||
useActivitySubscriptionsNudged()
|
useActivitySubscriptionsNudged()
|
||||||
|
const [showTooltip, setShowTooltip] = useState(false)
|
||||||
|
|
||||||
const onDismissTooltip = () => {
|
useEffect(() => {
|
||||||
|
if (!activitySubscriptionsNudged) {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
setShowTooltip(true)
|
||||||
|
}, 500)
|
||||||
|
return () => clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
}, [activitySubscriptionsNudged])
|
||||||
|
|
||||||
|
const onDismissTooltip = (visible: boolean) => {
|
||||||
|
if (visible) return
|
||||||
|
|
||||||
|
setShowTooltip(false)
|
||||||
setActivitySubscriptionsNudged(true)
|
setActivitySubscriptionsNudged(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +69,7 @@ export function SubscribeProfileButton({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tooltip.Outer
|
<Tooltip.Outer
|
||||||
visible={!activitySubscriptionsNudged}
|
visible={showTooltip}
|
||||||
onVisibleChange={onDismissTooltip}
|
onVisibleChange={onDismissTooltip}
|
||||||
position="bottom">
|
position="bottom">
|
||||||
<Tooltip.Target>
|
<Tooltip.Target>
|
||||||
@@ -65,7 +78,6 @@ export function SubscribeProfileButton({
|
|||||||
testID="dmBtn"
|
testID="dmBtn"
|
||||||
size="small"
|
size="small"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
variant="solid"
|
|
||||||
shape="round"
|
shape="round"
|
||||||
label={_(msg`Get notified when ${name} posts`)}
|
label={_(msg`Get notified when ${name} posts`)}
|
||||||
onPress={wrappedOnPress}>
|
onPress={wrappedOnPress}>
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import React from 'react'
|
import {useCallback, useMemo, useState} from 'react'
|
||||||
import {type StyleProp, View, type ViewStyle} from 'react-native'
|
import {LayoutAnimation, Text as NestedText, View} from 'react-native'
|
||||||
import {
|
import {
|
||||||
type AppBskyFeedDefs,
|
type AppBskyFeedDefs,
|
||||||
type AppBskyFeedPostgate,
|
type AppBskyFeedPostgate,
|
||||||
AtUri,
|
AtUri,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Plural, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
import isEqual from 'lodash.isequal'
|
|
||||||
|
|
||||||
|
import {useHaptics} from '#/lib/haptics'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
|
import {isIOS} from '#/platform/detection'
|
||||||
import {STALE} from '#/state/queries'
|
import {STALE} from '#/state/queries'
|
||||||
import {useMyListsQuery} from '#/state/queries/my-lists'
|
import {useMyListsQuery} from '#/state/queries/my-lists'
|
||||||
import {useGetPost} from '#/state/queries/post'
|
import {useGetPost} from '#/state/queries/post'
|
||||||
@@ -37,13 +38,17 @@ import {
|
|||||||
} from '#/state/queries/usePostThread'
|
} from '#/state/queries/usePostThread'
|
||||||
import {useAgent, useSession} from '#/state/session'
|
import {useAgent, useSession} from '#/state/session'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
|
import {atoms as a, useTheme, web} from '#/alf'
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import {Divider} from '#/components/Divider'
|
|
||||||
import * as Toggle from '#/components/forms/Toggle'
|
import * as Toggle from '#/components/forms/Toggle'
|
||||||
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
import {
|
||||||
|
ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon,
|
||||||
|
ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon,
|
||||||
|
} from '#/components/icons/Chevron'
|
||||||
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
||||||
|
import {CloseQuote_Stroke2_Corner1_Rounded as QuoteIcon} from '#/components/icons/Quote'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
@@ -52,6 +57,10 @@ export type PostInteractionSettingsFormProps = {
|
|||||||
onSave: () => void
|
onSave: () => void
|
||||||
isSaving?: boolean
|
isSaving?: boolean
|
||||||
|
|
||||||
|
isDirty?: boolean
|
||||||
|
persist?: boolean
|
||||||
|
onChangePersist?: (v: boolean) => void
|
||||||
|
|
||||||
postgate: AppBskyFeedPostgate.Record
|
postgate: AppBskyFeedPostgate.Record
|
||||||
onChangePostgate: (v: AppBskyFeedPostgate.Record) => void
|
onChangePostgate: (v: AppBskyFeedPostgate.Record) => void
|
||||||
|
|
||||||
@@ -61,57 +70,39 @@ export type PostInteractionSettingsFormProps = {
|
|||||||
replySettingsDisabled?: boolean
|
replySettingsDisabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Threadgate settings dialog. Used in the composer.
|
||||||
|
*/
|
||||||
export function PostInteractionSettingsControlledDialog({
|
export function PostInteractionSettingsControlledDialog({
|
||||||
control,
|
control,
|
||||||
...rest
|
...rest
|
||||||
}: PostInteractionSettingsFormProps & {
|
}: PostInteractionSettingsFormProps & {
|
||||||
control: Dialog.DialogControlProps
|
control: Dialog.DialogControlProps
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
|
||||||
const {_} = useLingui()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.Outer control={control}>
|
<Dialog.Outer
|
||||||
|
control={control}
|
||||||
|
nativeOptions={{
|
||||||
|
preventExpansion: true,
|
||||||
|
preventDismiss: rest.isDirty && rest.persist,
|
||||||
|
}}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
<Dialog.ScrollableInner
|
<DialogInner {...rest} />
|
||||||
label={_(msg`Edit post interaction settings`)}
|
|
||||||
style={[{maxWidth: 500}, a.w_full]}>
|
|
||||||
<View style={[a.gap_md]}>
|
|
||||||
<Header />
|
|
||||||
<PostInteractionSettingsForm {...rest} />
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.pt_sm,
|
|
||||||
a.text_sm,
|
|
||||||
a.leading_snug,
|
|
||||||
t.atoms.text_contrast_medium,
|
|
||||||
]}>
|
|
||||||
<Trans>
|
|
||||||
You can set default interaction settings in{' '}
|
|
||||||
<Text style={[a.font_semi_bold, t.atoms.text_contrast_medium]}>
|
|
||||||
Settings → Moderation → Interaction settings
|
|
||||||
</Text>
|
|
||||||
.
|
|
||||||
</Trans>
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<Dialog.Close />
|
|
||||||
</Dialog.ScrollableInner>
|
|
||||||
</Dialog.Outer>
|
</Dialog.Outer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Header() {
|
function DialogInner(props: Omit<PostInteractionSettingsFormProps, 'control'>) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.gap_md, a.pb_sm]}>
|
<Dialog.ScrollableInner
|
||||||
<Text style={[a.text_2xl, a.font_semi_bold]}>
|
label={_(msg`Edit post interaction settings`)}
|
||||||
<Trans>Post interaction settings</Trans>
|
style={[web({maxWidth: 400}), a.w_full]}>
|
||||||
</Text>
|
<Header />
|
||||||
<Text style={[a.text_md, a.pb_xs]}>
|
<PostInteractionSettingsForm {...props} />
|
||||||
<Trans>Customize who can interact with this post.</Trans>
|
<Dialog.Close />
|
||||||
</Text>
|
</Dialog.ScrollableInner>
|
||||||
<Divider />
|
|
||||||
</View>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,12 +125,17 @@ export type PostInteractionSettingsDialogProps = {
|
|||||||
initialThreadgateView?: AppBskyFeedDefs.ThreadgateView
|
initialThreadgateView?: AppBskyFeedDefs.ThreadgateView
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Threadgate settings dialog. Used in the thread.
|
||||||
|
*/
|
||||||
export function PostInteractionSettingsDialog(
|
export function PostInteractionSettingsDialog(
|
||||||
props: PostInteractionSettingsDialogProps,
|
props: PostInteractionSettingsDialogProps,
|
||||||
) {
|
) {
|
||||||
const postThreadContext = usePostThreadContext()
|
const postThreadContext = usePostThreadContext()
|
||||||
return (
|
return (
|
||||||
<Dialog.Outer control={props.control}>
|
<Dialog.Outer
|
||||||
|
control={props.control}
|
||||||
|
nativeOptions={{preventExpansion: true}}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
<PostThreadContextProvider context={postThreadContext}>
|
<PostThreadContextProvider context={postThreadContext}>
|
||||||
<PostInteractionSettingsDialogControlledInner {...props} />
|
<PostInteractionSettingsDialogControlledInner {...props} />
|
||||||
@@ -153,7 +149,7 @@ export function PostInteractionSettingsDialogControlledInner(
|
|||||||
) {
|
) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const [isSaving, setIsSaving] = React.useState(false)
|
const [isSaving, setIsSaving] = useState(false)
|
||||||
|
|
||||||
const {data: threadgateViewLoaded, isLoading: isLoadingThreadgate} =
|
const {data: threadgateViewLoaded, isLoading: isLoadingThreadgate} =
|
||||||
useThreadgateViewQuery({postUri: props.rootPostUri})
|
useThreadgateViewQuery({postUri: props.rootPostUri})
|
||||||
@@ -165,28 +161,28 @@ export function PostInteractionSettingsDialogControlledInner(
|
|||||||
const {mutateAsync: setThreadgateAllow} = useSetThreadgateAllowMutation()
|
const {mutateAsync: setThreadgateAllow} = useSetThreadgateAllowMutation()
|
||||||
|
|
||||||
const [editedPostgate, setEditedPostgate] =
|
const [editedPostgate, setEditedPostgate] =
|
||||||
React.useState<AppBskyFeedPostgate.Record>()
|
useState<AppBskyFeedPostgate.Record>()
|
||||||
const [editedAllowUISettings, setEditedAllowUISettings] =
|
const [editedAllowUISettings, setEditedAllowUISettings] =
|
||||||
React.useState<ThreadgateAllowUISetting[]>()
|
useState<ThreadgateAllowUISetting[]>()
|
||||||
|
|
||||||
const isLoading = isLoadingThreadgate || isLoadingPostgate
|
const isLoading = isLoadingThreadgate || isLoadingPostgate
|
||||||
const threadgateView = threadgateViewLoaded || props.initialThreadgateView
|
const threadgateView = threadgateViewLoaded || props.initialThreadgateView
|
||||||
const isThreadgateOwnedByViewer = React.useMemo(() => {
|
const isThreadgateOwnedByViewer = useMemo(() => {
|
||||||
return currentAccount?.did === new AtUri(props.rootPostUri).host
|
return currentAccount?.did === new AtUri(props.rootPostUri).host
|
||||||
}, [props.rootPostUri, currentAccount?.did])
|
}, [props.rootPostUri, currentAccount?.did])
|
||||||
|
|
||||||
const postgateValue = React.useMemo(() => {
|
const postgateValue = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
editedPostgate || postgate || createPostgateRecord({post: props.postUri})
|
editedPostgate || postgate || createPostgateRecord({post: props.postUri})
|
||||||
)
|
)
|
||||||
}, [postgate, editedPostgate, props.postUri])
|
}, [postgate, editedPostgate, props.postUri])
|
||||||
const allowUIValue = React.useMemo(() => {
|
const allowUIValue = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
editedAllowUISettings || threadgateViewToAllowUISetting(threadgateView)
|
editedAllowUISettings || threadgateViewToAllowUISetting(threadgateView)
|
||||||
)
|
)
|
||||||
}, [threadgateView, editedAllowUISettings])
|
}, [threadgateView, editedAllowUISettings])
|
||||||
|
|
||||||
const onSave = React.useCallback(async () => {
|
const onSave = useCallback(async () => {
|
||||||
if (!editedPostgate && !editedAllowUISettings) {
|
if (!editedPostgate && !editedAllowUISettings) {
|
||||||
props.control.close()
|
props.control.close()
|
||||||
return
|
return
|
||||||
@@ -248,15 +244,24 @@ export function PostInteractionSettingsDialogControlledInner(
|
|||||||
return (
|
return (
|
||||||
<Dialog.ScrollableInner
|
<Dialog.ScrollableInner
|
||||||
label={_(msg`Edit post interaction settings`)}
|
label={_(msg`Edit post interaction settings`)}
|
||||||
style={[{maxWidth: 500}, a.w_full]}>
|
style={[web({maxWidth: 400}), a.w_full]}>
|
||||||
<View style={[a.gap_md]}>
|
|
||||||
<Header />
|
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<View style={[a.flex_1, a.py_4xl, a.align_center, a.justify_center]}>
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.py_5xl,
|
||||||
|
a.gap_md,
|
||||||
|
a.align_center,
|
||||||
|
a.justify_center,
|
||||||
|
]}>
|
||||||
<Loader size="xl" />
|
<Loader size="xl" />
|
||||||
|
<Text style={[a.italic, a.text_center]}>
|
||||||
|
<Trans>Loading post interaction settings...</Trans>
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
|
<>
|
||||||
|
<Header />
|
||||||
<PostInteractionSettingsForm
|
<PostInteractionSettingsForm
|
||||||
replySettingsDisabled={!isThreadgateOwnedByViewer}
|
replySettingsDisabled={!isThreadgateOwnedByViewer}
|
||||||
isSaving={isSaving}
|
isSaving={isSaving}
|
||||||
@@ -266,8 +271,9 @@ export function PostInteractionSettingsDialogControlledInner(
|
|||||||
threadgateAllowUISettings={allowUIValue}
|
threadgateAllowUISettings={allowUIValue}
|
||||||
onChangeThreadgateAllowUISettings={setEditedAllowUISettings}
|
onChangeThreadgateAllowUISettings={setEditedAllowUISettings}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</View>
|
<Dialog.Close />
|
||||||
</Dialog.ScrollableInner>
|
</Dialog.ScrollableInner>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -281,11 +287,20 @@ export function PostInteractionSettingsForm({
|
|||||||
threadgateAllowUISettings,
|
threadgateAllowUISettings,
|
||||||
onChangeThreadgateAllowUISettings,
|
onChangeThreadgateAllowUISettings,
|
||||||
replySettingsDisabled,
|
replySettingsDisabled,
|
||||||
|
isDirty,
|
||||||
|
persist,
|
||||||
|
onChangePersist,
|
||||||
}: PostInteractionSettingsFormProps) {
|
}: PostInteractionSettingsFormProps) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {data: lists} = useMyListsQuery('curate')
|
const playHaptic = useHaptics()
|
||||||
const [quotesEnabled, setQuotesEnabled] = React.useState(
|
const [showLists, setShowLists] = useState(false)
|
||||||
|
const {
|
||||||
|
data: lists,
|
||||||
|
isPending: isListsPending,
|
||||||
|
isError: isListsError,
|
||||||
|
} = useMyListsQuery('curate')
|
||||||
|
const [quotesEnabled, setQuotesEnabled] = useState(
|
||||||
!(
|
!(
|
||||||
postgate.embeddingRules &&
|
postgate.embeddingRules &&
|
||||||
postgate.embeddingRules.find(
|
postgate.embeddingRules.find(
|
||||||
@@ -294,27 +309,7 @@ export function PostInteractionSettingsForm({
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const onPressAudience = (setting: ThreadgateAllowUISetting) => {
|
const onChangeQuotesEnabled = useCallback(
|
||||||
// remove boolean values
|
|
||||||
let newSelected: ThreadgateAllowUISetting[] =
|
|
||||||
threadgateAllowUISettings.filter(
|
|
||||||
v => v.type !== 'nobody' && v.type !== 'everybody',
|
|
||||||
)
|
|
||||||
// toggle
|
|
||||||
const i = newSelected.findIndex(v => isEqual(v, setting))
|
|
||||||
if (i === -1) {
|
|
||||||
newSelected.push(setting)
|
|
||||||
} else {
|
|
||||||
newSelected.splice(i, 1)
|
|
||||||
}
|
|
||||||
if (newSelected.length === 0) {
|
|
||||||
newSelected.push({type: 'everybody'})
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeThreadgateAllowUISettings(newSelected)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onChangeQuotesEnabled = React.useCallback(
|
|
||||||
(enabled: boolean) => {
|
(enabled: boolean) => {
|
||||||
setQuotesEnabled(enabled)
|
setQuotesEnabled(enabled)
|
||||||
onChangePostgate(
|
onChangePostgate(
|
||||||
@@ -330,36 +325,62 @@ export function PostInteractionSettingsForm({
|
|||||||
const noOneCanReply = !!threadgateAllowUISettings.find(
|
const noOneCanReply = !!threadgateAllowUISettings.find(
|
||||||
v => v.type === 'nobody',
|
v => v.type === 'nobody',
|
||||||
)
|
)
|
||||||
|
const everyoneCanReply = !!threadgateAllowUISettings.find(
|
||||||
|
v => v.type === 'everybody',
|
||||||
|
)
|
||||||
|
const numberOfListsSelected = threadgateAllowUISettings.filter(
|
||||||
|
v => v.type === 'list',
|
||||||
|
).length
|
||||||
|
|
||||||
|
const toggleGroupValues = useMemo(() => {
|
||||||
|
const values: string[] = []
|
||||||
|
for (const setting of threadgateAllowUISettings) {
|
||||||
|
switch (setting.type) {
|
||||||
|
case 'everybody':
|
||||||
|
case 'nobody':
|
||||||
|
// no granularity, early return with nothing
|
||||||
|
return []
|
||||||
|
case 'followers':
|
||||||
|
values.push('followers')
|
||||||
|
break
|
||||||
|
case 'following':
|
||||||
|
values.push('following')
|
||||||
|
break
|
||||||
|
case 'mention':
|
||||||
|
values.push('mention')
|
||||||
|
break
|
||||||
|
case 'list':
|
||||||
|
values.push(`list:${setting.list}`)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return values
|
||||||
|
}, [threadgateAllowUISettings])
|
||||||
|
|
||||||
|
const toggleGroupOnChange = (values: string[]) => {
|
||||||
|
const settings: ThreadgateAllowUISetting[] = []
|
||||||
|
|
||||||
|
if (values.length === 0) {
|
||||||
|
settings.push({type: 'everybody'})
|
||||||
|
} else {
|
||||||
|
for (const value of values) {
|
||||||
|
if (value.startsWith('list:')) {
|
||||||
|
const listId = value.slice('list:'.length)
|
||||||
|
settings.push({type: 'list', list: listId})
|
||||||
|
} else {
|
||||||
|
settings.push({type: value as 'followers' | 'following' | 'mention'})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeThreadgateAllowUISettings(settings)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View style={[a.flex_1, a.gap_lg]}>
|
||||||
<View style={[a.flex_1, a.gap_md]}>
|
|
||||||
<View style={[a.gap_lg]}>
|
<View style={[a.gap_lg]}>
|
||||||
<View style={[a.gap_sm]}>
|
|
||||||
<Text style={[a.font_semi_bold, a.text_lg]}>
|
|
||||||
<Trans>Quote settings</Trans>
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Toggle.Item
|
|
||||||
name="quoteposts"
|
|
||||||
type="checkbox"
|
|
||||||
label={
|
|
||||||
quotesEnabled
|
|
||||||
? _(msg`Click to disable quote posts of this post.`)
|
|
||||||
: _(msg`Click to enable quote posts of this post.`)
|
|
||||||
}
|
|
||||||
value={quotesEnabled}
|
|
||||||
onChange={onChangeQuotesEnabled}
|
|
||||||
style={[a.justify_between, a.pt_xs]}>
|
|
||||||
<Text style={[t.atoms.text_contrast_medium]}>
|
|
||||||
<Trans>Allow quote posts</Trans>
|
|
||||||
</Text>
|
|
||||||
<Toggle.Switch />
|
|
||||||
</Toggle.Item>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
{replySettingsDisabled && (
|
{replySettingsDisabled && (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
@@ -373,11 +394,7 @@ export function PostInteractionSettingsForm({
|
|||||||
]}>
|
]}>
|
||||||
<CircleInfo fill={t.atoms.text_contrast_low.color} />
|
<CircleInfo fill={t.atoms.text_contrast_low.color} />
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[a.flex_1, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||||
a.flex_1,
|
|
||||||
a.leading_snug,
|
|
||||||
t.atoms.text_contrast_medium,
|
|
||||||
]}>
|
|
||||||
<Trans>
|
<Trans>
|
||||||
Reply settings are chosen by the author of the thread
|
Reply settings are chosen by the author of the thread
|
||||||
</Trans>
|
</Trans>
|
||||||
@@ -385,174 +402,270 @@ export function PostInteractionSettingsForm({
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<View
|
<View style={[a.gap_sm, {opacity: replySettingsDisabled ? 0.3 : 1}]}>
|
||||||
style={[
|
<Text style={[a.text_md, a.font_medium]}>
|
||||||
a.gap_sm,
|
<Trans>Who can reply</Trans>
|
||||||
{
|
|
||||||
opacity: replySettingsDisabled ? 0.3 : 1,
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
<Text style={[a.font_semi_bold, a.text_lg]}>
|
|
||||||
<Trans>Reply settings</Trans>
|
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text style={[a.pt_sm, t.atoms.text_contrast_medium]}>
|
<Toggle.Group
|
||||||
<Trans>Allow replies from:</Trans>
|
label={_(msg`Set who can reply to your post`)}
|
||||||
</Text>
|
type="radio"
|
||||||
|
maxSelections={1}
|
||||||
<View style={[a.flex_row, a.gap_sm]}>
|
disabled={replySettingsDisabled}
|
||||||
<Selectable
|
values={
|
||||||
label={_(msg`Everybody`)}
|
everyoneCanReply ? ['everyone'] : noOneCanReply ? ['nobody'] : []
|
||||||
isSelected={
|
|
||||||
!!threadgateAllowUISettings.find(v => v.type === 'everybody')
|
|
||||||
}
|
}
|
||||||
onPress={() =>
|
onChange={val => {
|
||||||
|
if (val.includes('everyone')) {
|
||||||
onChangeThreadgateAllowUISettings([{type: 'everybody'}])
|
onChangeThreadgateAllowUISettings([{type: 'everybody'}])
|
||||||
}
|
} else if (val.includes('nobody')) {
|
||||||
style={{flex: 1}}
|
|
||||||
disabled={replySettingsDisabled}
|
|
||||||
/>
|
|
||||||
<Selectable
|
|
||||||
label={_(msg`Nobody`)}
|
|
||||||
isSelected={noOneCanReply}
|
|
||||||
onPress={() =>
|
|
||||||
onChangeThreadgateAllowUISettings([{type: 'nobody'}])
|
onChangeThreadgateAllowUISettings([{type: 'nobody'}])
|
||||||
|
} else {
|
||||||
|
onChangeThreadgateAllowUISettings([{type: 'mention'}])
|
||||||
}
|
}
|
||||||
style={{flex: 1}}
|
}}>
|
||||||
disabled={replySettingsDisabled}
|
<View style={[a.flex_row, a.gap_sm]}>
|
||||||
/>
|
<Toggle.Item
|
||||||
|
name="everyone"
|
||||||
|
type="checkbox"
|
||||||
|
label={_(msg`Allow anyone to reply`)}
|
||||||
|
style={[a.flex_1]}>
|
||||||
|
{({selected}) => (
|
||||||
|
<Toggle.Panel active={selected}>
|
||||||
|
<Toggle.Radio />
|
||||||
|
<Toggle.PanelText>
|
||||||
|
<Trans>Anyone</Trans>
|
||||||
|
</Toggle.PanelText>
|
||||||
|
</Toggle.Panel>
|
||||||
|
)}
|
||||||
|
</Toggle.Item>
|
||||||
|
<Toggle.Item
|
||||||
|
name="nobody"
|
||||||
|
type="checkbox"
|
||||||
|
label={_(msg`Disable replies entirely`)}
|
||||||
|
style={[a.flex_1]}>
|
||||||
|
{({selected}) => (
|
||||||
|
<Toggle.Panel active={selected}>
|
||||||
|
<Toggle.Radio />
|
||||||
|
<Toggle.PanelText>
|
||||||
|
<Trans>Nobody</Trans>
|
||||||
|
</Toggle.PanelText>
|
||||||
|
</Toggle.Panel>
|
||||||
|
)}
|
||||||
|
</Toggle.Item>
|
||||||
</View>
|
</View>
|
||||||
|
</Toggle.Group>
|
||||||
|
|
||||||
{!noOneCanReply && (
|
<Toggle.Group
|
||||||
<>
|
label={_(
|
||||||
<Text style={[a.pt_sm, t.atoms.text_contrast_medium]}>
|
msg`Set precisely which groups of people can reply to your post`,
|
||||||
<Trans>Or combine these options:</Trans>
|
)}
|
||||||
</Text>
|
values={toggleGroupValues}
|
||||||
|
onChange={toggleGroupOnChange}
|
||||||
|
disabled={replySettingsDisabled}>
|
||||||
|
<Toggle.PanelGroup>
|
||||||
|
<Toggle.Item
|
||||||
|
name="followers"
|
||||||
|
type="checkbox"
|
||||||
|
label={_(msg`Allow your followers to reply`)}
|
||||||
|
hitSlop={0}>
|
||||||
|
{({selected}) => (
|
||||||
|
<Toggle.Panel active={selected} adjacent="trailing">
|
||||||
|
<Toggle.Checkbox />
|
||||||
|
<Toggle.PanelText>
|
||||||
|
<Trans>Your followers</Trans>
|
||||||
|
</Toggle.PanelText>
|
||||||
|
</Toggle.Panel>
|
||||||
|
)}
|
||||||
|
</Toggle.Item>
|
||||||
|
<Toggle.Item
|
||||||
|
name="following"
|
||||||
|
type="checkbox"
|
||||||
|
label={_(msg`Allow people you follow to reply`)}
|
||||||
|
hitSlop={0}>
|
||||||
|
{({selected}) => (
|
||||||
|
<Toggle.Panel active={selected} adjacent="both">
|
||||||
|
<Toggle.Checkbox />
|
||||||
|
<Toggle.PanelText>
|
||||||
|
<Trans>People you follow</Trans>
|
||||||
|
</Toggle.PanelText>
|
||||||
|
</Toggle.Panel>
|
||||||
|
)}
|
||||||
|
</Toggle.Item>
|
||||||
|
<Toggle.Item
|
||||||
|
name="mention"
|
||||||
|
type="checkbox"
|
||||||
|
label={_(msg`Allow people you mention to reply`)}
|
||||||
|
hitSlop={0}>
|
||||||
|
{({selected}) => (
|
||||||
|
<Toggle.Panel active={selected} adjacent="both">
|
||||||
|
<Toggle.Checkbox />
|
||||||
|
<Toggle.PanelText>
|
||||||
|
<Trans>People you mention</Trans>
|
||||||
|
</Toggle.PanelText>
|
||||||
|
</Toggle.Panel>
|
||||||
|
)}
|
||||||
|
</Toggle.Item>
|
||||||
|
|
||||||
<View style={[a.gap_sm]}>
|
<Button
|
||||||
<Selectable
|
label={
|
||||||
label={_(msg`Mentioned users`)}
|
showLists
|
||||||
isSelected={
|
? _(msg`Hide lists`)
|
||||||
!!threadgateAllowUISettings.find(
|
: _(msg`Show lists of users to select from`)
|
||||||
v => v.type === 'mention',
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
onPress={() => onPressAudience({type: 'mention'})}
|
accessibilityHint={_(msg`Toggle showing lists`)}
|
||||||
disabled={replySettingsDisabled}
|
accessibilityRole="togglebutton"
|
||||||
/>
|
hitSlop={0}
|
||||||
<Selectable
|
onPress={() => {
|
||||||
label={_(msg`Users you follow`)}
|
playHaptic('Light')
|
||||||
isSelected={
|
if (isIOS && !showLists) {
|
||||||
!!threadgateAllowUISettings.find(
|
LayoutAnimation.configureNext({
|
||||||
v => v.type === 'following',
|
...LayoutAnimation.Presets.linear,
|
||||||
)
|
duration: 175,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
onPress={() => onPressAudience({type: 'following'})}
|
setShowLists(s => !s)
|
||||||
disabled={replySettingsDisabled}
|
}}>
|
||||||
|
<Toggle.Panel
|
||||||
|
active={numberOfListsSelected > 0}
|
||||||
|
adjacent={showLists ? 'both' : 'leading'}>
|
||||||
|
<Toggle.PanelText>
|
||||||
|
{numberOfListsSelected === 0 ? (
|
||||||
|
<Trans>Select from your lists</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>
|
||||||
|
Select from your lists{' '}
|
||||||
|
<NestedText style={[a.font_normal, a.italic]}>
|
||||||
|
<Plural
|
||||||
|
value={numberOfListsSelected}
|
||||||
|
other="(# selected)"
|
||||||
/>
|
/>
|
||||||
<Selectable
|
</NestedText>
|
||||||
label={_(msg`Your followers`)}
|
</Trans>
|
||||||
isSelected={
|
)}
|
||||||
!!threadgateAllowUISettings.find(
|
</Toggle.PanelText>
|
||||||
v => v.type === 'followers',
|
<Toggle.PanelIcon
|
||||||
)
|
icon={showLists ? ChevronUpIcon : ChevronDownIcon}
|
||||||
}
|
|
||||||
onPress={() => onPressAudience({type: 'followers'})}
|
|
||||||
disabled={replySettingsDisabled}
|
|
||||||
/>
|
/>
|
||||||
{lists && lists.length > 0
|
</Toggle.Panel>
|
||||||
? lists.map(list => (
|
</Button>
|
||||||
<Selectable
|
{showLists &&
|
||||||
|
(isListsPending ? (
|
||||||
|
<Toggle.Panel>
|
||||||
|
<Toggle.PanelText>
|
||||||
|
<Trans>Loading lists...</Trans>
|
||||||
|
</Toggle.PanelText>
|
||||||
|
</Toggle.Panel>
|
||||||
|
) : isListsError ? (
|
||||||
|
<Toggle.Panel>
|
||||||
|
<Toggle.PanelText>
|
||||||
|
<Trans>
|
||||||
|
An error occurred while loading your lists :/
|
||||||
|
</Trans>
|
||||||
|
</Toggle.PanelText>
|
||||||
|
</Toggle.Panel>
|
||||||
|
) : lists.length === 0 ? (
|
||||||
|
<Toggle.Panel>
|
||||||
|
<Toggle.PanelText>
|
||||||
|
<Trans>You don't have any lists yet.</Trans>
|
||||||
|
</Toggle.PanelText>
|
||||||
|
</Toggle.Panel>
|
||||||
|
) : (
|
||||||
|
lists.map((list, i) => (
|
||||||
|
<Toggle.Item
|
||||||
key={list.uri}
|
key={list.uri}
|
||||||
label={_(msg`Users in "${list.name}"`)}
|
name={`list:${list.uri}`}
|
||||||
isSelected={
|
type="checkbox"
|
||||||
!!threadgateAllowUISettings.find(
|
label={_(msg`Allow users in ${list.name} to reply`)}
|
||||||
v => v.type === 'list' && v.list === list.uri,
|
hitSlop={0}>
|
||||||
)
|
{({selected}) => (
|
||||||
}
|
<Toggle.Panel
|
||||||
onPress={() =>
|
active={selected}
|
||||||
onPressAudience({type: 'list', list: list.uri})
|
adjacent={
|
||||||
}
|
i === lists.length - 1 ? 'leading' : 'both'
|
||||||
disabled={replySettingsDisabled}
|
}>
|
||||||
|
<Toggle.Checkbox />
|
||||||
|
<UserAvatar
|
||||||
|
size={24}
|
||||||
|
type="list"
|
||||||
|
avatar={list.avatar}
|
||||||
/>
|
/>
|
||||||
|
<Toggle.PanelText>{list.name}</Toggle.PanelText>
|
||||||
|
</Toggle.Panel>
|
||||||
|
)}
|
||||||
|
</Toggle.Item>
|
||||||
))
|
))
|
||||||
: // No loading states to avoid jumps for the common case (no lists)
|
))}
|
||||||
null}
|
</Toggle.PanelGroup>
|
||||||
|
</Toggle.Group>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</View>
|
||||||
|
|
||||||
|
<Toggle.Item
|
||||||
|
name="quoteposts"
|
||||||
|
type="checkbox"
|
||||||
|
label={
|
||||||
|
quotesEnabled
|
||||||
|
? _(msg`Disable quote posts of this post.`)
|
||||||
|
: _(msg`Enable quote posts of this post.`)
|
||||||
|
}
|
||||||
|
value={quotesEnabled}
|
||||||
|
onChange={onChangeQuotesEnabled}>
|
||||||
|
{({selected}) => (
|
||||||
|
<Toggle.Panel active={selected}>
|
||||||
|
<Toggle.PanelText icon={QuoteIcon}>
|
||||||
|
<Trans>Allow quote posts</Trans>
|
||||||
|
</Toggle.PanelText>
|
||||||
|
<Toggle.Switch />
|
||||||
|
</Toggle.Panel>
|
||||||
|
)}
|
||||||
|
</Toggle.Item>
|
||||||
|
|
||||||
|
{typeof persist !== 'undefined' && (
|
||||||
|
<View style={[{minHeight: 24}, a.justify_center]}>
|
||||||
|
{isDirty ? (
|
||||||
|
<Toggle.Item
|
||||||
|
name="persist"
|
||||||
|
type="checkbox"
|
||||||
|
label={_(msg`Save these options for next time`)}
|
||||||
|
value={persist}
|
||||||
|
onChange={() => onChangePersist?.(!persist)}>
|
||||||
|
<Toggle.Checkbox />
|
||||||
|
<Toggle.LabelText
|
||||||
|
style={[a.text_md, a.font_normal, t.atoms.text]}>
|
||||||
|
<Trans>Save these options for next time</Trans>
|
||||||
|
</Toggle.LabelText>
|
||||||
|
</Toggle.Item>
|
||||||
|
) : (
|
||||||
|
<Text style={[a.text_md, t.atoms.text_contrast_medium]}>
|
||||||
|
<Trans>These are your default settings</Trans>
|
||||||
|
</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
)}
|
||||||
</View>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
disabled={!canSave || isSaving}
|
disabled={!canSave || isSaving}
|
||||||
label={_(msg`Save`)}
|
label={_(msg`Save`)}
|
||||||
onPress={onSave}
|
onPress={onSave}
|
||||||
color="primary"
|
color="primary"
|
||||||
size="large"
|
size="large">
|
||||||
variant="solid"
|
<ButtonText>
|
||||||
style={a.mt_xl}>
|
<Trans>Save</Trans>
|
||||||
<ButtonText>{_(msg`Save`)}</ButtonText>
|
</ButtonText>
|
||||||
{isSaving && <ButtonIcon icon={Loader} position="right" />}
|
{isSaving && <ButtonIcon icon={Loader} />}
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Selectable({
|
function Header() {
|
||||||
label,
|
|
||||||
isSelected,
|
|
||||||
onPress,
|
|
||||||
style,
|
|
||||||
disabled,
|
|
||||||
}: {
|
|
||||||
label: string
|
|
||||||
isSelected: boolean
|
|
||||||
onPress: () => void
|
|
||||||
style?: StyleProp<ViewStyle>
|
|
||||||
disabled?: boolean
|
|
||||||
}) {
|
|
||||||
const t = useTheme()
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<View style={[a.pb_lg]}>
|
||||||
disabled={disabled}
|
<Text style={[a.text_2xl, a.font_bold]}>
|
||||||
onPress={onPress}
|
<Trans>Post interaction settings</Trans>
|
||||||
label={label}
|
|
||||||
accessibilityRole="checkbox"
|
|
||||||
aria-checked={isSelected}
|
|
||||||
accessibilityState={{
|
|
||||||
checked: isSelected,
|
|
||||||
}}
|
|
||||||
style={a.flex_1}>
|
|
||||||
{({hovered, focused}) => (
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.flex_1,
|
|
||||||
a.flex_row,
|
|
||||||
a.align_center,
|
|
||||||
a.justify_between,
|
|
||||||
a.rounded_sm,
|
|
||||||
a.p_md,
|
|
||||||
{minHeight: 40}, // for consistency with checkmark icon visible or not
|
|
||||||
t.atoms.bg_contrast_50,
|
|
||||||
(hovered || focused) && t.atoms.bg_contrast_100,
|
|
||||||
isSelected && {
|
|
||||||
backgroundColor: t.palette.primary_100,
|
|
||||||
},
|
|
||||||
style,
|
|
||||||
]}>
|
|
||||||
<Text style={[a.text_sm, isSelected && a.font_semi_bold]}>
|
|
||||||
{label}
|
|
||||||
</Text>
|
</Text>
|
||||||
{isSelected ? (
|
|
||||||
<Check size="sm" fill={t.palette.primary_500} />
|
|
||||||
) : (
|
|
||||||
<View />
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -567,7 +680,7 @@ export function usePrefetchPostInteractionSettings({
|
|||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
const getPost = useGetPost()
|
const getPost = useGetPost()
|
||||||
|
|
||||||
return React.useCallback(async () => {
|
return useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
queryClient.prefetchQuery({
|
queryClient.prefetchQuery({
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import {createContext, useContext} from 'react'
|
||||||
|
import {View, type ViewStyle} from 'react-native'
|
||||||
|
|
||||||
|
import {atoms as a, tokens, useTheme} from '#/alf'
|
||||||
|
import {type Props as SVGIconProps} from '#/components/icons/common'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
|
const PanelContext = createContext<{active: boolean}>({active: false})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A nice container for Toggles. See the Threadgate dialog for an example.
|
||||||
|
*/
|
||||||
|
export function Panel({
|
||||||
|
children,
|
||||||
|
active = false,
|
||||||
|
adjacent,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
active?: boolean
|
||||||
|
adjacent?: 'leading' | 'trailing' | 'both'
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
|
||||||
|
const leading = adjacent === 'leading' || adjacent === 'both'
|
||||||
|
const trailing = adjacent === 'trailing' || adjacent === 'both'
|
||||||
|
const rounding = {
|
||||||
|
borderTopLeftRadius: leading
|
||||||
|
? tokens.borderRadius.xs
|
||||||
|
: tokens.borderRadius.md,
|
||||||
|
borderTopRightRadius: leading
|
||||||
|
? tokens.borderRadius.xs
|
||||||
|
: tokens.borderRadius.md,
|
||||||
|
borderBottomLeftRadius: trailing
|
||||||
|
? tokens.borderRadius.xs
|
||||||
|
: tokens.borderRadius.md,
|
||||||
|
borderBottomRightRadius: trailing
|
||||||
|
? tokens.borderRadius.xs
|
||||||
|
: tokens.borderRadius.md,
|
||||||
|
} satisfies ViewStyle
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.w_full,
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.gap_sm,
|
||||||
|
a.px_md,
|
||||||
|
a.py_md,
|
||||||
|
{minHeight: tokens.space._2xl + tokens.space.md * 2},
|
||||||
|
rounding,
|
||||||
|
active
|
||||||
|
? {backgroundColor: t.palette.primary_50}
|
||||||
|
: t.atoms.bg_contrast_50,
|
||||||
|
]}>
|
||||||
|
<PanelContext value={{active}}>{children}</PanelContext>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PanelText({
|
||||||
|
children,
|
||||||
|
icon,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
icon?: React.ComponentType<SVGIconProps>
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const ctx = useContext(PanelContext)
|
||||||
|
|
||||||
|
const text = (
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.text_md,
|
||||||
|
a.flex_1,
|
||||||
|
ctx.active
|
||||||
|
? [a.font_medium, t.atoms.text]
|
||||||
|
: [t.atoms.text_contrast_medium],
|
||||||
|
]}>
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (icon) {
|
||||||
|
// eslint-disable-next-line bsky-internal/avoid-unwrapped-text
|
||||||
|
return (
|
||||||
|
<View style={[a.flex_row, a.align_center, a.gap_xs, a.flex_1]}>
|
||||||
|
<PanelIcon icon={icon} />
|
||||||
|
{text}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PanelIcon({
|
||||||
|
icon: Icon,
|
||||||
|
}: {
|
||||||
|
icon: React.ComponentType<SVGIconProps>
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const ctx = useContext(PanelContext)
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
style={[
|
||||||
|
ctx.active ? t.atoms.text : t.atoms.text_contrast_medium,
|
||||||
|
a.flex_shrink_0,
|
||||||
|
]}
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A group of panels. TODO: auto-leading/trailing
|
||||||
|
*/
|
||||||
|
export function PanelGroup({children}: {children: React.ReactNode}) {
|
||||||
|
return <View style={[a.w_full, a.gap_2xs]}>{children}</View>
|
||||||
|
}
|
||||||
@@ -1,12 +1,20 @@
|
|||||||
import React from 'react'
|
import {createContext, useCallback, useContext, useMemo} from 'react'
|
||||||
import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native'
|
import {
|
||||||
import Animated, {LinearTransition} from 'react-native-reanimated'
|
Pressable,
|
||||||
|
type PressableProps,
|
||||||
|
type StyleProp,
|
||||||
|
View,
|
||||||
|
type ViewStyle,
|
||||||
|
} from 'react-native'
|
||||||
|
import Animated, {Easing, LinearTransition} from 'react-native-reanimated'
|
||||||
|
|
||||||
import {HITSLOP_10} from '#/lib/constants'
|
import {HITSLOP_10} from '#/lib/constants'
|
||||||
|
import {useHaptics} from '#/lib/haptics'
|
||||||
import {isNative} from '#/platform/detection'
|
import {isNative} from '#/platform/detection'
|
||||||
import {
|
import {
|
||||||
atoms as a,
|
atoms as a,
|
||||||
native,
|
native,
|
||||||
|
platform,
|
||||||
type TextStyleProp,
|
type TextStyleProp,
|
||||||
useTheme,
|
useTheme,
|
||||||
type ViewStyleProp,
|
type ViewStyleProp,
|
||||||
@@ -15,6 +23,8 @@ import {useInteractionState} from '#/components/hooks/useInteractionState'
|
|||||||
import {CheckThick_Stroke2_Corner0_Rounded as Checkmark} from '#/components/icons/Check'
|
import {CheckThick_Stroke2_Corner0_Rounded as Checkmark} from '#/components/icons/Check'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
|
export * from './Panel'
|
||||||
|
|
||||||
export type ItemState = {
|
export type ItemState = {
|
||||||
name: string
|
name: string
|
||||||
selected: boolean
|
selected: boolean
|
||||||
@@ -25,7 +35,7 @@ export type ItemState = {
|
|||||||
focused: boolean
|
focused: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const ItemContext = React.createContext<ItemState>({
|
const ItemContext = createContext<ItemState>({
|
||||||
name: '',
|
name: '',
|
||||||
selected: false,
|
selected: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
@@ -36,7 +46,7 @@ const ItemContext = React.createContext<ItemState>({
|
|||||||
})
|
})
|
||||||
ItemContext.displayName = 'ToggleItemContext'
|
ItemContext.displayName = 'ToggleItemContext'
|
||||||
|
|
||||||
const GroupContext = React.createContext<{
|
const GroupContext = createContext<{
|
||||||
values: string[]
|
values: string[]
|
||||||
disabled: boolean
|
disabled: boolean
|
||||||
type: 'radio' | 'checkbox'
|
type: 'radio' | 'checkbox'
|
||||||
@@ -70,10 +80,11 @@ export type ItemProps = ViewStyleProp & {
|
|||||||
onChange?: (selected: boolean) => void
|
onChange?: (selected: boolean) => void
|
||||||
isInvalid?: boolean
|
isInvalid?: boolean
|
||||||
children: ((props: ItemState) => React.ReactNode) | React.ReactNode
|
children: ((props: ItemState) => React.ReactNode) | React.ReactNode
|
||||||
|
hitSlop?: PressableProps['hitSlop']
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useItemContext() {
|
export function useItemContext() {
|
||||||
return React.useContext(ItemContext)
|
return useContext(ItemContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Group({
|
export function Group({
|
||||||
@@ -88,9 +99,8 @@ export function Group({
|
|||||||
}: GroupProps) {
|
}: GroupProps) {
|
||||||
const groupRole = type === 'radio' ? 'radiogroup' : undefined
|
const groupRole = type === 'radio' ? 'radiogroup' : undefined
|
||||||
const values = type === 'radio' ? providedValues.slice(0, 1) : providedValues
|
const values = type === 'radio' ? providedValues.slice(0, 1) : providedValues
|
||||||
const [maxReached, setMaxReached] = React.useState(false)
|
|
||||||
|
|
||||||
const setFieldValue = React.useCallback<
|
const setFieldValue = useCallback<
|
||||||
(props: {name: string; value: boolean}) => void
|
(props: {name: string; value: boolean}) => void
|
||||||
>(
|
>(
|
||||||
({name, value}) => {
|
({name, value}) => {
|
||||||
@@ -105,25 +115,13 @@ export function Group({
|
|||||||
[type, onChange, values],
|
[type, onChange, values],
|
||||||
)
|
)
|
||||||
|
|
||||||
React.useEffect(() => {
|
const maxReached = !!(
|
||||||
if (type === 'checkbox') {
|
type === 'checkbox' &&
|
||||||
if (
|
|
||||||
maxSelections &&
|
maxSelections &&
|
||||||
values.length >= maxSelections &&
|
values.length >= maxSelections
|
||||||
maxReached === false
|
)
|
||||||
) {
|
|
||||||
setMaxReached(true)
|
|
||||||
} else if (
|
|
||||||
maxSelections &&
|
|
||||||
values.length < maxSelections &&
|
|
||||||
maxReached === true
|
|
||||||
) {
|
|
||||||
setMaxReached(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [type, values.length, maxSelections, maxReached, setMaxReached])
|
|
||||||
|
|
||||||
const context = React.useMemo(
|
const context = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
values,
|
values,
|
||||||
type,
|
type,
|
||||||
@@ -170,7 +168,7 @@ export function Item({
|
|||||||
disabled: groupDisabled,
|
disabled: groupDisabled,
|
||||||
setFieldValue,
|
setFieldValue,
|
||||||
maxSelectionsReached,
|
maxSelectionsReached,
|
||||||
} = React.useContext(GroupContext)
|
} = useContext(GroupContext)
|
||||||
const {
|
const {
|
||||||
state: hovered,
|
state: hovered,
|
||||||
onIn: onHoverIn,
|
onIn: onHoverIn,
|
||||||
@@ -182,19 +180,21 @@ export function Item({
|
|||||||
onOut: onPressOut,
|
onOut: onPressOut,
|
||||||
} = useInteractionState()
|
} = useInteractionState()
|
||||||
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
|
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
|
||||||
|
const playHaptic = useHaptics()
|
||||||
|
|
||||||
const role = groupType === 'radio' ? 'radio' : type
|
const role = groupType === 'radio' ? 'radio' : type
|
||||||
const selected = selectedValues.includes(name) || !!value
|
const selected = selectedValues.includes(name) || !!value
|
||||||
const disabled =
|
const disabled =
|
||||||
groupDisabled || itemDisabled || (!selected && maxSelectionsReached)
|
groupDisabled || itemDisabled || (!selected && maxSelectionsReached)
|
||||||
|
|
||||||
const onPress = React.useCallback(() => {
|
const onPress = useCallback(() => {
|
||||||
|
playHaptic('Light')
|
||||||
const next = !selected
|
const next = !selected
|
||||||
setFieldValue({name, value: next})
|
setFieldValue({name, value: next})
|
||||||
onChange?.(next)
|
onChange?.(next)
|
||||||
}, [name, selected, onChange, setFieldValue])
|
}, [playHaptic, name, selected, onChange, setFieldValue])
|
||||||
|
|
||||||
const state = React.useMemo(
|
const state = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
name,
|
name,
|
||||||
selected,
|
selected,
|
||||||
@@ -250,8 +250,8 @@ export function LabelText({
|
|||||||
style={[
|
style={[
|
||||||
a.font_semi_bold,
|
a.font_semi_bold,
|
||||||
a.leading_tight,
|
a.leading_tight,
|
||||||
|
a.user_select_none,
|
||||||
{
|
{
|
||||||
userSelect: 'none',
|
|
||||||
color: disabled
|
color: disabled
|
||||||
? t.atoms.text_contrast_low.color
|
? t.atoms.text_contrast_low.color
|
||||||
: t.atoms.text_contrast_high.color,
|
: t.atoms.text_contrast_high.color,
|
||||||
@@ -287,21 +287,26 @@ export function createSharedToggleStyles({
|
|||||||
|
|
||||||
if (selected) {
|
if (selected) {
|
||||||
base.push({
|
base.push({
|
||||||
backgroundColor: t.palette.primary_25,
|
backgroundColor: t.palette.primary_500,
|
||||||
borderColor: t.palette.primary_500,
|
borderColor: t.palette.primary_500,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (hovered) {
|
if (hovered) {
|
||||||
baseHover.push({
|
baseHover.push({
|
||||||
backgroundColor: t.palette.primary_100,
|
backgroundColor: t.palette.primary_400,
|
||||||
borderColor: t.palette.primary_600,
|
borderColor: t.palette.primary_400,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
base.push({
|
||||||
|
backgroundColor: t.palette.contrast_25,
|
||||||
|
borderColor: t.palette.contrast_100,
|
||||||
|
})
|
||||||
|
|
||||||
if (hovered) {
|
if (hovered) {
|
||||||
baseHover.push({
|
baseHover.push({
|
||||||
backgroundColor: t.palette.contrast_50,
|
backgroundColor: t.palette.contrast_50,
|
||||||
borderColor: t.palette.contrast_500,
|
borderColor: t.palette.contrast_200,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -318,6 +323,20 @@ export function createSharedToggleStyles({
|
|||||||
borderColor: t.palette.negative_600,
|
borderColor: t.palette.negative_600,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (selected) {
|
||||||
|
base.push({
|
||||||
|
backgroundColor: t.palette.negative_500,
|
||||||
|
borderColor: t.palette.negative_500,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (hovered) {
|
||||||
|
baseHover.push({
|
||||||
|
backgroundColor: t.palette.negative_400,
|
||||||
|
borderColor: t.palette.negative_400,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
@@ -325,6 +344,13 @@ export function createSharedToggleStyles({
|
|||||||
backgroundColor: t.palette.contrast_100,
|
backgroundColor: t.palette.contrast_100,
|
||||||
borderColor: t.palette.contrast_400,
|
borderColor: t.palette.contrast_400,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (selected) {
|
||||||
|
base.push({
|
||||||
|
backgroundColor: t.palette.primary_100,
|
||||||
|
borderColor: t.palette.contrast_400,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -350,66 +376,125 @@ export function Checkbox() {
|
|||||||
style={[
|
style={[
|
||||||
a.justify_center,
|
a.justify_center,
|
||||||
a.align_center,
|
a.align_center,
|
||||||
a.rounded_xs,
|
|
||||||
t.atoms.border_contrast_high,
|
t.atoms.border_contrast_high,
|
||||||
|
a.transition_color,
|
||||||
{
|
{
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
height: 24,
|
height: 24,
|
||||||
width: 24,
|
width: 24,
|
||||||
|
borderRadius: 6,
|
||||||
},
|
},
|
||||||
baseStyles,
|
baseStyles,
|
||||||
hovered ? baseHoverStyles : {},
|
hovered ? baseHoverStyles : {},
|
||||||
]}>
|
]}>
|
||||||
{selected ? <Checkmark size="xs" fill={t.palette.primary_500} /> : null}
|
{selected && <Checkmark width={14} fill={t.palette.white} />}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Switch() {
|
export function Switch() {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {selected, hovered, focused, disabled, isInvalid} = useItemContext()
|
const {selected, hovered, disabled, isInvalid} = useItemContext()
|
||||||
const {baseStyles, baseHoverStyles, indicatorStyles} =
|
const {baseStyles, baseHoverStyles, indicatorStyles} = useMemo(() => {
|
||||||
createSharedToggleStyles({
|
const base: ViewStyle[] = []
|
||||||
theme: t,
|
const baseHover: ViewStyle[] = []
|
||||||
hovered,
|
const indicator: ViewStyle[] = []
|
||||||
focused,
|
|
||||||
selected,
|
if (selected) {
|
||||||
disabled,
|
base.push({
|
||||||
isInvalid,
|
backgroundColor: t.palette.primary_500,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (hovered) {
|
||||||
|
baseHover.push({
|
||||||
|
backgroundColor: t.palette.primary_400,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
base.push({
|
||||||
|
backgroundColor: t.palette.contrast_200,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (hovered) {
|
||||||
|
baseHover.push({
|
||||||
|
backgroundColor: t.palette.contrast_100,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInvalid) {
|
||||||
|
base.push({
|
||||||
|
backgroundColor: t.palette.negative_200,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (hovered) {
|
||||||
|
baseHover.push({
|
||||||
|
backgroundColor: t.palette.negative_100,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selected) {
|
||||||
|
base.push({
|
||||||
|
backgroundColor: t.palette.negative_500,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (hovered) {
|
||||||
|
baseHover.push({
|
||||||
|
backgroundColor: t.palette.negative_400,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (disabled) {
|
||||||
|
base.push({
|
||||||
|
backgroundColor: t.palette.contrast_50,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (selected) {
|
||||||
|
base.push({
|
||||||
|
backgroundColor: t.palette.primary_100,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
baseStyles: base,
|
||||||
|
baseHoverStyles: disabled ? [] : baseHover,
|
||||||
|
indicatorStyles: indicator,
|
||||||
|
}
|
||||||
|
}, [t, hovered, disabled, selected, isInvalid])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.relative,
|
a.relative,
|
||||||
a.rounded_full,
|
a.rounded_full,
|
||||||
t.atoms.bg,
|
t.atoms.bg,
|
||||||
t.atoms.border_contrast_high,
|
|
||||||
{
|
{
|
||||||
borderWidth: 1,
|
height: 28,
|
||||||
height: 24,
|
width: 48,
|
||||||
width: 36,
|
|
||||||
padding: 3,
|
padding: 3,
|
||||||
},
|
},
|
||||||
|
a.transition_color,
|
||||||
baseStyles,
|
baseStyles,
|
||||||
hovered ? baseHoverStyles : {},
|
hovered ? baseHoverStyles : {},
|
||||||
]}>
|
]}>
|
||||||
<Animated.View
|
<Animated.View
|
||||||
layout={LinearTransition.duration(100)}
|
layout={LinearTransition.duration(
|
||||||
|
platform({
|
||||||
|
web: 100,
|
||||||
|
default: 200,
|
||||||
|
}),
|
||||||
|
).easing(Easing.inOut(Easing.cubic))}
|
||||||
style={[
|
style={[
|
||||||
a.rounded_full,
|
a.rounded_full,
|
||||||
{
|
{
|
||||||
height: 16,
|
backgroundColor: t.palette.white,
|
||||||
width: 16,
|
height: 22,
|
||||||
},
|
width: 22,
|
||||||
selected
|
|
||||||
? {
|
|
||||||
backgroundColor: t.palette.primary_500,
|
|
||||||
alignSelf: 'flex-end',
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
backgroundColor: t.palette.contrast_400,
|
|
||||||
alignSelf: 'flex-start',
|
|
||||||
},
|
},
|
||||||
|
selected ? {alignSelf: 'flex-end'} : {alignSelf: 'flex-start'},
|
||||||
indicatorStyles,
|
indicatorStyles,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@@ -420,7 +505,7 @@ export function Switch() {
|
|||||||
export function Radio() {
|
export function Radio() {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {selected, hovered, focused, disabled, isInvalid} =
|
const {selected, hovered, focused, disabled, isInvalid} =
|
||||||
React.useContext(ItemContext)
|
useContext(ItemContext)
|
||||||
const {baseStyles, baseHoverStyles, indicatorStyles} =
|
const {baseStyles, baseHoverStyles, indicatorStyles} =
|
||||||
createSharedToggleStyles({
|
createSharedToggleStyles({
|
||||||
theme: t,
|
theme: t,
|
||||||
@@ -437,29 +522,27 @@ export function Radio() {
|
|||||||
a.align_center,
|
a.align_center,
|
||||||
a.rounded_full,
|
a.rounded_full,
|
||||||
t.atoms.border_contrast_high,
|
t.atoms.border_contrast_high,
|
||||||
|
a.transition_color,
|
||||||
{
|
{
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
height: 24,
|
height: 25,
|
||||||
width: 24,
|
width: 25,
|
||||||
|
margin: -1,
|
||||||
},
|
},
|
||||||
baseStyles,
|
baseStyles,
|
||||||
hovered ? baseHoverStyles : {},
|
hovered ? baseHoverStyles : {},
|
||||||
]}>
|
]}>
|
||||||
{selected ? (
|
{selected && (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.absolute,
|
a.absolute,
|
||||||
a.rounded_full,
|
a.rounded_full,
|
||||||
{height: 16, width: 16},
|
{height: 12, width: 12},
|
||||||
selected
|
{backgroundColor: t.palette.white},
|
||||||
? {
|
|
||||||
backgroundColor: t.palette.primary_500,
|
|
||||||
}
|
|
||||||
: {},
|
|
||||||
indicatorStyles,
|
indicatorStyles,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
) : null}
|
)}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -19,3 +19,10 @@ export const ChevronBottom_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
|||||||
export const ChevronTopBottom_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
export const ChevronTopBottom_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
path: 'M11.293 4.293a1 1 0 0 1 1.414 0l4 4a1 1 0 0 1-1.414 1.414L12 6.414 8.707 9.707a1 1 0 0 1-1.414-1.414l4-4Zm-4 10a1 1 0 0 1 1.414 0L12 17.586l3.293-3.293a1 1 0 0 1 1.414 1.414l-4 4a1 1 0 0 1-1.414 0l-4-4a1 1 0 0 1 0-1.414Z',
|
path: 'M11.293 4.293a1 1 0 0 1 1.414 0l4 4a1 1 0 0 1-1.414 1.414L12 6.414 8.707 9.707a1 1 0 0 1-1.414-1.414l4-4Zm-4 10a1 1 0 0 1 1.414 0L12 17.586l3.293-3.293a1 1 0 0 1 1.414 1.414l-4 4a1 1 0 0 1-1.414 0l-4-4a1 1 0 0 1 0-1.414Z',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NOTE: Use with size `2xs`
|
||||||
|
*/
|
||||||
|
export const TinyChevronBottom_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M10.928 18.882c.757.499 1.786.417 2.452-.25l9-9a1.953 1.953 0 0 0-2.76-2.76L12 14.493l-7.62-7.62a1.952 1.952 0 0 0-2.76 2.76l9 9 .308.25Z',
|
||||||
|
})
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export type Props = {
|
|||||||
} & Omit<SvgProps, 'style' | 'size'>
|
} & Omit<SvgProps, 'style' | 'size'>
|
||||||
|
|
||||||
export const sizes = {
|
export const sizes = {
|
||||||
|
'2xs': 8,
|
||||||
xs: 12,
|
xs: 12,
|
||||||
sm: 16,
|
sm: 16,
|
||||||
md: 20,
|
md: 20,
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export function VerificationsDialog({
|
|||||||
verificationState: FullVerificationState
|
verificationState: FullVerificationState
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Dialog.Outer control={control}>
|
<Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
<Inner
|
<Inner
|
||||||
control={control}
|
control={control}
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import {type I18n} from '@lingui/core'
|
import {type I18n} from '@lingui/core'
|
||||||
|
|
||||||
export function niceDate(i18n: I18n, date: number | string | Date) {
|
export function niceDate(
|
||||||
|
i18n: I18n,
|
||||||
|
date: number | string | Date,
|
||||||
|
dateStyle: 'short' | 'medium' | 'long' | 'full' = 'long',
|
||||||
|
) {
|
||||||
const d = new Date(date)
|
const d = new Date(date)
|
||||||
|
|
||||||
return i18n.date(d, {
|
return i18n.date(d, {
|
||||||
dateStyle: 'long',
|
dateStyle,
|
||||||
timeStyle: 'short',
|
timeStyle: 'short',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -570,7 +570,7 @@ function ExpandedPostDetails({
|
|||||||
<BackdatedPostIndicator post={post} />
|
<BackdatedPostIndicator post={post} />
|
||||||
<View style={[a.flex_row, a.align_center, a.flex_wrap, a.gap_sm]}>
|
<View style={[a.flex_row, a.align_center, a.flex_wrap, a.gap_sm]}>
|
||||||
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
||||||
{niceDate(i18n, post.indexedAt)}
|
{niceDate(i18n, post.indexedAt, 'medium')}
|
||||||
</Text>
|
</Text>
|
||||||
{isRootPost && (
|
{isRootPost && (
|
||||||
<WhoCanReply post={post} isThreadAuthor={isThreadAuthor} />
|
<WhoCanReply post={post} isThreadAuthor={isThreadAuthor} />
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ export function ItemIcon({
|
|||||||
* also so that we can calculate transforms.
|
* also so that we can calculate transforms.
|
||||||
*/
|
*/
|
||||||
const iconSize = {
|
const iconSize = {
|
||||||
|
'2xs': 8,
|
||||||
xs: 12,
|
xs: 12,
|
||||||
sm: 16,
|
sm: 16,
|
||||||
md: 20,
|
md: 20,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {createContext, useContext, useMemo, useRef, useState} from 'react'
|
import {createContext, useContext, useMemo, useRef, useState} from 'react'
|
||||||
import {View} from 'react-native'
|
import {type StyleProp, View, type ViewStyle} from 'react-native'
|
||||||
import {
|
import {
|
||||||
Gesture,
|
Gesture,
|
||||||
GestureDetector,
|
GestureDetector,
|
||||||
@@ -29,8 +29,10 @@ Context.displayName = 'GlobalGestureEventsContext'
|
|||||||
|
|
||||||
export function GlobalGestureEventsProvider({
|
export function GlobalGestureEventsProvider({
|
||||||
children,
|
children,
|
||||||
|
style,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
|
style?: StyleProp<ViewStyle>
|
||||||
}) {
|
}) {
|
||||||
const refCount = useRef(0)
|
const refCount = useRef(0)
|
||||||
const events = useMemo(() => new EventEmitter<GlobalGestureEvents>(), [])
|
const events = useMemo(() => new EventEmitter<GlobalGestureEvents>(), [])
|
||||||
@@ -73,7 +75,9 @@ export function GlobalGestureEventsProvider({
|
|||||||
return (
|
return (
|
||||||
<Context.Provider value={ctx}>
|
<Context.Provider value={ctx}>
|
||||||
<GestureDetector gesture={gesture}>
|
<GestureDetector gesture={gesture}>
|
||||||
<View collapsable={false}>{children}</View>
|
<View collapsable={false} style={style}>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
</GestureDetector>
|
</GestureDetector>
|
||||||
</Context.Provider>
|
</Context.Provider>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,7 +4,13 @@ import {useMutation, useQueryClient} from '@tanstack/react-query'
|
|||||||
import {preferencesQueryKey} from '#/state/queries/preferences'
|
import {preferencesQueryKey} from '#/state/queries/preferences'
|
||||||
import {useAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
|
|
||||||
export function usePostInteractionSettingsMutation() {
|
export function usePostInteractionSettingsMutation({
|
||||||
|
onError,
|
||||||
|
onSettled,
|
||||||
|
}: {
|
||||||
|
onError?: (error: Error) => void
|
||||||
|
onSettled?: () => void
|
||||||
|
} = {}) {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
return useMutation({
|
return useMutation({
|
||||||
@@ -16,5 +22,7 @@ export function usePostInteractionSettingsMutation() {
|
|||||||
queryKey: preferencesQueryKey,
|
queryKey: preferencesQueryKey,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
onError,
|
||||||
|
onSettled,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import {device, useStorage} from '#/storage'
|
||||||
|
|
||||||
|
export function useThreadgateNudged() {
|
||||||
|
const [threadgateNudged = false, setThreadgateNudged] = useStorage(device, [
|
||||||
|
'threadgateNudged',
|
||||||
|
])
|
||||||
|
|
||||||
|
return [threadgateNudged, setThreadgateNudged] as const
|
||||||
|
}
|
||||||
@@ -37,6 +37,7 @@ export type Device = {
|
|||||||
devMode: boolean
|
devMode: boolean
|
||||||
demoMode: boolean
|
demoMode: boolean
|
||||||
activitySubscriptionsNudged?: boolean
|
activitySubscriptionsNudged?: boolean
|
||||||
|
threadgateNudged?: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Policy update overlays. New IDs are required for each new announcement.
|
* Policy update overlays. New IDs are required for each new announcement.
|
||||||
|
|||||||
@@ -10,11 +10,12 @@ import {
|
|||||||
type SelfLabel,
|
type SelfLabel,
|
||||||
} from '#/lib/moderation'
|
} from '#/lib/moderation'
|
||||||
import {isWeb} from '#/platform/detection'
|
import {isWeb} from '#/platform/detection'
|
||||||
import {atoms as a, native, useTheme, web} from '#/alf'
|
import {atoms as a, useTheme, web} from '#/alf'
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import * as Toggle from '#/components/forms/Toggle'
|
import * as Toggle from '#/components/forms/Toggle'
|
||||||
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||||
|
import {TinyChevronBottom_Stroke2_Corner0_Rounded as TinyChevronIcon} from '#/components/icons/Chevron'
|
||||||
import {Shield_Stroke2_Corner0_Rounded} from '#/components/icons/Shield'
|
import {Shield_Stroke2_Corner0_Rounded} from '#/components/icons/Shield'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
@@ -49,7 +50,6 @@ export function LabelsBtn({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
variant="solid"
|
|
||||||
color="secondary"
|
color="secondary"
|
||||||
size="small"
|
size="small"
|
||||||
testID="labelsBtn"
|
testID="labelsBtn"
|
||||||
@@ -60,13 +60,7 @@ export function LabelsBtn({
|
|||||||
label={_(msg`Content warnings`)}
|
label={_(msg`Content warnings`)}
|
||||||
accessibilityHint={_(
|
accessibilityHint={_(
|
||||||
msg`Opens a dialog to add a content warning to your post`,
|
msg`Opens a dialog to add a content warning to your post`,
|
||||||
)}
|
)}>
|
||||||
style={[
|
|
||||||
native({
|
|
||||||
paddingHorizontal: 8,
|
|
||||||
paddingVertical: 6,
|
|
||||||
}),
|
|
||||||
]}>
|
|
||||||
<ButtonIcon icon={hasLabel ? Check : Shield_Stroke2_Corner0_Rounded} />
|
<ButtonIcon icon={hasLabel ? Check : Shield_Stroke2_Corner0_Rounded} />
|
||||||
<ButtonText numberOfLines={1}>
|
<ButtonText numberOfLines={1}>
|
||||||
{labels.length > 0 ? (
|
{labels.length > 0 ? (
|
||||||
@@ -75,6 +69,7 @@ export function LabelsBtn({
|
|||||||
<Trans>Labels</Trans>
|
<Trans>Labels</Trans>
|
||||||
)}
|
)}
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
|
<ButtonIcon icon={TinyChevronIcon} size="2xs" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
|
<Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
|
||||||
|
|||||||
@@ -1,17 +1,31 @@
|
|||||||
|
import {useEffect, useMemo, useState} from 'react'
|
||||||
import {Keyboard, type StyleProp, type ViewStyle} from 'react-native'
|
import {Keyboard, type StyleProp, type ViewStyle} from 'react-native'
|
||||||
import {type AnimatedStyle} from 'react-native-reanimated'
|
import {type AnimatedStyle} from 'react-native-reanimated'
|
||||||
import {type AppBskyFeedPostgate} from '@atproto/api'
|
import {type AppBskyFeedPostgate} from '@atproto/api'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
import deepEqual from 'lodash.isequal'
|
||||||
|
|
||||||
|
import {isNetworkError} from '#/lib/strings/errors'
|
||||||
|
import {logger} from '#/logger'
|
||||||
import {isNative} from '#/platform/detection'
|
import {isNative} from '#/platform/detection'
|
||||||
import {type ThreadgateAllowUISetting} from '#/state/queries/threadgate'
|
import {usePostInteractionSettingsMutation} from '#/state/queries/post-interaction-settings'
|
||||||
import {native} from '#/alf'
|
import {createPostgateRecord} from '#/state/queries/postgate/util'
|
||||||
|
import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||||
|
import {
|
||||||
|
type ThreadgateAllowUISetting,
|
||||||
|
threadgateAllowUISettingToAllowRecordValue,
|
||||||
|
threadgateRecordToAllowUISetting,
|
||||||
|
} from '#/state/queries/threadgate'
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import {PostInteractionSettingsControlledDialog} from '#/components/dialogs/PostInteractionSettingsDialog'
|
import {PostInteractionSettingsControlledDialog} from '#/components/dialogs/PostInteractionSettingsDialog'
|
||||||
import {Earth_Stroke2_Corner0_Rounded as Earth} from '#/components/icons/Globe'
|
import {TinyChevronBottom_Stroke2_Corner0_Rounded as TinyChevronIcon} from '#/components/icons/Chevron'
|
||||||
import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
|
import {Earth_Stroke2_Corner0_Rounded as EarthIcon} from '#/components/icons/Globe'
|
||||||
|
import {Group3_Stroke2_Corner0_Rounded as GroupIcon} from '#/components/icons/Group'
|
||||||
|
import * as Tooltip from '#/components/Tooltip'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
import {useThreadgateNudged} from '#/storage/hooks/threadgate-nudged'
|
||||||
|
|
||||||
export function ThreadgateBtn({
|
export function ThreadgateBtn({
|
||||||
postgate,
|
postgate,
|
||||||
@@ -29,15 +43,82 @@ export function ThreadgateBtn({
|
|||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const control = Dialog.useDialogControl()
|
const control = Dialog.useDialogControl()
|
||||||
|
const [threadgateNudged, setThreadgateNudged] = useThreadgateNudged()
|
||||||
|
const [showTooltip, setShowTooltip] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!threadgateNudged) {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
setShowTooltip(true)
|
||||||
|
}, 1000)
|
||||||
|
return () => clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
}, [threadgateNudged])
|
||||||
|
|
||||||
|
const onDismissTooltip = (visible: boolean) => {
|
||||||
|
if (visible) return
|
||||||
|
setThreadgateNudged(true)
|
||||||
|
setShowTooltip(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const {data: preferences} = usePreferencesQuery()
|
||||||
|
const [persist, setPersist] = useState(false)
|
||||||
|
|
||||||
const onPress = () => {
|
const onPress = () => {
|
||||||
if (isNative && Keyboard.isVisible()) {
|
if (isNative && Keyboard.isVisible()) {
|
||||||
Keyboard.dismiss()
|
Keyboard.dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setShowTooltip(false)
|
||||||
|
setThreadgateNudged(true)
|
||||||
|
|
||||||
control.open()
|
control.open()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const prefThreadgateAllowUISettings = threadgateRecordToAllowUISetting({
|
||||||
|
$type: 'app.bsky.feed.threadgate',
|
||||||
|
post: '',
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
allow: preferences?.postInteractionSettings.threadgateAllowRules,
|
||||||
|
})
|
||||||
|
const prefPostgate = createPostgateRecord({
|
||||||
|
post: '',
|
||||||
|
embeddingRules:
|
||||||
|
preferences?.postInteractionSettings?.postgateEmbeddingRules || [],
|
||||||
|
})
|
||||||
|
|
||||||
|
const isDirty = useMemo(() => {
|
||||||
|
const everybody = [{type: 'everybody'}]
|
||||||
|
return (
|
||||||
|
!deepEqual(
|
||||||
|
threadgateAllowUISettings,
|
||||||
|
prefThreadgateAllowUISettings ?? everybody,
|
||||||
|
) ||
|
||||||
|
!deepEqual(postgate.embeddingRules, prefPostgate?.embeddingRules ?? [])
|
||||||
|
)
|
||||||
|
}, [
|
||||||
|
prefThreadgateAllowUISettings,
|
||||||
|
prefPostgate,
|
||||||
|
threadgateAllowUISettings,
|
||||||
|
postgate,
|
||||||
|
])
|
||||||
|
|
||||||
|
const {mutate: persistChanges, isPending: isSaving} =
|
||||||
|
usePostInteractionSettingsMutation({
|
||||||
|
onError: err => {
|
||||||
|
if (!isNetworkError(err)) {
|
||||||
|
logger.error('Failed to persist threadgate settings', {
|
||||||
|
safeMessage: err,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSettled: () => {
|
||||||
|
control.close(() => {
|
||||||
|
setPersist(false)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const anyoneCanReply =
|
const anyoneCanReply =
|
||||||
threadgateAllowUISettings.length === 1 &&
|
threadgateAllowUISettings.length === 1 &&
|
||||||
threadgateAllowUISettings[0].type === 'everybody'
|
threadgateAllowUISettings[0].type === 'everybody'
|
||||||
@@ -50,34 +131,54 @@ export function ThreadgateBtn({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<Tooltip.Outer
|
||||||
|
visible={showTooltip}
|
||||||
|
onVisibleChange={onDismissTooltip}
|
||||||
|
position="top">
|
||||||
|
<Tooltip.Target>
|
||||||
<Button
|
<Button
|
||||||
variant="solid"
|
color={showTooltip ? 'primary_subtle' : 'secondary'}
|
||||||
color="secondary"
|
|
||||||
size="small"
|
size="small"
|
||||||
testID="openReplyGateButton"
|
testID="openReplyGateButton"
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
label={label}
|
label={label}
|
||||||
accessibilityHint={_(
|
accessibilityHint={_(
|
||||||
msg`Opens a dialog to choose who can reply to this thread`,
|
msg`Opens a dialog to choose who can interact with this post`,
|
||||||
)}
|
)}>
|
||||||
style={[
|
<ButtonIcon icon={anyoneCanInteract ? EarthIcon : GroupIcon} />
|
||||||
native({
|
|
||||||
paddingHorizontal: 8,
|
|
||||||
paddingVertical: 6,
|
|
||||||
}),
|
|
||||||
]}>
|
|
||||||
<ButtonIcon icon={anyoneCanInteract ? Earth : Group} />
|
|
||||||
<ButtonText numberOfLines={1}>{label}</ButtonText>
|
<ButtonText numberOfLines={1}>{label}</ButtonText>
|
||||||
|
<ButtonIcon icon={TinyChevronIcon} size="2xs" />
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip.Target>
|
||||||
|
<Tooltip.TextBubble>
|
||||||
|
<Text>
|
||||||
|
<Trans>Psst! You can edit who can interact with this post.</Trans>
|
||||||
|
</Text>
|
||||||
|
</Tooltip.TextBubble>
|
||||||
|
</Tooltip.Outer>
|
||||||
|
|
||||||
<PostInteractionSettingsControlledDialog
|
<PostInteractionSettingsControlledDialog
|
||||||
control={control}
|
control={control}
|
||||||
onSave={() => {
|
onSave={() => {
|
||||||
|
if (persist) {
|
||||||
|
persistChanges({
|
||||||
|
threadgateAllowRules: threadgateAllowUISettingToAllowRecordValue(
|
||||||
|
threadgateAllowUISettings,
|
||||||
|
),
|
||||||
|
postgateEmbeddingRules: postgate.embeddingRules ?? [],
|
||||||
|
})
|
||||||
|
} else {
|
||||||
control.close()
|
control.close()
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
|
isSaving={isSaving}
|
||||||
postgate={postgate}
|
postgate={postgate}
|
||||||
onChangePostgate={onChangePostgate}
|
onChangePostgate={onChangePostgate}
|
||||||
threadgateAllowUISettings={threadgateAllowUISettings}
|
threadgateAllowUISettings={threadgateAllowUISettings}
|
||||||
onChangeThreadgateAllowUISettings={onChangeThreadgateAllowUISettings}
|
onChangeThreadgateAllowUISettings={onChangeThreadgateAllowUISettings}
|
||||||
|
isDirty={isDirty}
|
||||||
|
persist={persist}
|
||||||
|
onChangePersist={setPersist}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -155,6 +155,15 @@ export function Forms() {
|
|||||||
</View>
|
</View>
|
||||||
</Toggle.Group>
|
</Toggle.Group>
|
||||||
|
|
||||||
|
<Toggle.Item name="d" disabled value label="Click me">
|
||||||
|
<Toggle.Switch />
|
||||||
|
<Toggle.LabelText>Click me</Toggle.LabelText>
|
||||||
|
</Toggle.Item>
|
||||||
|
<Toggle.Item name="d" disabled value isInvalid label="Click me">
|
||||||
|
<Toggle.Switch />
|
||||||
|
<Toggle.LabelText>Click me</Toggle.LabelText>
|
||||||
|
</Toggle.Item>
|
||||||
|
|
||||||
<Toggle.Group
|
<Toggle.Group
|
||||||
label="Toggle"
|
label="Toggle"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import {Modal, View} from 'react-native'
|
|||||||
|
|
||||||
import {useDialogStateControlContext} from '#/state/dialogs'
|
import {useDialogStateControlContext} from '#/state/dialogs'
|
||||||
import {useComposerState} from '#/state/shell/composer'
|
import {useComposerState} from '#/state/shell/composer'
|
||||||
|
import {ComposePost, useComposerCancelRef} from '#/view/com/composer/Composer'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {ComposePost, useComposerCancelRef} from '../com/composer/Composer'
|
import {SheetCompatProvider as TooltipSheetCompatProvider} from '#/components/Tooltip'
|
||||||
|
|
||||||
export function Composer({}: {winHeight: number}) {
|
export function Composer({}: {winHeight: number}) {
|
||||||
const {setFullyExpandedCount} = useDialogStateControlContext()
|
const {setFullyExpandedCount} = useDialogStateControlContext()
|
||||||
@@ -33,6 +34,7 @@ export function Composer({}: {winHeight: number}) {
|
|||||||
animationType="slide"
|
animationType="slide"
|
||||||
onRequestClose={() => ref.current?.onPressCancel()}>
|
onRequestClose={() => ref.current?.onPressCancel()}>
|
||||||
<View style={[t.atoms.bg, a.flex_1]}>
|
<View style={[t.atoms.bg, a.flex_1]}>
|
||||||
|
<TooltipSheetCompatProvider>
|
||||||
<ComposePost
|
<ComposePost
|
||||||
cancelRef={ref}
|
cancelRef={ref}
|
||||||
replyTo={state?.replyTo}
|
replyTo={state?.replyTo}
|
||||||
@@ -44,6 +46,7 @@ export function Composer({}: {winHeight: number}) {
|
|||||||
imageUris={state?.imageUris}
|
imageUris={state?.imageUris}
|
||||||
videoUri={state?.videoUri}
|
videoUri={state?.videoUri}
|
||||||
/>
|
/>
|
||||||
|
</TooltipSheetCompatProvider>
|
||||||
</View>
|
</View>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user