Merge branch '3p-moderators' of github.com:bluesky-social/social-app into 3p-moderators

This commit is contained in:
Paul Frazee
2024-03-10 10:45:51 -07:00
35 changed files with 586 additions and 254 deletions
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M5.002 17.036V5h14v12.036h-3.986a1 1 0 0 0-.639.23l-2.375 1.968-2.344-1.965a1 1 0 0 0-.643-.233H5.002ZM20.002 3h-16a1 1 0 0 0-1 1v14.036a1 1 0 0 0 1 1h4.65l2.704 2.266a1 1 0 0 0 1.28.004l2.74-2.27h4.626a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1Zm-7.878 3.663c-1.39 0-2.5 1.135-2.5 2.515a1 1 0 0 0 2 0c0-.294.232-.515.5-.515a.507.507 0 0 1 .489.6.174.174 0 0 1-.027.048 1.1 1.1 0 0 1-.267.226c-.508.345-1.128.923-1.286 1.978a1 1 0 1 0 1.978.297.762.762 0 0 1 .14-.359c.063-.086.155-.169.293-.262.436-.297 1.18-.885 1.18-2.013 0-1.38-1.11-2.515-2.5-2.515ZM12 15.75a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 738 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v4a1 1 0 0 1-.293.707L15 14.414V20a1 1 0 0 1-.758.97l-4 1A1 1 0 0 1 9 21v-6.586L3.293 8.707A1 1 0 0 1 3 8V4Zm2 1v2.586l5.707 5.707A1 1 0 0 1 11 14v5.72l2-.5V14a1 1 0 0 1 .293-.707L19 7.586V5H5Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 371 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12.472 3.118A1 1 0 0 1 13 4v16a1 1 0 0 1-1.555.832L5.697 17H2a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1h3.697l5.748-3.832a1 1 0 0 1 1.027-.05ZM11 5.868 6.555 8.833A1 1 0 0 1 6 9H3v6h3a1 1 0 0 1 .555.168L11 18.131V5.87Zm7.364-1.645a1 1 0 0 1 1.414 0A10.969 10.969 0 0 1 23 12c0 3.037-1.232 5.788-3.222 7.778a1 1 0 1 1-1.414-1.414A8.969 8.969 0 0 0 21 12a8.969 8.969 0 0 0-2.636-6.364 1 1 0 0 1 0-1.414Zm-3.182 3.181a1 1 0 0 1 1.414 0A6.483 6.483 0 0 1 18.5 12a6.483 6.483 0 0 1-1.904 4.597 1 1 0 0 1-1.414-1.415A4.483 4.483 0 0 0 16.5 12a4.483 4.483 0 0 0-1.318-3.182 1 1 0 0 1 0-1.414Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 717 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M7.416 5H3a1 1 0 0 0 0 2h1.064l.938 14.067A1 1 0 0 0 6 22h12a1 1 0 0 0 .998-.933L19.936 7H21a1 1 0 1 0 0-2h-4.416a5 5 0 0 0-9.168 0Zm2.348 0h4.472c-.55-.614-1.348-1-2.236-1-.888 0-1.687.386-2.236 1Zm6.087 2H6.07l.867 13h10.128l.867-13h-2.036a1 1 0 0 1-.044 0ZM10 10a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-5a1 1 0 0 1 1-1Zm4 0a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-5a1 1 0 0 1 1-1Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 508 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M11.14 4.494a.995.995 0 0 1 1.72 0l7.001 12.008a.996.996 0 0 1-.86 1.498H4.999a.996.996 0 0 1-.86-1.498L11.14 4.494Zm3.447-1.007c-1.155-1.983-4.019-1.983-5.174 0L2.41 15.494C1.247 17.491 2.686 20 4.998 20h14.004c2.312 0 3.751-2.509 2.587-4.506L14.587 3.487ZM13 9.019a1 1 0 1 0-2 0v2.994a1 1 0 1 0 2 0V9.02Zm-1 4.731a1.25 1.25 0 1 0 0 2.5 1.25 1.25 0 0 0 0-2.5Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 503 B

