Update hashtag menu to use Menu, convert to native link for additional a11y and click handling (#7529)
* Make tag a normal link on web * Replace old TagMenu with new RichTextTag component, expand and improve click utils * Clarify intents * Ensure we're passing down hint * ope * DRY
This commit is contained in:
+108
-18
@@ -15,7 +15,6 @@ import {
|
|||||||
linkRequiresWarning,
|
linkRequiresWarning,
|
||||||
} from '#/lib/strings/url-helpers'
|
} from '#/lib/strings/url-helpers'
|
||||||
import {isNative, isWeb} from '#/platform/detection'
|
import {isNative, isWeb} from '#/platform/detection'
|
||||||
import {shouldClickOpenNewTab} from '#/platform/urls'
|
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
import {atoms as a, flatten, TextStyleProp, useTheme, web} from '#/alf'
|
import {atoms as a, flatten, TextStyleProp, useTheme, web} from '#/alf'
|
||||||
import {Button, ButtonProps} from '#/components/Button'
|
import {Button, ButtonProps} from '#/components/Button'
|
||||||
@@ -55,6 +54,12 @@ type BaseLinkProps = Pick<
|
|||||||
*/
|
*/
|
||||||
onPress?: (e: GestureResponderEvent) => void | false
|
onPress?: (e: GestureResponderEvent) => void | false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for when the link is long pressed (on native). Prevent default
|
||||||
|
* and return `false` to exit early and prevent default long press hander.
|
||||||
|
*/
|
||||||
|
onLongPress?: (e: GestureResponderEvent) => void | false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Web-only attribute. Sets `download` attr on web.
|
* Web-only attribute. Sets `download` attr on web.
|
||||||
*/
|
*/
|
||||||
@@ -72,6 +77,7 @@ export function useLink({
|
|||||||
action = 'push',
|
action = 'push',
|
||||||
disableMismatchWarning,
|
disableMismatchWarning,
|
||||||
onPress: outerOnPress,
|
onPress: outerOnPress,
|
||||||
|
onLongPress: outerOnLongPress,
|
||||||
shareOnLongPress,
|
shareOnLongPress,
|
||||||
}: BaseLinkProps & {
|
}: BaseLinkProps & {
|
||||||
displayText: string
|
displayText: string
|
||||||
@@ -175,8 +181,14 @@ export function useLink({
|
|||||||
}
|
}
|
||||||
}, [disableMismatchWarning, displayText, href, isExternal, openModal])
|
}, [disableMismatchWarning, displayText, href, isExternal, openModal])
|
||||||
|
|
||||||
const onLongPress =
|
const onLongPress = React.useCallback(
|
||||||
isNative && isExternal && shareOnLongPress ? handleLongPress : undefined
|
(e: GestureResponderEvent) => {
|
||||||
|
const exitEarlyIfFalse = outerOnLongPress?.(e)
|
||||||
|
if (exitEarlyIfFalse === false) return
|
||||||
|
return isNative && shareOnLongPress ? handleLongPress() : undefined
|
||||||
|
},
|
||||||
|
[outerOnLongPress, handleLongPress, shareOnLongPress],
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isExternal,
|
isExternal,
|
||||||
@@ -202,14 +214,16 @@ export function Link({
|
|||||||
to,
|
to,
|
||||||
action = 'push',
|
action = 'push',
|
||||||
onPress: outerOnPress,
|
onPress: outerOnPress,
|
||||||
|
onLongPress: outerOnLongPress,
|
||||||
download,
|
download,
|
||||||
...rest
|
...rest
|
||||||
}: LinkProps) {
|
}: LinkProps) {
|
||||||
const {href, isExternal, onPress} = useLink({
|
const {href, isExternal, onPress, onLongPress} = useLink({
|
||||||
to,
|
to,
|
||||||
displayText: typeof children === 'string' ? children : '',
|
displayText: typeof children === 'string' ? children : '',
|
||||||
action,
|
action,
|
||||||
onPress: outerOnPress,
|
onPress: outerOnPress,
|
||||||
|
onLongPress: outerOnLongPress,
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -220,6 +234,7 @@ export function Link({
|
|||||||
accessibilityRole="link"
|
accessibilityRole="link"
|
||||||
href={href}
|
href={href}
|
||||||
onPress={download ? undefined : onPress}
|
onPress={download ? undefined : onPress}
|
||||||
|
onLongPress={onLongPress}
|
||||||
{...web({
|
{...web({
|
||||||
hrefAttrs: {
|
hrefAttrs: {
|
||||||
target: download ? undefined : isExternal ? 'blank' : undefined,
|
target: download ? undefined : isExternal ? 'blank' : undefined,
|
||||||
@@ -241,7 +256,7 @@ export type InlineLinkProps = React.PropsWithChildren<
|
|||||||
TextStyleProp &
|
TextStyleProp &
|
||||||
Pick<TextProps, 'selectable' | 'numberOfLines'>
|
Pick<TextProps, 'selectable' | 'numberOfLines'>
|
||||||
> &
|
> &
|
||||||
Pick<ButtonProps, 'label'> & {
|
Pick<ButtonProps, 'label' | 'accessibilityHint'> & {
|
||||||
disableUnderline?: boolean
|
disableUnderline?: boolean
|
||||||
title?: TextProps['title']
|
title?: TextProps['title']
|
||||||
}
|
}
|
||||||
@@ -253,6 +268,7 @@ export function InlineLinkText({
|
|||||||
disableMismatchWarning,
|
disableMismatchWarning,
|
||||||
style,
|
style,
|
||||||
onPress: outerOnPress,
|
onPress: outerOnPress,
|
||||||
|
onLongPress: outerOnLongPress,
|
||||||
download,
|
download,
|
||||||
selectable,
|
selectable,
|
||||||
label,
|
label,
|
||||||
@@ -268,6 +284,7 @@ export function InlineLinkText({
|
|||||||
action,
|
action,
|
||||||
disableMismatchWarning,
|
disableMismatchWarning,
|
||||||
onPress: outerOnPress,
|
onPress: outerOnPress,
|
||||||
|
onLongPress: outerOnLongPress,
|
||||||
shareOnLongPress,
|
shareOnLongPress,
|
||||||
})
|
})
|
||||||
const {
|
const {
|
||||||
@@ -319,6 +336,21 @@ export function InlineLinkText({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function WebOnlyInlineLinkText({
|
||||||
|
children,
|
||||||
|
to,
|
||||||
|
onPress,
|
||||||
|
...props
|
||||||
|
}: Omit<InlineLinkProps, 'onLongPress'>) {
|
||||||
|
return isWeb ? (
|
||||||
|
<InlineLinkText {...props} to={to} onPress={onPress}>
|
||||||
|
{children}
|
||||||
|
</InlineLinkText>
|
||||||
|
) : (
|
||||||
|
<Text {...props}>{children}</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility to create a static `onPress` handler for a `Link` that would otherwise link to a URI
|
* Utility to create a static `onPress` handler for a `Link` that would otherwise link to a URI
|
||||||
*
|
*
|
||||||
@@ -327,7 +359,10 @@ export function InlineLinkText({
|
|||||||
*/
|
*/
|
||||||
export function createStaticClick(
|
export function createStaticClick(
|
||||||
onPressHandler: Exclude<BaseLinkProps['onPress'], undefined>,
|
onPressHandler: Exclude<BaseLinkProps['onPress'], undefined>,
|
||||||
): Pick<BaseLinkProps, 'to' | 'onPress'> {
|
): {
|
||||||
|
to: BaseLinkProps['to']
|
||||||
|
onPress: Exclude<BaseLinkProps['onPress'], undefined>
|
||||||
|
} {
|
||||||
return {
|
return {
|
||||||
to: '#',
|
to: '#',
|
||||||
onPress(e: GestureResponderEvent) {
|
onPress(e: GestureResponderEvent) {
|
||||||
@@ -338,17 +373,72 @@ export function createStaticClick(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WebOnlyInlineLinkText({
|
/**
|
||||||
children,
|
* Utility to create a static `onPress` handler for a `Link`, but only if the
|
||||||
to,
|
* click was not modified in some way e.g. `Cmd` or a middle click.
|
||||||
onPress,
|
*
|
||||||
...props
|
* On native, this behaves the same as `createStaticClick` because there are no
|
||||||
}: InlineLinkProps) {
|
* options to "modify" the click in this sense.
|
||||||
return isWeb ? (
|
*
|
||||||
<InlineLinkText {...props} to={to} onPress={onPress}>
|
* Example:
|
||||||
{children}
|
* `<Link {...createStaticClick(e => {...})} />`
|
||||||
</InlineLinkText>
|
*/
|
||||||
) : (
|
export function createStaticClickIfUnmodified(
|
||||||
<Text {...props}>{children}</Text>
|
onPressHandler: Exclude<BaseLinkProps['onPress'], undefined>,
|
||||||
|
): {onPress: Exclude<BaseLinkProps['onPress'], undefined>} {
|
||||||
|
return {
|
||||||
|
onPress(e: GestureResponderEvent) {
|
||||||
|
if (!isWeb || !isModifiedClickEvent(e)) {
|
||||||
|
e.preventDefault()
|
||||||
|
onPressHandler(e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the click event has a meta key pressed, indicating the user
|
||||||
|
* intends to deviate from default behavior.
|
||||||
|
*/
|
||||||
|
export function isClickEventWithMetaKey(e: GestureResponderEvent) {
|
||||||
|
if (!isWeb) return false
|
||||||
|
const event = e as unknown as MouseEvent
|
||||||
|
return event.metaKey || event.altKey || event.ctrlKey || event.shiftKey
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the web click target is anything other than `_self`
|
||||||
|
*/
|
||||||
|
export function isClickTargetExternal(e: GestureResponderEvent) {
|
||||||
|
if (!isWeb) return false
|
||||||
|
const event = e as unknown as MouseEvent
|
||||||
|
const el = event.currentTarget as HTMLAnchorElement
|
||||||
|
return el && el.target && el.target !== '_self'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if a click event has been modified in some way from its default
|
||||||
|
* behavior, e.g. `Cmd` or a middle click.
|
||||||
|
* {@link https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button}
|
||||||
|
*/
|
||||||
|
export function isModifiedClickEvent(e: GestureResponderEvent): boolean {
|
||||||
|
if (!isWeb) return false
|
||||||
|
const event = e as unknown as MouseEvent
|
||||||
|
const isPrimaryButton = event.button === 0
|
||||||
|
return (
|
||||||
|
isClickEventWithMetaKey(e) || isClickTargetExternal(e) || !isPrimaryButton
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if a click event has been modified in a way that should indiciate
|
||||||
|
* that the user intends to open a new tab.
|
||||||
|
* {@link https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button}
|
||||||
|
*/
|
||||||
|
export function shouldClickOpenNewTab(e: GestureResponderEvent) {
|
||||||
|
if (!isWeb) return false
|
||||||
|
const event = e as unknown as MouseEvent
|
||||||
|
const isMiddleClick = isWeb && event.button === 1
|
||||||
|
return isClickEventWithMetaKey(e) || isClickTargetExternal(e) || isMiddleClick
|
||||||
|
}
|
||||||
|
|||||||
@@ -47,7 +47,12 @@ export function Root({
|
|||||||
return <Context.Provider value={context}>{children}</Context.Provider>
|
return <Context.Provider value={context}>{children}</Context.Provider>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Trigger({children, label, role = 'button'}: TriggerProps) {
|
export function Trigger({
|
||||||
|
children,
|
||||||
|
label,
|
||||||
|
role = 'button',
|
||||||
|
hint,
|
||||||
|
}: TriggerProps) {
|
||||||
const context = useMenuContext()
|
const context = useMenuContext()
|
||||||
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
|
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
|
||||||
const {
|
const {
|
||||||
@@ -65,11 +70,13 @@ export function Trigger({children, label, role = 'button'}: TriggerProps) {
|
|||||||
pressed,
|
pressed,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
ref: null,
|
||||||
onPress: context.control.open,
|
onPress: context.control.open,
|
||||||
onFocus,
|
onFocus,
|
||||||
onBlur,
|
onBlur,
|
||||||
onPressIn,
|
onPressIn,
|
||||||
onPressOut,
|
onPressOut,
|
||||||
|
accessibilityHint: hint,
|
||||||
accessibilityLabel: label,
|
accessibilityLabel: label,
|
||||||
accessibilityRole: role,
|
accessibilityRole: role,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -110,7 +110,12 @@ const RadixTriggerPassThrough = React.forwardRef(
|
|||||||
)
|
)
|
||||||
RadixTriggerPassThrough.displayName = 'RadixTriggerPassThrough'
|
RadixTriggerPassThrough.displayName = 'RadixTriggerPassThrough'
|
||||||
|
|
||||||
export function Trigger({children, label, role = 'button'}: TriggerProps) {
|
export function Trigger({
|
||||||
|
children,
|
||||||
|
label,
|
||||||
|
role = 'button',
|
||||||
|
hint,
|
||||||
|
}: TriggerProps) {
|
||||||
const {control} = useMenuContext()
|
const {control} = useMenuContext()
|
||||||
const {
|
const {
|
||||||
state: hovered,
|
state: hovered,
|
||||||
@@ -153,6 +158,7 @@ export function Trigger({children, label, role = 'button'}: TriggerProps) {
|
|||||||
onBlur: onBlur,
|
onBlur: onBlur,
|
||||||
onMouseEnter,
|
onMouseEnter,
|
||||||
onMouseLeave,
|
onMouseLeave,
|
||||||
|
accessibilityHint: hint,
|
||||||
accessibilityLabel: label,
|
accessibilityLabel: label,
|
||||||
accessibilityRole: role,
|
accessibilityRole: role,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export type ItemContextType = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type RadixPassThroughTriggerProps = {
|
export type RadixPassThroughTriggerProps = {
|
||||||
|
ref: React.RefObject<any>
|
||||||
id: string
|
id: string
|
||||||
type: 'button'
|
type: 'button'
|
||||||
disabled: boolean
|
disabled: boolean
|
||||||
@@ -37,6 +38,7 @@ export type RadixPassThroughTriggerProps = {
|
|||||||
export type TriggerProps = {
|
export type TriggerProps = {
|
||||||
children(props: TriggerChildProps): React.ReactNode
|
children(props: TriggerChildProps): React.ReactNode
|
||||||
label: string
|
label: string
|
||||||
|
hint?: string
|
||||||
role?: AccessibilityRole
|
role?: AccessibilityRole
|
||||||
}
|
}
|
||||||
export type TriggerChildProps =
|
export type TriggerChildProps =
|
||||||
@@ -59,11 +61,13 @@ export type TriggerChildProps =
|
|||||||
* object is empty.
|
* object is empty.
|
||||||
*/
|
*/
|
||||||
props: {
|
props: {
|
||||||
|
ref: null
|
||||||
onPress: () => void
|
onPress: () => void
|
||||||
onFocus: () => void
|
onFocus: () => void
|
||||||
onBlur: () => void
|
onBlur: () => void
|
||||||
onPressIn: () => void
|
onPressIn: () => void
|
||||||
onPressOut: () => void
|
onPressOut: () => void
|
||||||
|
accessibilityHint?: string
|
||||||
accessibilityLabel: string
|
accessibilityLabel: string
|
||||||
accessibilityRole: AccessibilityRole
|
accessibilityRole: AccessibilityRole
|
||||||
}
|
}
|
||||||
@@ -85,6 +89,7 @@ export type TriggerChildProps =
|
|||||||
onBlur: () => void
|
onBlur: () => void
|
||||||
onMouseEnter: () => void
|
onMouseEnter: () => void
|
||||||
onMouseLeave: () => void
|
onMouseLeave: () => void
|
||||||
|
accessibilityHint?: string
|
||||||
accessibilityLabel: string
|
accessibilityLabel: string
|
||||||
accessibilityRole: AccessibilityRole
|
accessibilityRole: AccessibilityRole
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,13 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {TextStyle} from 'react-native'
|
import {TextStyle} from 'react-native'
|
||||||
import {AppBskyRichtextFacet, RichText as RichTextAPI} from '@atproto/api'
|
import {AppBskyRichtextFacet, RichText as RichTextAPI} from '@atproto/api'
|
||||||
import {msg} from '@lingui/macro'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
import {useNavigation} from '@react-navigation/native'
|
|
||||||
|
|
||||||
import {NavigationProp} from '#/lib/routes/types'
|
|
||||||
import {toShortUrl} from '#/lib/strings/url-helpers'
|
import {toShortUrl} from '#/lib/strings/url-helpers'
|
||||||
import {isNative} from '#/platform/detection'
|
import {atoms as a, flatten, TextStyleProp} from '#/alf'
|
||||||
import {atoms as a, flatten, native, TextStyleProp, useTheme, web} from '#/alf'
|
|
||||||
import {isOnlyEmoji} from '#/alf/typography'
|
import {isOnlyEmoji} from '#/alf/typography'
|
||||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
|
||||||
import {InlineLinkText, LinkProps} from '#/components/Link'
|
import {InlineLinkText, LinkProps} from '#/components/Link'
|
||||||
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
|
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
|
||||||
import {TagMenu, useTagMenuControl} from '#/components/TagMenu'
|
import {RichTextTag} from '#/components/RichTextTag'
|
||||||
import {Text, TextProps} from '#/components/Typography'
|
import {Text, TextProps} from '#/components/Typography'
|
||||||
|
|
||||||
const WORD_WRAP = {wordWrap: 1}
|
const WORD_WRAP = {wordWrap: 1}
|
||||||
@@ -149,10 +143,9 @@ export function RichText({
|
|||||||
els.push(
|
els.push(
|
||||||
<RichTextTag
|
<RichTextTag
|
||||||
key={key}
|
key={key}
|
||||||
text={segment.text}
|
display={segment.text}
|
||||||
tag={tag.tag}
|
tag={tag.tag}
|
||||||
style={interactiveStyles}
|
textStyle={interactiveStyles}
|
||||||
selectable={selectable}
|
|
||||||
authorHandle={authorHandle}
|
authorHandle={authorHandle}
|
||||||
/>,
|
/>,
|
||||||
)
|
)
|
||||||
@@ -177,82 +170,3 @@ export function RichText({
|
|||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function RichTextTag({
|
|
||||||
text,
|
|
||||||
tag,
|
|
||||||
style,
|
|
||||||
selectable,
|
|
||||||
authorHandle,
|
|
||||||
}: {
|
|
||||||
text: string
|
|
||||||
tag: string
|
|
||||||
selectable?: boolean
|
|
||||||
authorHandle?: string
|
|
||||||
} & TextStyleProp) {
|
|
||||||
const t = useTheme()
|
|
||||||
const {_} = useLingui()
|
|
||||||
const control = useTagMenuControl()
|
|
||||||
const {
|
|
||||||
state: hovered,
|
|
||||||
onIn: onHoverIn,
|
|
||||||
onOut: onHoverOut,
|
|
||||||
} = useInteractionState()
|
|
||||||
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
|
|
||||||
const navigation = useNavigation<NavigationProp>()
|
|
||||||
|
|
||||||
const navigateToPage = React.useCallback(() => {
|
|
||||||
navigation.push('Hashtag', {
|
|
||||||
tag: encodeURIComponent(tag),
|
|
||||||
})
|
|
||||||
}, [navigation, tag])
|
|
||||||
|
|
||||||
const openDialog = React.useCallback(() => {
|
|
||||||
control.open()
|
|
||||||
}, [control])
|
|
||||||
|
|
||||||
/*
|
|
||||||
* N.B. On web, this is wrapped in another pressable comopnent with a11y
|
|
||||||
* labels, etc. That's why only some of these props are applied here.
|
|
||||||
*/
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<TagMenu control={control} tag={tag} authorHandle={authorHandle}>
|
|
||||||
<Text
|
|
||||||
emoji
|
|
||||||
selectable={selectable}
|
|
||||||
{...native({
|
|
||||||
accessibilityLabel: _(msg`Hashtag: #${tag}`),
|
|
||||||
accessibilityHint: _(msg`Long press to open tag menu for #${tag}`),
|
|
||||||
accessibilityRole: isNative ? 'button' : undefined,
|
|
||||||
onPress: navigateToPage,
|
|
||||||
onLongPress: openDialog,
|
|
||||||
})}
|
|
||||||
{...web({
|
|
||||||
onMouseEnter: onHoverIn,
|
|
||||||
onMouseLeave: onHoverOut,
|
|
||||||
})}
|
|
||||||
// @ts-ignore
|
|
||||||
onFocus={onFocus}
|
|
||||||
onBlur={onBlur}
|
|
||||||
style={[
|
|
||||||
web({
|
|
||||||
cursor: 'pointer',
|
|
||||||
}),
|
|
||||||
{color: t.palette.primary_500},
|
|
||||||
(hovered || focused) && {
|
|
||||||
...web({
|
|
||||||
outline: 0,
|
|
||||||
textDecorationLine: 'underline',
|
|
||||||
textDecorationColor: t.palette.primary_500,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
style,
|
|
||||||
]}>
|
|
||||||
{text}
|
|
||||||
</Text>
|
|
||||||
</TagMenu>
|
|
||||||
</React.Fragment>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {StyleProp, Text as RNText, TextStyle} from 'react-native'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {useNavigation} from '@react-navigation/native'
|
||||||
|
|
||||||
|
import {NavigationProp} from '#/lib/routes/types'
|
||||||
|
import {isInvalidHandle} from '#/lib/strings/handles'
|
||||||
|
import {isNative, isWeb} from '#/platform/detection'
|
||||||
|
import {
|
||||||
|
usePreferencesQuery,
|
||||||
|
useRemoveMutedWordsMutation,
|
||||||
|
useUpsertMutedWordsMutation,
|
||||||
|
} from '#/state/queries/preferences'
|
||||||
|
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
|
||||||
|
import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
|
||||||
|
import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person'
|
||||||
|
import {
|
||||||
|
createStaticClick,
|
||||||
|
createStaticClickIfUnmodified,
|
||||||
|
InlineLinkText,
|
||||||
|
} from '#/components/Link'
|
||||||
|
import {Loader} from '#/components/Loader'
|
||||||
|
import * as Menu from '#/components/Menu'
|
||||||
|
|
||||||
|
export function RichTextTag({
|
||||||
|
tag,
|
||||||
|
display,
|
||||||
|
authorHandle,
|
||||||
|
textStyle,
|
||||||
|
}: {
|
||||||
|
tag: string
|
||||||
|
display: string
|
||||||
|
authorHandle?: string
|
||||||
|
textStyle: StyleProp<TextStyle>
|
||||||
|
}) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const {isLoading: isPreferencesLoading, data: preferences} =
|
||||||
|
usePreferencesQuery()
|
||||||
|
const {
|
||||||
|
mutateAsync: upsertMutedWord,
|
||||||
|
variables: optimisticUpsert,
|
||||||
|
reset: resetUpsert,
|
||||||
|
} = useUpsertMutedWordsMutation()
|
||||||
|
const {
|
||||||
|
mutateAsync: removeMutedWords,
|
||||||
|
variables: optimisticRemove,
|
||||||
|
reset: resetRemove,
|
||||||
|
} = useRemoveMutedWordsMutation()
|
||||||
|
const navigation = useNavigation<NavigationProp>()
|
||||||
|
const label = _(msg`Hashtag ${tag}`)
|
||||||
|
const hint = isNative
|
||||||
|
? _(msg`Long press to open tag menu for #${tag}`)
|
||||||
|
: _(msg`Click to open tag menu for ${tag}`)
|
||||||
|
|
||||||
|
const isMuted = Boolean(
|
||||||
|
(preferences?.moderationPrefs.mutedWords?.find(
|
||||||
|
m => m.value === tag && m.targets.includes('tag'),
|
||||||
|
) ??
|
||||||
|
optimisticUpsert?.find(
|
||||||
|
m => m.value === tag && m.targets.includes('tag'),
|
||||||
|
)) &&
|
||||||
|
!optimisticRemove?.find(m => m?.value === tag),
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Mute word records that exactly match the tag in question.
|
||||||
|
*/
|
||||||
|
const removeableMuteWords = React.useMemo(() => {
|
||||||
|
return (
|
||||||
|
preferences?.moderationPrefs.mutedWords?.filter(word => {
|
||||||
|
return word.value === tag
|
||||||
|
}) || []
|
||||||
|
)
|
||||||
|
}, [tag, preferences?.moderationPrefs?.mutedWords])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu.Root>
|
||||||
|
<Menu.Trigger label={label} hint={hint}>
|
||||||
|
{({props: menuProps}) => (
|
||||||
|
<InlineLinkText
|
||||||
|
to={{
|
||||||
|
screen: 'Hashtag',
|
||||||
|
params: {tag: encodeURIComponent(tag)},
|
||||||
|
}}
|
||||||
|
{...menuProps}
|
||||||
|
onPress={e => {
|
||||||
|
if (isWeb) {
|
||||||
|
return createStaticClickIfUnmodified(() => {
|
||||||
|
if (!isNative) {
|
||||||
|
menuProps.onPress()
|
||||||
|
}
|
||||||
|
}).onPress(e)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onLongPress={createStaticClick(menuProps.onPress).onPress}
|
||||||
|
accessibilityHint={hint}
|
||||||
|
label={label}
|
||||||
|
style={textStyle}>
|
||||||
|
{isNative ? (
|
||||||
|
display
|
||||||
|
) : (
|
||||||
|
<RNText ref={menuProps.ref}>{display}</RNText>
|
||||||
|
)}
|
||||||
|
</InlineLinkText>
|
||||||
|
)}
|
||||||
|
</Menu.Trigger>
|
||||||
|
<Menu.Outer>
|
||||||
|
<Menu.Group>
|
||||||
|
<Menu.Item
|
||||||
|
label={_(msg`See ${tag} posts`)}
|
||||||
|
onPress={() => {
|
||||||
|
navigation.push('Hashtag', {
|
||||||
|
tag: encodeURIComponent(tag),
|
||||||
|
})
|
||||||
|
}}>
|
||||||
|
<Menu.ItemText>
|
||||||
|
<Trans>See #{tag} posts</Trans>
|
||||||
|
</Menu.ItemText>
|
||||||
|
<Menu.ItemIcon icon={Search} />
|
||||||
|
</Menu.Item>
|
||||||
|
{authorHandle && !isInvalidHandle(authorHandle) && (
|
||||||
|
<Menu.Item
|
||||||
|
label={_(msg`See ${tag} posts by user`)}
|
||||||
|
onPress={() => {
|
||||||
|
navigation.push('Hashtag', {
|
||||||
|
tag: encodeURIComponent(tag),
|
||||||
|
author: authorHandle,
|
||||||
|
})
|
||||||
|
}}>
|
||||||
|
<Menu.ItemText>
|
||||||
|
<Trans>See #{tag} posts by user</Trans>
|
||||||
|
</Menu.ItemText>
|
||||||
|
<Menu.ItemIcon icon={Person} />
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
</Menu.Group>
|
||||||
|
<Menu.Divider />
|
||||||
|
<Menu.Item
|
||||||
|
label={isMuted ? _(msg`Unmute ${tag}`) : _(msg`Mute ${tag}`)}
|
||||||
|
onPress={() => {
|
||||||
|
if (isMuted) {
|
||||||
|
resetUpsert()
|
||||||
|
removeMutedWords(removeableMuteWords)
|
||||||
|
} else {
|
||||||
|
resetRemove()
|
||||||
|
upsertMutedWord([
|
||||||
|
{value: tag, targets: ['tag'], actorTarget: 'all'},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<Menu.ItemText>
|
||||||
|
{isMuted ? _(msg`Unmute ${tag}`) : _(msg`Mute ${tag}`)}
|
||||||
|
</Menu.ItemText>
|
||||||
|
<Menu.ItemIcon icon={isPreferencesLoading ? Loader : Mute} />
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Outer>
|
||||||
|
</Menu.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,290 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import {View} from 'react-native'
|
|
||||||
import {msg, Trans} from '@lingui/macro'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
import {useNavigation} from '@react-navigation/native'
|
|
||||||
|
|
||||||
import {NavigationProp} from '#/lib/routes/types'
|
|
||||||
import {isInvalidHandle} from '#/lib/strings/handles'
|
|
||||||
import {
|
|
||||||
usePreferencesQuery,
|
|
||||||
useRemoveMutedWordsMutation,
|
|
||||||
useUpsertMutedWordsMutation,
|
|
||||||
} from '#/state/queries/preferences'
|
|
||||||
import {atoms as a, native, useTheme} from '#/alf'
|
|
||||||
import {Button, ButtonText} from '#/components/Button'
|
|
||||||
import * as Dialog from '#/components/Dialog'
|
|
||||||
import {Divider} from '#/components/Divider'
|
|
||||||
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
|
|
||||||
import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
|
|
||||||
import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person'
|
|
||||||
import {createStaticClick, Link} from '#/components/Link'
|
|
||||||
import {Loader} from '#/components/Loader'
|
|
||||||
import {Text} from '#/components/Typography'
|
|
||||||
|
|
||||||
export function useTagMenuControl() {
|
|
||||||
return Dialog.useDialogControl()
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TagMenu({
|
|
||||||
children,
|
|
||||||
control,
|
|
||||||
tag,
|
|
||||||
authorHandle,
|
|
||||||
}: React.PropsWithChildren<{
|
|
||||||
control: Dialog.DialogOuterProps['control']
|
|
||||||
/**
|
|
||||||
* This should be the sanitized tag value from the facet itself, not the
|
|
||||||
* "display" value with a leading `#`.
|
|
||||||
*/
|
|
||||||
tag: string
|
|
||||||
authorHandle?: string
|
|
||||||
}>) {
|
|
||||||
const navigation = useNavigation<NavigationProp>()
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{children}
|
|
||||||
<Dialog.Outer control={control}>
|
|
||||||
<Dialog.Handle />
|
|
||||||
<TagMenuInner
|
|
||||||
control={control}
|
|
||||||
tag={tag}
|
|
||||||
authorHandle={authorHandle}
|
|
||||||
navigation={navigation}
|
|
||||||
/>
|
|
||||||
</Dialog.Outer>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TagMenuInner({
|
|
||||||
control,
|
|
||||||
tag,
|
|
||||||
authorHandle,
|
|
||||||
navigation,
|
|
||||||
}: {
|
|
||||||
control: Dialog.DialogOuterProps['control']
|
|
||||||
tag: string
|
|
||||||
authorHandle?: string
|
|
||||||
// Passed down because on native, we don't use real portals (and context would be wrong).
|
|
||||||
navigation: NavigationProp
|
|
||||||
}) {
|
|
||||||
const {_} = useLingui()
|
|
||||||
const t = useTheme()
|
|
||||||
const {isLoading: isPreferencesLoading, data: preferences} =
|
|
||||||
usePreferencesQuery()
|
|
||||||
const {
|
|
||||||
mutateAsync: upsertMutedWord,
|
|
||||||
variables: optimisticUpsert,
|
|
||||||
reset: resetUpsert,
|
|
||||||
} = useUpsertMutedWordsMutation()
|
|
||||||
const {
|
|
||||||
mutateAsync: removeMutedWords,
|
|
||||||
variables: optimisticRemove,
|
|
||||||
reset: resetRemove,
|
|
||||||
} = useRemoveMutedWordsMutation()
|
|
||||||
const displayTag = '#' + tag
|
|
||||||
|
|
||||||
const isMuted = Boolean(
|
|
||||||
(preferences?.moderationPrefs.mutedWords?.find(
|
|
||||||
m => m.value === tag && m.targets.includes('tag'),
|
|
||||||
) ??
|
|
||||||
optimisticUpsert?.find(
|
|
||||||
m => m.value === tag && m.targets.includes('tag'),
|
|
||||||
)) &&
|
|
||||||
!optimisticRemove?.find(m => m?.value === tag),
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Mute word records that exactly match the tag in question.
|
|
||||||
*/
|
|
||||||
const removeableMuteWords = React.useMemo(() => {
|
|
||||||
return (
|
|
||||||
preferences?.moderationPrefs.mutedWords?.filter(word => {
|
|
||||||
return word.value === tag
|
|
||||||
}) || []
|
|
||||||
)
|
|
||||||
}, [tag, preferences?.moderationPrefs?.mutedWords])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog.Inner label={_(msg`Tag menu: ${displayTag}`)}>
|
|
||||||
{isPreferencesLoading ? (
|
|
||||||
<View style={[a.w_full, a.align_center]}>
|
|
||||||
<Loader size="lg" />
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.rounded_md,
|
|
||||||
a.border,
|
|
||||||
a.mb_md,
|
|
||||||
t.atoms.border_contrast_low,
|
|
||||||
t.atoms.bg_contrast_25,
|
|
||||||
]}>
|
|
||||||
<Link
|
|
||||||
label={_(msg`View all posts with tag ${displayTag}`)}
|
|
||||||
{...createStaticClick(() => {
|
|
||||||
control.close(() => {
|
|
||||||
navigation.push('Hashtag', {
|
|
||||||
tag: encodeURIComponent(tag),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.w_full,
|
|
||||||
a.flex_row,
|
|
||||||
a.align_center,
|
|
||||||
a.justify_start,
|
|
||||||
a.gap_md,
|
|
||||||
a.px_lg,
|
|
||||||
a.py_md,
|
|
||||||
]}>
|
|
||||||
<Search size="lg" style={[t.atoms.text_contrast_medium]} />
|
|
||||||
<Text
|
|
||||||
numberOfLines={1}
|
|
||||||
ellipsizeMode="middle"
|
|
||||||
style={[
|
|
||||||
a.flex_1,
|
|
||||||
a.text_md,
|
|
||||||
a.font_bold,
|
|
||||||
native({top: 2}),
|
|
||||||
t.atoms.text_contrast_medium,
|
|
||||||
]}>
|
|
||||||
<Trans>
|
|
||||||
See{' '}
|
|
||||||
<Text style={[a.text_md, a.font_bold, t.atoms.text]}>
|
|
||||||
{displayTag}
|
|
||||||
</Text>{' '}
|
|
||||||
posts
|
|
||||||
</Trans>
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{authorHandle && !isInvalidHandle(authorHandle) && (
|
|
||||||
<>
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<Link
|
|
||||||
label={_(
|
|
||||||
msg`View all posts by @${authorHandle} with tag ${displayTag}`,
|
|
||||||
)}
|
|
||||||
{...createStaticClick(() => {
|
|
||||||
control.close(() => {
|
|
||||||
navigation.push('Hashtag', {
|
|
||||||
tag: encodeURIComponent(tag),
|
|
||||||
author: authorHandle,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.w_full,
|
|
||||||
a.flex_row,
|
|
||||||
a.align_center,
|
|
||||||
a.justify_start,
|
|
||||||
a.gap_md,
|
|
||||||
a.px_lg,
|
|
||||||
a.py_md,
|
|
||||||
]}>
|
|
||||||
<Person size="lg" style={[t.atoms.text_contrast_medium]} />
|
|
||||||
<Text
|
|
||||||
numberOfLines={1}
|
|
||||||
ellipsizeMode="middle"
|
|
||||||
style={[
|
|
||||||
a.flex_1,
|
|
||||||
a.text_md,
|
|
||||||
a.font_bold,
|
|
||||||
native({top: 2}),
|
|
||||||
t.atoms.text_contrast_medium,
|
|
||||||
]}>
|
|
||||||
<Trans>
|
|
||||||
See{' '}
|
|
||||||
<Text style={[a.text_md, a.font_bold, t.atoms.text]}>
|
|
||||||
{displayTag}
|
|
||||||
</Text>{' '}
|
|
||||||
posts by this user
|
|
||||||
</Trans>
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{preferences ? (
|
|
||||||
<>
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
label={
|
|
||||||
isMuted
|
|
||||||
? _(msg`Unmute all ${displayTag} posts`)
|
|
||||||
: _(msg`Mute all ${displayTag} posts`)
|
|
||||||
}
|
|
||||||
onPress={() => {
|
|
||||||
control.close(() => {
|
|
||||||
if (isMuted) {
|
|
||||||
resetUpsert()
|
|
||||||
removeMutedWords(removeableMuteWords)
|
|
||||||
} else {
|
|
||||||
resetRemove()
|
|
||||||
upsertMutedWord([
|
|
||||||
{
|
|
||||||
value: tag,
|
|
||||||
targets: ['tag'],
|
|
||||||
actorTarget: 'all',
|
|
||||||
},
|
|
||||||
])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.w_full,
|
|
||||||
a.flex_row,
|
|
||||||
a.align_center,
|
|
||||||
a.justify_start,
|
|
||||||
a.gap_md,
|
|
||||||
a.px_lg,
|
|
||||||
a.py_md,
|
|
||||||
]}>
|
|
||||||
<Mute size="lg" style={[t.atoms.text_contrast_medium]} />
|
|
||||||
<Text
|
|
||||||
numberOfLines={1}
|
|
||||||
ellipsizeMode="middle"
|
|
||||||
style={[
|
|
||||||
a.flex_1,
|
|
||||||
a.text_md,
|
|
||||||
a.font_bold,
|
|
||||||
native({top: 2}),
|
|
||||||
t.atoms.text_contrast_medium,
|
|
||||||
]}>
|
|
||||||
{isMuted ? _(msg`Unmute`) : _(msg`Mute`)}{' '}
|
|
||||||
<Text style={[a.text_md, a.font_bold, t.atoms.text]}>
|
|
||||||
{displayTag}
|
|
||||||
</Text>{' '}
|
|
||||||
<Trans>posts</Trans>
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
label={_(msg`Close this dialog`)}
|
|
||||||
size="small"
|
|
||||||
variant="ghost"
|
|
||||||
color="secondary"
|
|
||||||
onPress={() => control.close()}>
|
|
||||||
<ButtonText>
|
|
||||||
<Trans>Cancel</Trans>
|
|
||||||
</ButtonText>
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Dialog.Inner>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import {msg} from '@lingui/macro'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
import {useNavigation} from '@react-navigation/native'
|
|
||||||
|
|
||||||
import {NavigationProp} from '#/lib/routes/types'
|
|
||||||
import {isInvalidHandle} from '#/lib/strings/handles'
|
|
||||||
import {enforceLen} from '#/lib/strings/helpers'
|
|
||||||
import {
|
|
||||||
usePreferencesQuery,
|
|
||||||
useRemoveMutedWordsMutation,
|
|
||||||
useUpsertMutedWordsMutation,
|
|
||||||
} from '#/state/queries/preferences'
|
|
||||||
import {EventStopper} from '#/view/com/util/EventStopper'
|
|
||||||
import {NativeDropdown} from '#/view/com/util/forms/NativeDropdown'
|
|
||||||
import {web} from '#/alf'
|
|
||||||
import * as Dialog from '#/components/Dialog'
|
|
||||||
|
|
||||||
export function useTagMenuControl(): Dialog.DialogControlProps {
|
|
||||||
return {
|
|
||||||
id: '',
|
|
||||||
// @ts-ignore
|
|
||||||
ref: null,
|
|
||||||
open: () => {
|
|
||||||
throw new Error(`TagMenu controls are only available on native platforms`)
|
|
||||||
},
|
|
||||||
close: () => {
|
|
||||||
throw new Error(`TagMenu controls are only available on native platforms`)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TagMenu({
|
|
||||||
children,
|
|
||||||
tag,
|
|
||||||
authorHandle,
|
|
||||||
}: React.PropsWithChildren<{
|
|
||||||
/**
|
|
||||||
* This should be the sanitized tag value from the facet itself, not the
|
|
||||||
* "display" value with a leading `#`.
|
|
||||||
*/
|
|
||||||
tag: string
|
|
||||||
authorHandle?: string
|
|
||||||
}>) {
|
|
||||||
const {_} = useLingui()
|
|
||||||
const navigation = useNavigation<NavigationProp>()
|
|
||||||
const {data: preferences} = usePreferencesQuery()
|
|
||||||
const {mutateAsync: upsertMutedWord, variables: optimisticUpsert} =
|
|
||||||
useUpsertMutedWordsMutation()
|
|
||||||
const {mutateAsync: removeMutedWords, variables: optimisticRemove} =
|
|
||||||
useRemoveMutedWordsMutation()
|
|
||||||
const isMuted = Boolean(
|
|
||||||
(preferences?.moderationPrefs.mutedWords?.find(
|
|
||||||
m => m.value === tag && m.targets.includes('tag'),
|
|
||||||
) ??
|
|
||||||
optimisticUpsert?.find(
|
|
||||||
m => m.value === tag && m.targets.includes('tag'),
|
|
||||||
)) &&
|
|
||||||
!optimisticRemove?.find(m => m?.value === tag),
|
|
||||||
)
|
|
||||||
const truncatedTag = '#' + enforceLen(tag, 15, true, 'middle')
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Mute word records that exactly match the tag in question.
|
|
||||||
*/
|
|
||||||
const removeableMuteWords = React.useMemo(() => {
|
|
||||||
return (
|
|
||||||
preferences?.moderationPrefs.mutedWords?.filter(word => {
|
|
||||||
return word.value === tag
|
|
||||||
}) || []
|
|
||||||
)
|
|
||||||
}, [tag, preferences?.moderationPrefs?.mutedWords])
|
|
||||||
|
|
||||||
const dropdownItems = React.useMemo(() => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
label: _(msg`See ${truncatedTag} posts`),
|
|
||||||
onPress() {
|
|
||||||
navigation.push('Hashtag', {
|
|
||||||
tag: encodeURIComponent(tag),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
testID: 'tagMenuSearch',
|
|
||||||
icon: {
|
|
||||||
ios: {
|
|
||||||
name: 'magnifyingglass',
|
|
||||||
},
|
|
||||||
android: '',
|
|
||||||
web: 'magnifying-glass',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
authorHandle &&
|
|
||||||
!isInvalidHandle(authorHandle) && {
|
|
||||||
label: _(msg`See ${truncatedTag} posts by user`),
|
|
||||||
onPress() {
|
|
||||||
navigation.push('Hashtag', {
|
|
||||||
tag: encodeURIComponent(tag),
|
|
||||||
author: authorHandle,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
testID: 'tagMenuSearchByUser',
|
|
||||||
icon: {
|
|
||||||
ios: {
|
|
||||||
name: 'magnifyingglass',
|
|
||||||
},
|
|
||||||
android: '',
|
|
||||||
web: ['far', 'user'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
preferences && {
|
|
||||||
label: 'separator',
|
|
||||||
},
|
|
||||||
preferences && {
|
|
||||||
label: isMuted
|
|
||||||
? _(msg`Unmute ${truncatedTag}`)
|
|
||||||
: _(msg`Mute ${truncatedTag}`),
|
|
||||||
onPress() {
|
|
||||||
if (isMuted) {
|
|
||||||
removeMutedWords(removeableMuteWords)
|
|
||||||
} else {
|
|
||||||
upsertMutedWord([
|
|
||||||
{value: tag, targets: ['tag'], actorTarget: 'all'},
|
|
||||||
])
|
|
||||||
}
|
|
||||||
},
|
|
||||||
testID: 'tagMenuMute',
|
|
||||||
icon: {
|
|
||||||
ios: {
|
|
||||||
name: 'speaker.slash',
|
|
||||||
},
|
|
||||||
android: 'ic_menu_sort_alphabetically',
|
|
||||||
web: isMuted ? 'eye' : ['far', 'eye-slash'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
].filter(Boolean)
|
|
||||||
}, [
|
|
||||||
_,
|
|
||||||
authorHandle,
|
|
||||||
isMuted,
|
|
||||||
navigation,
|
|
||||||
preferences,
|
|
||||||
tag,
|
|
||||||
truncatedTag,
|
|
||||||
upsertMutedWord,
|
|
||||||
removeMutedWords,
|
|
||||||
removeableMuteWords,
|
|
||||||
])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EventStopper>
|
|
||||||
<NativeDropdown
|
|
||||||
accessibilityLabel={_(msg`Click here to open tag menu for ${tag}`)}
|
|
||||||
accessibilityHint=""
|
|
||||||
// @ts-ignore
|
|
||||||
items={dropdownItems}
|
|
||||||
triggerStyle={web({
|
|
||||||
textAlign: 'left',
|
|
||||||
})}>
|
|
||||||
{children}
|
|
||||||
</NativeDropdown>
|
|
||||||
</EventStopper>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
+1
-13
@@ -1,4 +1,4 @@
|
|||||||
import {GestureResponderEvent, Linking} from 'react-native'
|
import {Linking} from 'react-native'
|
||||||
|
|
||||||
import {isNative, isWeb} from './detection'
|
import {isNative, isWeb} from './detection'
|
||||||
|
|
||||||
@@ -24,15 +24,3 @@ export function clearHash() {
|
|||||||
window.location.hash = ''
|
window.location.hash = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shouldClickOpenNewTab(e: GestureResponderEvent) {
|
|
||||||
/**
|
|
||||||
* A `GestureResponderEvent`, but cast to `any` to avoid using a bunch
|
|
||||||
* of @ts-ignore below.
|
|
||||||
*/
|
|
||||||
const event = e as any
|
|
||||||
const isMiddleClick = isWeb && event.button === 1
|
|
||||||
const isMetaKey =
|
|
||||||
isWeb && (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey)
|
|
||||||
return isMetaKey || isMiddleClick
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import {usePalette} from '#/lib/hooks/usePalette'
|
|||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
import {s} from '#/lib/styles'
|
import {s} from '#/lib/styles'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {shouldClickOpenNewTab} from '#/platform/urls'
|
|
||||||
import {FeedSourceInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
|
import {FeedSourceInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
|
||||||
import {
|
import {
|
||||||
useAddSavedFeedsMutation,
|
useAddSavedFeedsMutation,
|
||||||
@@ -29,6 +28,7 @@ import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
|||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {useTheme} from '#/alf'
|
import {useTheme} from '#/alf'
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
|
import {shouldClickOpenNewTab} from '#/components/Link'
|
||||||
import * as Prompt from '#/components/Prompt'
|
import * as Prompt from '#/components/Prompt'
|
||||||
import {RichText} from '#/components/RichText'
|
import {RichText} from '#/components/RichText'
|
||||||
import {Text} from '../util/text/Text'
|
import {Text} from '../util/text/Text'
|
||||||
|
|||||||
Reference in New Issue
Block a user