+3
View File
@@ -43,6 +43,9 @@
height: calc(100% + env(safe-area-inset-top)); height: calc(100% + env(safe-area-inset-top));
scrollbar-gutter: stable both-edges; scrollbar-gutter: stable both-edges;
} }
html, body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
/* Buttons and inputs have a font set by UA, so we'll have to reset that */ /* Buttons and inputs have a font set by UA, so we'll have to reset that */
button, input, textarea { button, input, textarea {
+6
View File
@@ -80,6 +80,7 @@ import {msg} from '@lingui/macro'
import {i18n, MessageDescriptor} from '@lingui/core' import {i18n, MessageDescriptor} from '@lingui/core'
import HashtagScreen from '#/screens/Hashtag' import HashtagScreen from '#/screens/Hashtag'
import {logEvent} from './lib/statsig/statsig' import {logEvent} from './lib/statsig/statsig'
import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
const navigationRef = createNavigationContainerRef<AllNavigatorParams>() const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
@@ -199,6 +200,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
getComponent={() => ProfileFeedLikedByScreen} getComponent={() => ProfileFeedLikedByScreen}
options={{title: title(msg`Liked by`)}} options={{title: title(msg`Liked by`)}}
/> />
<Stack.Screen
name="ProfileLabelerLikedBy"
getComponent={() => ProfileLabelerLikedByScreen}
options={{title: title(msg`Liked by`)}}
/>
<Stack.Screen <Stack.Screen
name="Debug" name="Debug"
getComponent={() => Storybook} getComponent={() => Storybook}
+7
View File
@@ -49,6 +49,9 @@ export const atoms = {
h_full: { h_full: {
height: '100%', height: '100%',
}, },
h_full_vh: web({
height: '100vh',
}),
/* /*
* Border radius * Border radius
@@ -524,6 +527,10 @@ export const atoms = {
/* /*
* Margin * Margin
*/ */
mx_auto: {
marginLeft: 'auto',
marginRight: 'auto',
},
m_2xs: { m_2xs: {
margin: tokens.space._2xs, margin: tokens.space._2xs,
}, },
+3 -1
View File
@@ -27,7 +27,7 @@ export type ButtonColor =
| 'gradient_sunset' | 'gradient_sunset'
| 'gradient_nordic' | 'gradient_nordic'
| 'gradient_bonfire' | 'gradient_bonfire'
export type ButtonSize = 'tiny' | 'small' | 'large' export type ButtonSize = 'tiny' | 'small' | 'medium' | 'large'
export type ButtonShape = 'round' | 'square' | 'default' export type ButtonShape = 'round' | 'square' | 'default'
export type VariantProps = { export type VariantProps = {
/** /**
@@ -274,6 +274,8 @@ export function Button({
if (shape === 'default') { if (shape === 'default') {
if (size === 'large') { if (size === 'large') {
baseStyles.push({paddingVertical: 15}, a.px_2xl, a.rounded_sm, a.gap_md) baseStyles.push({paddingVertical: 15}, a.px_2xl, a.rounded_sm, a.gap_md)
} else if (size === 'medium') {
baseStyles.push({paddingVertical: 12}, a.px_2xl, a.rounded_sm, a.gap_md)
} else if (size === 'small') { } else if (size === 'small') {
baseStyles.push({paddingVertical: 9}, a.px_lg, a.rounded_sm, a.gap_sm) baseStyles.push({paddingVertical: 9}, a.px_lg, a.rounded_sm, a.gap_sm)
} else if (size === 'tiny') { } else if (size === 'tiny') {
+2 -4
View File
@@ -21,8 +21,7 @@ export function useDialogControl(): DialogOuterProps['control'] {
open: () => {}, open: () => {},
close: () => {}, close: () => {},
}) })
const {activeDialogs, openDialogs} = useDialogStateContext() const {activeDialogs} = useDialogStateContext()
const isOpen = openDialogs.includes(id)
React.useEffect(() => { React.useEffect(() => {
activeDialogs.current.set(id, control) activeDialogs.current.set(id, control)
@@ -36,7 +35,6 @@ export function useDialogControl(): DialogOuterProps['control'] {
() => ({ () => ({
id, id,
ref: control, ref: control,
isOpen,
open: () => { open: () => {
control.current.open() control.current.open()
}, },
@@ -44,6 +42,6 @@ export function useDialogControl(): DialogOuterProps['control'] {
control.current.close(cb) control.current.close(cb)
}, },
}), }),
[id, control, isOpen], [id, control],
) )
} }
+1 -1
View File
@@ -22,7 +22,7 @@ export type DialogControlRefProps = {
export type DialogControlProps = DialogControlRefProps & { export type DialogControlProps = DialogControlRefProps & {
id: string id: string
ref: React.RefObject<DialogControlRefProps> ref: React.RefObject<DialogControlRefProps>
isOpen: boolean isOpen?: boolean
} }
export type DialogContextProps = { export type DialogContextProps = {
+137
View File
@@ -0,0 +1,137 @@
import React from 'react'
import {View} from 'react-native'
import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {logger} from '#/logger'
import {List} from '#/view/com/util/List'
import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
import {useLikedByQuery} from '#/state/queries/post-liked-by'
import {cleanError} from '#/lib/strings/errors'
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as Refresh} from './icons/ArrowRotateCounterClockwise'
import {atoms as a, useTheme} from '#/alf'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
export function LikedByList({uri}: {uri: string}) {
const t = useTheme()
const {_} = useLingui()
const [isPTRing, setIsPTRing] = React.useState(false)
const {
data: resolvedUri,
error: resolveError,
isFetching: isFetchingResolvedUri,
} = useResolveUriQuery(uri)
const {
data,
isFetching,
isFetched,
isFetchingNextPage,
hasNextPage,
fetchNextPage,
isError,
error,
refetch,
} = useLikedByQuery(resolvedUri?.uri)
const likes = React.useMemo(() => {
if (data?.pages) {
return data.pages.flatMap(page => page.likes)
}
return []
}, [data])
const onRefresh = React.useCallback(async () => {
setIsPTRing(true)
try {
await refetch()
} catch (err) {
logger.error('Failed to refresh likes', {message: err})
}
setIsPTRing(false)
}, [refetch, setIsPTRing])
const onEndReached = React.useCallback(async () => {
if (isFetching || !hasNextPage || isError) return
try {
await fetchNextPage()
} catch (err) {
logger.error('Failed to load more likes', {message: err})
}
}, [isFetching, hasNextPage, isError, fetchNextPage])
const renderItem = React.useCallback(({item}: {item: GetLikes.Like}) => {
return (
<ProfileCardWithFollowBtn key={item.actor.did} profile={item.actor} />
)
}, [])
if (isFetchingResolvedUri || !isFetched) {
return (
<View style={[a.w_full, a.align_center, a.p_lg]}>
<Loader size="xl" />
</View>
)
}
if (resolveError || isError) {
return (
<View style={[a.p_lg]}>
<View style={[a.p_lg, a.rounded_sm, t.atoms.bg_contrast_25]}>
<Text style={[a.text_md, a.pb_lg]}>
{cleanError(resolveError || error)}
</Text>
<View style={[a.flex_row, a.justify_end]}>
<Button
label={_(msg``)}
onPress={onRefresh}
size="small"
variant="solid"
color="primary">
<ButtonText>
<Trans>Try again</Trans>
</ButtonText>
<ButtonIcon icon={Refresh} position="right" />
</Button>
</View>
</View>
</View>
)
}
return likes.length ? (
<List
data={likes}
keyExtractor={item => item.actor.did}
refreshing={isPTRing}
onRefresh={onRefresh}
onEndReached={onEndReached}
renderItem={renderItem}
initialNumToRender={15}
contentContainerStyle={{borderWidth: 0}}
// FIXME(dan)
// eslint-disable-next-line react/no-unstable-nested-components
ListFooterComponent={() => (
<View style={[a.w_full, a.align_center, a.p_lg]}>
{(isFetching || isFetchingNextPage) && <Loader size="xl" />}
</View>
)}
// @ts-ignore our .web version only -prf
desktopFixedHeight
/>
) : (
<View style={[a.p_lg]}>
<View style={[a.p_lg, a.rounded_sm, t.atoms.bg_contrast_25]}>
<Text style={[a.text_md, a.leading_snug]}>
<Trans>
Nobody has liked this yet. Maybe you should be the first!
</Trans>
</Text>
</View>
</View>
)
}
+4 -4
View File
@@ -92,10 +92,8 @@ export function Trigger({children, label, style}: TriggerProps) {
accessibilityLabel={label} accessibilityLabel={label}
onFocus={onFocus} onFocus={onFocus}
onBlur={onBlur} onBlur={onBlur}
style={flatten([style, web({outline: 0})])} style={flatten([style, focused && web({outline: 0})])}
onPointerDown={() => { onPointerDown={() => control.open()}
control.open()
}}
{...web({ {...web({
onMouseEnter, onMouseEnter,
onMouseLeave, onMouseLeave,
@@ -131,6 +129,7 @@ export function Outer({children}: React.PropsWithChildren<{}>) {
{children} {children}
</View> </View>
{/* Disabled until we can fix positioning
<DropdownMenu.Arrow <DropdownMenu.Arrow
className="DropdownMenuArrow" className="DropdownMenuArrow"
fill={ fill={
@@ -138,6 +137,7 @@ export function Outer({children}: React.PropsWithChildren<{}>) {
.backgroundColor .backgroundColor
} }
/> />
*/}
</DropdownMenu.Content> </DropdownMenu.Content>
</DropdownMenu.Portal> </DropdownMenu.Portal>
) )
+18 -7
View File
@@ -3,7 +3,7 @@ import {View, PressableProps} from 'react-native'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useTheme, atoms as a} from '#/alf' import {useTheme, atoms as a, useBreakpoints} from '#/alf'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {Button} from '#/components/Button' import {Button} from '#/components/Button'
@@ -25,6 +25,7 @@ export function Outer({
}: React.PropsWithChildren<{ }: React.PropsWithChildren<{
control: Dialog.DialogOuterProps['control'] control: Dialog.DialogOuterProps['control']
}>) { }>) {
const {gtMobile} = useBreakpoints()
const titleId = React.useId() const titleId = React.useId()
const descriptionId = React.useId() const descriptionId = React.useId()
@@ -38,12 +39,12 @@ export function Outer({
<Context.Provider value={context}> <Context.Provider value={context}>
<Dialog.Handle /> <Dialog.Handle />
<Dialog.Inner <Dialog.ScrollableInner
accessibilityLabelledBy={titleId} accessibilityLabelledBy={titleId}
accessibilityDescribedBy={descriptionId} accessibilityDescribedBy={descriptionId}
style={[{width: 'auto', maxWidth: 400}]}> style={[gtMobile ? {width: 'auto', maxWidth: 400} : a.w_full]}>
{children} {children}
</Dialog.Inner> </Dialog.ScrollableInner>
</Context.Provider> </Context.Provider>
</Dialog.Outer> </Dialog.Outer>
) )
@@ -71,8 +72,16 @@ export function Description({children}: React.PropsWithChildren<{}>) {
} }
export function Actions({children}: React.PropsWithChildren<{}>) { export function Actions({children}: React.PropsWithChildren<{}>) {
const {gtMobile} = useBreakpoints()
return ( return (
<View style={[a.w_full, a.flex_row, a.gap_sm, a.justify_end]}> <View
style={[
a.w_full,
a.gap_sm,
a.justify_end,
gtMobile ? [a.flex_row] : [a.flex_col, a.pt_md, a.pb_4xl],
]}>
{children} {children}
</View> </View>
) )
@@ -82,12 +91,13 @@ export function Cancel({
children, children,
}: React.PropsWithChildren<{onPress?: PressableProps['onPress']}>) { }: React.PropsWithChildren<{onPress?: PressableProps['onPress']}>) {
const {_} = useLingui() const {_} = useLingui()
const {gtMobile} = useBreakpoints()
const {close} = Dialog.useDialogContext() const {close} = Dialog.useDialogContext()
return ( return (
<Button <Button
variant="solid" variant="solid"
color="secondary" color="secondary"
size="small" size={gtMobile ? 'small' : 'medium'}
label={_(msg`Cancel`)} label={_(msg`Cancel`)}
onPress={() => close()}> onPress={() => close()}>
{children} {children}
@@ -100,6 +110,7 @@ export function Action({
onPress, onPress,
}: React.PropsWithChildren<{onPress?: () => void}>) { }: React.PropsWithChildren<{onPress?: () => void}>) {
const {_} = useLingui() const {_} = useLingui()
const {gtMobile} = useBreakpoints()
const {close} = Dialog.useDialogContext() const {close} = Dialog.useDialogContext()
const handleOnPress = React.useCallback(() => { const handleOnPress = React.useCallback(() => {
close() close()
@@ -109,7 +120,7 @@ export function Action({
<Button <Button
variant="solid" variant="solid"
color="primary" color="primary"
size="small" size={gtMobile ? 'small' : 'medium'}
label={_(msg`Confirm`)} label={_(msg`Confirm`)}
onPress={handleOnPress}> onPress={handleOnPress}>
{children} {children}
+7 -15
View File
@@ -5,7 +5,6 @@ import {HITSLOP_10} from 'lib/constants'
import { import {
useTheme, useTheme,
atoms as a, atoms as a,
web,
native, native,
flatten, flatten,
ViewStyleProp, ViewStyleProp,
@@ -228,13 +227,7 @@ export function Item({
onPressOut={onPressOut} onPressOut={onPressOut}
onFocus={onFocus} onFocus={onFocus}
onBlur={onBlur} onBlur={onBlur}
style={[ style={[a.flex_row, a.align_center, a.gap_sm, flatten(style)]}>
a.flex_row,
a.align_center,
a.gap_sm,
focused ? web({outline: 'none'}) : {},
flatten(style),
]}>
{typeof children === 'function' ? children(state) : children} {typeof children === 'function' ? children(state) : children}
</Pressable> </Pressable>
</ItemContext.Provider> </ItemContext.Provider>
@@ -271,7 +264,6 @@ export function Label({
export function createSharedToggleStyles({ export function createSharedToggleStyles({
theme: t, theme: t,
hovered, hovered,
focused,
selected, selected,
disabled, disabled,
isInvalid, isInvalid,
@@ -294,7 +286,7 @@ export function createSharedToggleStyles({
borderColor: t.palette.primary_500, borderColor: t.palette.primary_500,
}) })
if (hovered || focused) { if (hovered) {
baseHover.push({ baseHover.push({
backgroundColor: backgroundColor:
t.name === 'light' ? t.palette.primary_100 : t.palette.primary_800, t.name === 'light' ? t.palette.primary_100 : t.palette.primary_800,
@@ -303,7 +295,7 @@ export function createSharedToggleStyles({
}) })
} }
} else { } else {
if (hovered || focused) { if (hovered) {
baseHover.push({ baseHover.push({
backgroundColor: backgroundColor:
t.name === 'light' ? t.palette.contrast_50 : t.palette.contrast_100, t.name === 'light' ? t.palette.contrast_50 : t.palette.contrast_100,
@@ -320,7 +312,7 @@ export function createSharedToggleStyles({
t.name === 'light' ? t.palette.negative_300 : t.palette.negative_800, t.name === 'light' ? t.palette.negative_300 : t.palette.negative_800,
}) })
if (hovered || focused) { if (hovered) {
baseHover.push({ baseHover.push({
backgroundColor: backgroundColor:
t.name === 'light' ? t.palette.negative_25 : t.palette.negative_900, t.name === 'light' ? t.palette.negative_25 : t.palette.negative_900,
@@ -367,7 +359,7 @@ export function Checkbox() {
width: 20, width: 20,
}, },
baseStyles, baseStyles,
hovered || focused ? baseHoverStyles : {}, hovered ? baseHoverStyles : {},
]}> ]}>
{selected ? <Checkmark size="xs" fill={t.palette.primary_500} /> : null} {selected ? <Checkmark size="xs" fill={t.palette.primary_500} /> : null}
</View> </View>
@@ -399,7 +391,7 @@ export function Switch() {
width: 30, width: 30,
}, },
baseStyles, baseStyles,
hovered || focused ? baseHoverStyles : {}, hovered ? baseHoverStyles : {},
]}> ]}>
<View <View
style={[ style={[
@@ -451,7 +443,7 @@ export function Radio() {
width: 20, width: 20,
}, },
baseStyles, baseStyles,
hovered || focused ? baseHoverStyles : {}, hovered ? baseHoverStyles : {},
]}> ]}>
{selected ? ( {selected ? (
<View <View
+1 -1
View File
@@ -108,7 +108,7 @@ function ButtonInner({children}: React.PropsWithChildren<{}>) {
t.atoms.border_contrast_low, t.atoms.border_contrast_low,
baseStyles, baseStyles,
activeStyles, activeStyles,
(state.hovered || state.focused || state.pressed) && hoverStyles, (state.hovered || state.pressed) && hoverStyles,
]}> ]}>
{typeof children === 'string' ? ( {typeof children === 'string' ? (
<Text <Text
+5
View File
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const BubbleQuestion_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M5.002 17.036V5h14v12.036h-3.986a1 1 0 0 0-.639.23l-2.375 1.968-2.344-1.965a1 1 0 0 0-.643-.233H5.002ZM20.002 3h-16a1 1 0 0 0-1 1v14.036a1 1 0 0 0 1 1h4.65l2.704 2.266a1 1 0 0 0 1.28.004l2.74-2.27h4.626a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1Zm-7.878 3.663c-1.39 0-2.5 1.135-2.5 2.515a1 1 0 0 0 2 0c0-.294.232-.515.5-.515a.507.507 0 0 1 .489.6.174.174 0 0 1-.027.048 1.1 1.1 0 0 1-.267.226c-.508.345-1.128.923-1.286 1.978a1 1 0 1 0 1.978.297.762.762 0 0 1 .14-.359c.063-.086.155-.169.293-.262.436-.297 1.18-.885 1.18-2.013 0-1.38-1.11-2.515-2.5-2.515ZM12 15.75a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5Z',
})
+5
View File
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const SpeakerVolumeFull_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M12.472 3.118A1 1 0 0 1 13 4v16a1 1 0 0 1-1.555.832L5.697 17H2a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1h3.697l5.748-3.832a1 1 0 0 1 1.027-.05ZM11 5.868 6.555 8.833A1 1 0 0 1 6 9H3v6h3a1 1 0 0 1 .555.168L11 18.131V5.87Zm7.364-1.645a1 1 0 0 1 1.414 0A10.969 10.969 0 0 1 23 12c0 3.037-1.232 5.788-3.222 7.778a1 1 0 1 1-1.414-1.414A8.969 8.969 0 0 0 21 12a8.969 8.969 0 0 0-2.636-6.364 1 1 0 0 1 0-1.414Zm-3.182 3.181a1 1 0 0 1 1.414 0A6.483 6.483 0 0 1 18.5 12a6.483 6.483 0 0 1-1.904 4.597 1 1 0 0 1-1.414-1.415A4.483 4.483 0 0 0 16.5 12a4.483 4.483 0 0 0-1.318-3.182 1 1 0 0 1 0-1.414Z',
})
+5
View File
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Trash_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M7.416 5H3a1 1 0 0 0 0 2h1.064l.938 14.067A1 1 0 0 0 6 22h12a1 1 0 0 0 .998-.933L19.936 7H21a1 1 0 1 0 0-2h-4.416a5 5 0 0 0-9.168 0Zm2.348 0h4.472c-.55-.614-1.348-1-2.236-1-.888 0-1.687.386-2.236 1Zm6.087 2H6.07l.867 13h10.128l.867-13h-2.036a1 1 0 0 1-.044 0ZM10 10a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-5a1 1 0 0 1 1-1Zm4 0a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-5a1 1 0 0 1 1-1Z',
})
+7 -1
View File
@@ -2,9 +2,15 @@ import {RouteParams, Route} from './types'
export class Router { export class Router {
routes: [string, Route][] = [] routes: [string, Route][] = []
constructor(description: Record<string, string>) { constructor(description: Record<string, string | string[]>) {
for (const [screen, pattern] of Object.entries(description)) { for (const [screen, pattern] of Object.entries(description)) {
if (typeof pattern === 'string') {
this.routes.push([screen, createRoute(pattern)]) this.routes.push([screen, createRoute(pattern)])
} else {
pattern.forEach(subPattern => {
this.routes.push([screen, createRoute(subPattern)])
})
}
} }
} }
+1
View File
@@ -21,6 +21,7 @@ export type CommonNavigatorParams = {
PostRepostedBy: {name: string; rkey: string} PostRepostedBy: {name: string; rkey: string}
ProfileFeed: {name: string; rkey: string} ProfileFeed: {name: string; rkey: string}
ProfileFeedLikedBy: {name: string; rkey: string} ProfileFeedLikedBy: {name: string; rkey: string}
ProfileLabelerLikedBy: {name: string}
Debug: undefined Debug: undefined
DebugMod: undefined DebugMod: undefined
Log: undefined Log: undefined
+12
View File
@@ -48,6 +48,18 @@ export function Provider({children}: {children: React.ReactNode}) {
() => toStatsigUser(currentAccount?.did), () => toStatsigUser(currentAccount?.did),
[currentAccount?.did], [currentAccount?.did],
) )
React.useEffect(() => {
function refresh() {
// Intentionally refetching the config using the JS SDK rather than React SDK
// so that the new config is stored in cache but isn't used during this session.
// It will kick in for the next reload.
Statsig.updateUser(currentStatsigUser)
}
const id = setInterval(refresh, 3 * 60e3 /* 3 min */)
return () => clearInterval(id)
}, [currentStatsigUser])
return ( return (
<StatsigProvider <StatsigProvider
sdkKey="client-SXJakO39w9vIhl3D44u8UupyzFl4oZ2qPIkjwcvuPsV" sdkKey="client-SXJakO39w9vIhl3D44u8UupyzFl4oZ2qPIkjwcvuPsV"
+12
View File
@@ -48,6 +48,18 @@ export function Provider({children}: {children: React.ReactNode}) {
() => toStatsigUser(currentAccount?.did), () => toStatsigUser(currentAccount?.did),
[currentAccount?.did], [currentAccount?.did],
) )
React.useEffect(() => {
function refresh() {
// Intentionally refetching the config using the JS SDK rather than React SDK
// so that the new config is stored in cache but isn't used during this session.
// It will kick in for the next reload.
Statsig.updateUser(currentStatsigUser)
}
const id = setInterval(refresh, 3 * 60e3 /* 3 min */)
return () => clearInterval(id)
}, [currentStatsigUser])
return ( return (
<StatsigProvider <StatsigProvider
sdkKey="client-SXJakO39w9vIhl3D44u8UupyzFl4oZ2qPIkjwcvuPsV" sdkKey="client-SXJakO39w9vIhl3D44u8UupyzFl4oZ2qPIkjwcvuPsV"
+22 -2
View File
@@ -3,6 +3,8 @@ import {BSKY_SERVICE} from 'lib/constants'
import TLDs from 'tlds' import TLDs from 'tlds'
import psl from 'psl' import psl from 'psl'
export const BSKY_APP_HOST = 'https://bsky.app'
export function isValidDomain(str: string): boolean { export function isValidDomain(str: string): boolean {
return !!TLDs.find(tld => { return !!TLDs.find(tld => {
let i = str.lastIndexOf(tld) let i = str.lastIndexOf(tld)
@@ -67,8 +69,21 @@ export function isBskyAppUrl(url: string): boolean {
return url.startsWith('https://bsky.app/') return url.startsWith('https://bsky.app/')
} }
export function isRelativeUrl(url: string): boolean {
return /^\/[^/]/.test(url)
}
export function isBskyRSSUrl(url: string): boolean {
return (
(url.startsWith('https://bsky.app/') || isRelativeUrl(url)) &&
/\/rss\/?$/.test(url)
)
}
export function isExternalUrl(url: string): boolean { export function isExternalUrl(url: string): boolean {
return !isBskyAppUrl(url) && url.startsWith('http') const external = !isBskyAppUrl(url) && url.startsWith('http')
const rss = isBskyRSSUrl(url)
return external || rss
} }
export function isBskyPostUrl(url: string): boolean { export function isBskyPostUrl(url: string): boolean {
@@ -149,7 +164,7 @@ export function linkRequiresWarning(uri: string, label: string) {
const labelDomain = labelToDomain(label) const labelDomain = labelToDomain(label)
// If the uri started with a / we know it is internal. // If the uri started with a / we know it is internal.
if (uri.startsWith('/')) { if (isRelativeUrl(uri)) {
return false return false
} }
@@ -222,3 +237,8 @@ export function splitApexDomain(hostname: string): [string, string] {
hostnamep.domain, hostnamep.domain,
] ]
} }
export function createBskyAppAbsoluteUrl(path: string): string {
const sanitizedPath = path.replace(BSKY_APP_HOST, '').replace(/^\/+/, '')
return `${BSKY_APP_HOST.replace(/\/$/, '')}/${sanitizedPath}`
}
+2 -1
View File
@@ -12,7 +12,7 @@ export const router = new Router({
ModerationModlists: '/moderation/modlists', ModerationModlists: '/moderation/modlists',
ModerationMutedAccounts: '/moderation/muted-accounts', ModerationMutedAccounts: '/moderation/muted-accounts',
ModerationBlockedAccounts: '/moderation/blocked-accounts', ModerationBlockedAccounts: '/moderation/blocked-accounts',
Profile: '/profile/:name', Profile: ['/profile/:name', '/profile/:name/rss'],
ProfileFollowers: '/profile/:name/followers', ProfileFollowers: '/profile/:name/followers',
ProfileFollows: '/profile/:name/follows', ProfileFollows: '/profile/:name/follows',
ProfileList: '/profile/:name/lists/:rkey', ProfileList: '/profile/:name/lists/:rkey',
@@ -21,6 +21,7 @@ export const router = new Router({
PostRepostedBy: '/profile/:name/post/:rkey/reposted-by', PostRepostedBy: '/profile/:name/post/:rkey/reposted-by',
ProfileFeed: '/profile/:name/feed/:rkey', ProfileFeed: '/profile/:name/feed/:rkey',
ProfileFeedLikedBy: '/profile/:name/feed/:rkey/liked-by', ProfileFeedLikedBy: '/profile/:name/feed/:rkey/liked-by',
ProfileLabelerLikedBy: '/profile/:name/labeler/liked-by',
Debug: '/sys/debug', Debug: '/sys/debug',
DebugMod: '/sys/debug-mod', DebugMod: '/sys/debug-mod',
Log: '/sys/log', Log: '/sys/log',
@@ -35,10 +35,9 @@ import {
Heart2_Stroke2_Corner0_Rounded as Heart, Heart2_Stroke2_Corner0_Rounded as Heart,
Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled, Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled,
} from '#/components/icons/Heart2' } from '#/components/icons/Heart2'
import {LikesDialog} from '#/components/LikesDialog'
import * as Dialog from '#/components/Dialog'
import {DialogOuterProps} from '#/components/Dialog' import {DialogOuterProps} from '#/components/Dialog'
import * as Prompt from '#/components/Prompt' import * as Prompt from '#/components/Prompt'
import {Link} from '#/components/Link'
interface Props { interface Props {
profile: AppBskyActorDefs.ProfileViewDetailed profile: AppBskyActorDefs.ProfileViewDetailed
@@ -64,7 +63,6 @@ let ProfileHeaderLabeler = ({
const {currentAccount, hasSession} = useSession() const {currentAccount, hasSession} = useSession()
const {openModal} = useModalControls() const {openModal} = useModalControls()
const {track} = useAnalytics() const {track} = useAnalytics()
const likesControl = Dialog.useDialogControl()
const cantSubscribePrompt = Prompt.usePromptControl() const cantSubscribePrompt = Prompt.usePromptControl()
const moderation = useMemo( const moderation = useMemo(
@@ -86,9 +84,7 @@ let ProfileHeaderLabeler = ({
const [likeUri, setLikeUri] = React.useState<string>( const [likeUri, setLikeUri] = React.useState<string>(
labeler.viewer?.like || '', labeler.viewer?.like || '',
) )
const isLiked = !!likeUri const [likeCount, setLikeCount] = React.useState(labeler.likeCount || 0)
const likeCount =
isLiked && likeUri ? (labeler.likeCount || 0) + 1 : labeler.likeCount || 0
const onToggleLiked = React.useCallback(async () => { const onToggleLiked = React.useCallback(async () => {
if (!labeler) { if (!labeler) {
@@ -97,13 +93,15 @@ let ProfileHeaderLabeler = ({
try { try {
Haptics.default() Haptics.default()
if (isLiked && likeUri) { if (likeUri) {
await unlikeMod({uri: likeUri}) await unlikeMod({uri: likeUri})
track('CustomFeed:Unlike') track('CustomFeed:Unlike')
setLikeCount(c => c - 1)
setLikeUri('') setLikeUri('')
} else { } else {
const res = await likeMod({uri: labeler.uri, cid: labeler.cid}) const res = await likeMod({uri: labeler.uri, cid: labeler.cid})
track('CustomFeed:Like') track('CustomFeed:Like')
setLikeCount(c => c + 1)
setLikeUri(res.uri) setLikeUri(res.uri)
} }
} catch (e: any) { } catch (e: any) {
@@ -114,7 +112,7 @@ let ProfileHeaderLabeler = ({
) )
logger.error(`Failed to toggle labeler like`, {message: e.message}) logger.error(`Failed to toggle labeler like`, {message: e.message})
} }
}, [labeler, likeUri, isLiked, likeMod, unlikeMod, track, _]) }, [labeler, likeUri, likeMod, unlikeMod, track, _])
const onPressEditProfile = React.useCallback(() => { const onPressEditProfile = React.useCallback(() => {
track('ProfileHeader:EditProfileButtonClicked') track('ProfileHeader:EditProfileButtonClicked')
@@ -229,17 +227,22 @@ let ProfileHeaderLabeler = ({
label={_(msg`Like this feed`)} label={_(msg`Like this feed`)}
disabled={!hasSession || isLikePending || isUnlikePending} disabled={!hasSession || isLikePending || isUnlikePending}
onPress={onToggleLiked}> onPress={onToggleLiked}>
{isLiked ? ( {likeUri ? (
<HeartFilled fill={t.palette.negative_400} /> <HeartFilled fill={t.palette.negative_400} />
) : ( ) : (
<Heart fill={t.atoms.text_contrast_medium.color} /> <Heart fill={t.atoms.text_contrast_medium.color} />
)} )}
</Button> </Button>
{typeof labeler.likeCount === 'number' && ( {typeof likeCount === 'number' && (
<Button <Link
to={{
screen: 'ProfileLabelerLikedBy',
params: {
name: labeler.creator.handle || labeler.creator.did,
},
}}
size="tiny" size="tiny"
onPress={() => likesControl.open()}
label={_( label={_(
msg`Liked by ${likeCount} ${pluralize(likeCount, 'user')}`, msg`Liked by ${likeCount} ${pluralize(likeCount, 'user')}`,
)}> )}>
@@ -257,13 +260,12 @@ let ProfileHeaderLabeler = ({
</Trans> </Trans>
</Text> </Text>
)} )}
</Button> </Link>
)} )}
</View> </View>
</> </>
)} )}
</View> </View>
<LikesDialog control={likesControl} uri={labeler.uri} />
<CantSubscribePrompt control={cantSubscribePrompt} /> <CantSubscribePrompt control={cantSubscribePrompt} />
</ProfileHeaderShell> </ProfileHeaderShell>
) )
@@ -0,0 +1,50 @@
import React from 'react'
import {View} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {NativeStackScreenProps, CommonNavigatorParams} from '#/lib/routes/types'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {LikedByList} from '#/components/LikedByList'
import {useSetMinimalShellMode} from '#/state/shell'
import {makeRecordUri} from '#/lib/strings/url-helpers'
import {atoms as a, useTheme, useBreakpoints} from '#/alf'
export function ProfileLabelerLikedByScreen({
route,
}: NativeStackScreenProps<CommonNavigatorParams, 'ProfileLabelerLikedBy'>) {
const setMinimalShellMode = useSetMinimalShellMode()
const {name: handleOrDid} = route.params
const uri = makeRecordUri(handleOrDid, 'app.bsky.labeler.service', 'self')
const {_} = useLingui()
const t = useTheme()
const {gtMobile} = useBreakpoints()
useFocusEffect(
React.useCallback(() => {
setMinimalShellMode(false)
}, [setMinimalShellMode]),
)
return (
<View
style={[
a.mx_auto,
a.w_full,
a.h_full_vh,
gtMobile && [
a.border_l,
a.border_r,
t.atoms.border_contrast_low,
{
maxWidth: 600,
},
],
]}>
<ViewHeader title={_(msg`Liked By`)} />
<LikedByList uri={uri} />
</View>
)
}
+41 -19
View File
@@ -1,8 +1,9 @@
import React from 'react' import React from 'react'
import {SharedValue, useSharedValue} from 'react-native-reanimated'
import {DialogControlRefProps} from '#/components/Dialog' import {DialogControlRefProps} from '#/components/Dialog'
import {Provider as GlobalDialogsProvider} from '#/components/dialogs/Context' import {Provider as GlobalDialogsProvider} from '#/components/dialogs/Context'
const DialogContext = React.createContext<{ interface IDialogContext {
/** /**
* The currently active `useDialogControl` hooks. * The currently active `useDialogControl` hooks.
*/ */
@@ -13,13 +14,18 @@ const DialogContext = React.createContext<{
* The currently open dialogs, referenced by their IDs, generated from * The currently open dialogs, referenced by their IDs, generated from
* `useId`. * `useId`.
*/ */
openDialogs: string[] openDialogs: React.MutableRefObject<Set<string>>
}>({ /**
activeDialogs: { * The counterpart to `accessibilityViewIsModal` for Android. This property
current: new Map(), * applies to the parent of all non-modal views, and prevents TalkBack from
}, * navigating within content beneath an open dialog.
openDialogs: [], *
}) * @see https://reactnative.dev/docs/accessibility#importantforaccessibility-android
*/
importantForAccessibility: SharedValue<'auto' | 'no-hide-descendants'>
}
const DialogContext = React.createContext<IDialogContext>({} as IDialogContext)
const DialogControlContext = React.createContext<{ const DialogControlContext = React.createContext<{
closeAllDialogs(): boolean closeAllDialogs(): boolean
@@ -41,26 +47,42 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
const activeDialogs = React.useRef< const activeDialogs = React.useRef<
Map<string, React.MutableRefObject<DialogControlRefProps>> Map<string, React.MutableRefObject<DialogControlRefProps>>
>(new Map()) >(new Map())
const [openDialogs, setOpenDialogs] = React.useState<string[]>([]) const openDialogs = React.useRef<Set<string>>(new Set())
const importantForAccessibility = useSharedValue<
'auto' | 'no-hide-descendants'
>('auto')
const closeAllDialogs = React.useCallback(() => { const closeAllDialogs = React.useCallback(() => {
activeDialogs.current.forEach(dialog => dialog.current.close()) activeDialogs.current.forEach(dialog => dialog.current.close())
return openDialogs.length > 0 return openDialogs.current.size > 0
}, [openDialogs]) }, [])
const setDialogIsOpen = React.useCallback( const setDialogIsOpen = React.useCallback(
(id: string, isOpen: boolean) => { (id: string, isOpen: boolean) => {
setOpenDialogs(prev => { if (isOpen) {
const filtered = prev.filter(dialogId => dialogId !== id) as string[] openDialogs.current.add(id)
return isOpen ? [...filtered, id] : filtered importantForAccessibility.value = 'no-hide-descendants'
}) } else {
openDialogs.current.delete(id)
if (openDialogs.current.size < 1) {
importantForAccessibility.value = 'auto'
}
}
}, },
[setOpenDialogs], [importantForAccessibility],
) )
const context = React.useMemo( const context = React.useMemo<IDialogContext>(
() => ({activeDialogs, openDialogs}), () => ({
[openDialogs], activeDialogs: {
current: new Map(),
},
openDialogs: {
current: new Set(),
},
importantForAccessibility,
}),
[importantForAccessibility],
) )
const controls = React.useMemo( const controls = React.useMemo(
() => ({closeAllDialogs, setDialogIsOpen}), () => ({closeAllDialogs, setDialogIsOpen}),
+9
View File
@@ -5,6 +5,11 @@ import * as WebBrowser from 'expo-web-browser'
import {isNative} from '#/platform/detection' import {isNative} from '#/platform/detection'
import {useModalControls} from '../modals' import {useModalControls} from '../modals'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {
isBskyRSSUrl,
isRelativeUrl,
createBskyAppAbsoluteUrl,
} from 'lib/strings/url-helpers'
type StateContext = persisted.Schema['useInAppBrowser'] type StateContext = persisted.Schema['useInAppBrowser']
type SetContext = (v: persisted.Schema['useInAppBrowser']) => void type SetContext = (v: persisted.Schema['useInAppBrowser']) => void
@@ -57,6 +62,10 @@ export function useOpenLink() {
const openLink = React.useCallback( const openLink = React.useCallback(
(url: string, override?: boolean) => { (url: string, override?: boolean) => {
if (isBskyRSSUrl(url) && isRelativeUrl(url)) {
url = createBskyAppAbsoluteUrl(url)
}
if (isNative && !url.startsWith('mailto:')) { if (isNative && !url.startsWith('mailto:')) {
if (override === undefined && enabled === undefined) { if (override === undefined && enabled === undefined) {
openModal({ openModal({
+1 -1
View File
@@ -157,7 +157,7 @@ export function Step1({
</View> </View>
)} )}
{uiState.inviteCode ? ( {!uiState.isInviteCodeRequired || uiState.inviteCode ? (
<> <>
<View style={s.pb20}> <View style={s.pb20}>
<Text <Text
+9 -2
View File
@@ -8,7 +8,14 @@ import {View, ViewStyle} from 'react-native'
export function EventStopper({ export function EventStopper({
children, children,
style, style,
}: React.PropsWithChildren<{style?: ViewStyle | ViewStyle[]}>) { onKeyDown = true,
}: React.PropsWithChildren<{
style?: ViewStyle | ViewStyle[]
/**
* Default `true`. Set to `false` to allow onKeyDown to propagate
*/
onKeyDown?: boolean
}>) {
const stop = (e: any) => { const stop = (e: any) => {
e.stopPropagation() e.stopPropagation()
} }
@@ -18,7 +25,7 @@ export function EventStopper({
onTouchEnd={stop} onTouchEnd={stop}
// @ts-ignore web only -prf // @ts-ignore web only -prf
onClick={stop} onClick={stop}
onKeyDown={stop} onKeyDown={onKeyDown ? stop : undefined}
style={style}> style={style}>
{children} {children}
</View> </View>
+178 -157
View File
@@ -1,5 +1,11 @@
import React, {memo} from 'react' import React, {memo} from 'react'
import {StyleProp, View, ViewStyle} from 'react-native' import {
StyleProp,
ViewStyle,
Pressable,
View,
PressableProps,
} from 'react-native'
import Clipboard from '@react-native-clipboard/clipboard' import Clipboard from '@react-native-clipboard/clipboard'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {useNavigation} from '@react-navigation/native' import {useNavigation} from '@react-navigation/native'
@@ -12,10 +18,6 @@ import {
import {toShareUrl} from 'lib/strings/url-helpers' import {toShareUrl} from 'lib/strings/url-helpers'
import {useTheme} from 'lib/ThemeContext' import {useTheme} from 'lib/ThemeContext'
import {shareUrl} from 'lib/sharing' import {shareUrl} from 'lib/sharing'
import {
NativeDropdown,
DropdownItem as NativeDropdownItem,
} from './NativeDropdown'
import * as Toast from '../Toast' import * as Toast from '../Toast'
import {EventStopper} from '../EventStopper' import {EventStopper} from '../EventStopper'
import {useModalControls} from '#/state/modals' import {useModalControls} from '#/state/modals'
@@ -35,9 +37,20 @@ import {useSession} from '#/state/session'
import {isWeb} from '#/platform/detection' import {isWeb} from '#/platform/detection'
import {richTextToString} from '#/lib/strings/rich-text-helpers' import {richTextToString} from '#/lib/strings/rich-text-helpers'
import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog' import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
import {NEW_REPORT_DIALOG_ENABLED} from '#/lib/build-flags'
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
import {atoms as a, useTheme as useAlf, web} from '#/alf'
import * as Menu from '#/components/Menu'
import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard'
import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter'
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble'
import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
let PostDropdownBtn = ({ let PostDropdownBtn = ({
testID, testID,
postAuthor, postAuthor,
@@ -46,6 +59,7 @@ let PostDropdownBtn = ({
record, record,
richText, richText,
style, style,
hitSlop,
}: { }: {
testID: string testID: string
postAuthor: AppBskyActorDefs.ProfileViewBasic postAuthor: AppBskyActorDefs.ProfileViewBasic
@@ -54,9 +68,11 @@ let PostDropdownBtn = ({
record: AppBskyFeedPost.Record record: AppBskyFeedPost.Record
richText: RichTextAPI richText: RichTextAPI
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
hitSlop?: PressableProps['hitSlop']
}): React.ReactNode => { }): React.ReactNode => {
const {hasSession, currentAccount} = useSession() const {hasSession, currentAccount} = useSession()
const theme = useTheme() const theme = useTheme()
const alf = useAlf()
const {_} = useLingui() const {_} = useLingui()
const defaultCtrlColor = theme.palette.default.postCtrl const defaultCtrlColor = theme.palette.default.postCtrl
const {openModal} = useModalControls() const {openModal} = useModalControls()
@@ -152,149 +168,6 @@ let PostDropdownBtn = ({
hidePost({uri: postUri}) hidePost({uri: postUri})
}, [postUri, hidePost]) }, [postUri, hidePost])
const dropdownItems: NativeDropdownItem[] = [
{
label: _(msg`Translate`),
onPress() {
onOpenTranslate()
},
testID: 'postDropdownTranslateBtn',
icon: {
ios: {
name: 'character.book.closed',
},
android: 'ic_menu_sort_alphabetically',
web: 'language',
},
},
{
label: _(msg`Copy post text`),
onPress() {
onCopyPostText()
},
testID: 'postDropdownCopyTextBtn',
icon: {
ios: {
name: 'doc.on.doc',
},
android: 'ic_menu_edit',
web: ['far', 'paste'],
},
},
{
label: isWeb ? _(msg`Copy link to post`) : _(msg`Share`),
onPress() {
const url = toShareUrl(href)
shareUrl(url)
},
testID: 'postDropdownShareBtn',
icon: {
ios: {
name: 'square.and.arrow.up',
},
android: 'ic_menu_share',
web: 'share',
},
},
hasSession && {
label: 'separator',
},
hasSession && {
label: isThreadMuted ? _(msg`Unmute thread`) : _(msg`Mute thread`),
onPress() {
onToggleThreadMute()
},
testID: 'postDropdownMuteThreadBtn',
icon: {
ios: {
name: 'speaker.slash',
},
android: 'ic_lock_silent_mode',
web: 'comment-slash',
},
},
hasSession && {
label: _(msg`Mute words & tags`),
onPress() {
mutedWordsDialogControl.open()
},
testID: 'postDropdownMuteWordsBtn',
icon: {
ios: {
name: 'speaker.slash',
},
android: 'ic_lock_silent_mode',
web: 'filter',
},
},
hasSession &&
!isAuthor &&
!isPostHidden && {
label: _(msg`Hide post`),
onPress() {
openModal({
name: 'confirm',
title: _(msg`Hide this post?`),
message: _(msg`This will hide this post from your feeds.`),
onPressConfirm: onHidePost,
})
},
testID: 'postDropdownHideBtn',
icon: {
ios: {
name: 'eye.slash',
},
android: 'ic_menu_delete',
web: ['far', 'eye-slash'],
},
},
{
label: 'separator',
},
!isAuthor &&
hasSession && {
label: _(msg`Report post`),
onPress() {
if (NEW_REPORT_DIALOG_ENABLED) {
control.open()
} else {
openModal({
name: 'report',
uri: postUri,
cid: postCid,
})
}
},
testID: 'postDropdownReportBtn',
icon: {
ios: {
name: 'exclamationmark.triangle',
},
android: 'ic_menu_report_image',
web: 'circle-exclamation',
},
},
isAuthor && {
label: _(msg`Delete post`),
onPress() {
openModal({
name: 'confirm',
title: _(msg`Delete this post?`),
message: _(msg`Are you sure? This cannot be undone.`),
onPressConfirm: onDeletePost,
})
},
testID: 'postDropdownDeleteBtn',
icon: {
ios: {
name: 'trash',
},
android: 'ic_menu_delete',
web: ['far', 'trash-can'],
},
},
].filter(Boolean) as NativeDropdownItem[]
return ( return (
<> <>
<ReportDialog <ReportDialog
@@ -306,20 +179,168 @@ let PostDropdownBtn = ({
}} }}
/> />
<EventStopper> <EventStopper onKeyDown={false}>
<NativeDropdown <Menu.Root>
testID={testID} <Menu.Trigger label={_(msg`Open post options menu`)}>
items={dropdownItems} {({props, state}) => {
accessibilityLabel={_(msg`More post options`)} const styles = [
accessibilityHint=""> style,
<View style={style}> a.rounded_full,
(state.hovered || state.focused || state.pressed) && [
web({outline: 0}),
alf.atoms.bg_contrast_25,
],
]
return isWeb ? (
<View {...props} testID={testID} style={styles}>
<FontAwesomeIcon <FontAwesomeIcon
icon="ellipsis" icon="ellipsis"
size={20} size={20}
color={defaultCtrlColor} color={defaultCtrlColor}
style={{pointerEvents: 'none'}}
/> />
</View> </View>
</NativeDropdown> ) : (
<Pressable
{...props}
hitSlop={hitSlop}
testID={testID}
style={styles}>
<FontAwesomeIcon
icon="ellipsis"
size={20}
color={defaultCtrlColor}
style={{pointerEvents: 'none'}}
/>
</Pressable>
)
}}
</Menu.Trigger>
<Menu.Outer>
<Menu.Group>
<Menu.Item
testID="postDropdownTranslateBtn"
label={_(msg`Translate`)}
onPress={onOpenTranslate}>
<Menu.ItemText>{_(msg`Translate`)}</Menu.ItemText>
<Menu.ItemIcon icon={Translate} position="right" />
</Menu.Item>
<Menu.Item
testID="postDropdownCopyTextBtn"
label={_(msg`Copy post text`)}
onPress={onCopyPostText}>
<Menu.ItemText>{_(msg`Copy post text`)}</Menu.ItemText>
<Menu.ItemIcon icon={ClipboardIcon} position="right" />
</Menu.Item>
<Menu.Item
testID="postDropdownShareBtn"
label={isWeb ? _(msg`Copy link to post`) : _(msg`Share`)}
onPress={() => {
const url = toShareUrl(href)
shareUrl(url)
}}>
<Menu.ItemText>
{isWeb ? _(msg`Copy link to post`) : _(msg`Share`)}
</Menu.ItemText>
<Menu.ItemIcon icon={Share} position="right" />
</Menu.Item>
</Menu.Group>
{hasSession && (
<>
<Menu.Divider />
<Menu.Group>
<Menu.Item
testID="postDropdownMuteThreadBtn"
label={
isThreadMuted
? _(msg`Unmute thread`)
: _(msg`Mute thread`)
}
onPress={onToggleThreadMute}>
<Menu.ItemText>
{isThreadMuted
? _(msg`Unmute thread`)
: _(msg`Mute thread`)}
</Menu.ItemText>
<Menu.ItemIcon
icon={isThreadMuted ? Unmute : Mute}
position="right"
/>
</Menu.Item>
<Menu.Item
testID="postDropdownMuteWordsBtn"
label={_(msg`Mute words & tags`)}
onPress={() => mutedWordsDialogControl.open()}>
<Menu.ItemText>{_(msg`Mute words & tags`)}</Menu.ItemText>
<Menu.ItemIcon icon={Filter} position="right" />
</Menu.Item>
{!isAuthor && !isPostHidden && (
<Menu.Item
testID="postDropdownHideBtn"
label={_(msg`Hide post`)}
onPress={() => {
openModal({
name: 'confirm',
title: _(msg`Hide this post?`),
message: _(
msg`This will hide this post from your feeds.`,
),
onPressConfirm: onHidePost,
})
}}>
<Menu.ItemText>{_(msg`Hide post`)}</Menu.ItemText>
<Menu.ItemIcon icon={EyeSlash} position="right" />
</Menu.Item>
)}
</Menu.Group>
</>
)}
<Menu.Divider />
<Menu.Group>
{!isAuthor && (
<Menu.Item
testID="postDropdownReportBtn"
label={_(msg`Report post`)}
onPress={() => {
openModal({
name: 'report',
uri: postUri,
cid: postCid,
})
}}>
<Menu.ItemText>{_(msg`Report post`)}</Menu.ItemText>
<Menu.ItemIcon icon={Warning} position="right" />
</Menu.Item>
)}
{isAuthor && (
<Menu.Item
testID="postDropdownDeleteBtn"
label={_(msg`Delete post`)}
onPress={() => {
openModal({
name: 'confirm',
title: _(msg`Delete this post?`),
message: _(msg`Are you sure? This cannot be undone.`),
onPressConfirm: onDeletePost,
})
}}>
<Menu.ItemText>{_(msg`Delete post`)}</Menu.ItemText>
<Menu.ItemIcon icon={Trash} position="right" />
</Menu.Item>
)}
</Menu.Group>
</Menu.Outer>
</Menu.Root>
</EventStopper> </EventStopper>
</> </>
) )
+2 -3
View File
@@ -210,9 +210,7 @@ let PostCtrls = ({
style={[styles.btn]} style={[styles.btn]}
onPress={onShare} onPress={onShare}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={`${ accessibilityLabel={`${_(msg`Share`)}`}
post.viewer?.like ? _(msg`Unlike`) : _(msg`Like`)
} (${post.likeCount} ${pluralize(post.likeCount || 0, 'like')})`}
accessibilityHint="" accessibilityHint=""
hitSlop={big ? HITSLOP_20 : HITSLOP_10}> hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
<ArrowOutOfBox style={[defaultCtrlColor, styles.mt1]} width={22} /> <ArrowOutOfBox style={[defaultCtrlColor, styles.mt1]} width={22} />
@@ -228,6 +226,7 @@ let PostCtrls = ({
record={record} record={record}
richText={richText} richText={richText}
style={styles.btnPad} style={styles.btnPad}
hitSlop={big ? HITSLOP_20 : HITSLOP_10}
/> />
</View> </View>
</View> </View>
+5 -14
View File
@@ -30,7 +30,8 @@ import {useCloseAnyActiveElement} from '#/state/util'
import * as notifications from 'lib/notifications/notifications' import * as notifications from 'lib/notifications/notifications'
import {Outlet as PortalOutlet} from '#/components/Portal' import {Outlet as PortalOutlet} from '#/components/Portal'
import {MutedWordsDialog} from '#/components/dialogs/MutedWords' import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
import {useDialogStateContext} from '#/state/dialogs' import {useDialogStateContext} from 'state/dialogs'
import Animated from 'react-native-reanimated'
function ShellInner() { function ShellInner() {
const isDrawerOpen = useIsDrawerOpen() const isDrawerOpen = useIsDrawerOpen()
@@ -54,9 +55,9 @@ function ShellInner() {
const canGoBack = useNavigationState(state => !isStateAtTabRoot(state)) const canGoBack = useNavigationState(state => !isStateAtTabRoot(state))
const {hasSession, currentAccount} = useSession() const {hasSession, currentAccount} = useSession()
const closeAnyActiveElement = useCloseAnyActiveElement() const closeAnyActiveElement = useCloseAnyActiveElement()
const {importantForAccessibility} = useDialogStateContext()
// start undefined // start undefined
const currentAccountDid = React.useRef<string | undefined>(undefined) const currentAccountDid = React.useRef<string | undefined>(undefined)
const {openDialogs} = useDialogStateContext()
React.useEffect(() => { React.useEffect(() => {
let listener = {remove() {}} let listener = {remove() {}}
@@ -80,19 +81,9 @@ function ShellInner() {
} }
}, [currentAccount]) }, [currentAccount])
/**
* The counterpart to `accessibilityViewIsModal` for Android. This property
* applies to the parent of all non-modal views, and prevents TalkBack from
* navigating within content beneath an open dialog.
*
* @see https://reactnative.dev/docs/accessibility#importantforaccessibility-android
*/
const importantForAccessibility =
openDialogs.length > 0 ? 'no-hide-descendants' : undefined
return ( return (
<> <>
<View <Animated.View
style={containerPadding} style={containerPadding}
importantForAccessibility={importantForAccessibility}> importantForAccessibility={importantForAccessibility}>
<ErrorBoundary> <ErrorBoundary>
@@ -106,7 +97,7 @@ function ShellInner() {
<TabsNavigator /> <TabsNavigator />
</Drawer> </Drawer>
</ErrorBoundary> </ErrorBoundary>
</View> </Animated.View>
<Composer winHeight={winDim.height} /> <Composer winHeight={winDim.height} />
<ModalsContainer /> <ModalsContainer />
<MutedWordsDialog /> <MutedWordsDialog />
+3
View File
@@ -47,6 +47,9 @@
height: calc(100% + env(safe-area-inset-top)); height: calc(100% + env(safe-area-inset-top));
scrollbar-gutter: stable both-edges; scrollbar-gutter: stable both-edges;
} }
html, body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
/* Buttons and inputs have a font set by UA, so we'll have to reset that */ /* Buttons and inputs have a font set by UA, so we'll have to reset that */
button, input, textarea { button, input, textarea {