Merge branch 'main' into 3p-moderators
@@ -40,6 +40,8 @@ module.exports = function (config) {
|
|||||||
? process.env.BSKY_ANDROID_VERSION_CODE
|
? process.env.BSKY_ANDROID_VERSION_CODE
|
||||||
: process.env.BSKY_IOS_BUILD_NUMBER
|
: process.env.BSKY_IOS_BUILD_NUMBER
|
||||||
|
|
||||||
|
const IS_DEV = process.env.EXPO_PUBLIC_ENV === 'development'
|
||||||
|
|
||||||
return {
|
return {
|
||||||
expo: {
|
expo: {
|
||||||
version: VERSION,
|
version: VERSION,
|
||||||
@@ -103,7 +105,7 @@ module.exports = function (config) {
|
|||||||
scheme: 'https',
|
scheme: 'https',
|
||||||
host: 'bsky.app',
|
host: 'bsky.app',
|
||||||
},
|
},
|
||||||
{
|
IS_DEV && {
|
||||||
scheme: 'http',
|
scheme: 'http',
|
||||||
host: 'localhost:19006',
|
host: 'localhost:19006',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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="M8.371 3.89A2 2 0 0 1 10.035 3h3.93a2 2 0 0 1 1.664.89L17.035 6H20a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2.965L8.37 3.89ZM12 9a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7Z" clip-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 330 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="M8.371 3.89A2 2 0 0 1 10.035 3h3.93a2 2 0 0 1 1.664.89L17.035 6H20a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2.965L8.37 3.89ZM13.965 5h-3.93L8.63 7.11A2 2 0 0 1 6.965 8H4v11h16V8h-2.965a2 2 0 0 1-1.664-.89L13.965 5ZM12 11a2 2 0 1 0 0 4 2 2 0 0 0 0-4Zm-4 2a4 4 0 1 1 8 0 4 4 0 0 1-8 0Z" clip-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 447 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="M4 4a2 2 0 0 1 2-2h13.131c1.598 0 2.55 1.78 1.665 3.11L18.202 9l2.594 3.89c.886 1.33-.067 3.11-1.665 3.11H6v5a1 1 0 1 1-2 0V4Zm2 10h13.131l-2.593-3.89a2 2 0 0 1 0-2.22L19.13 4H6v10Z" clip-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 323 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none"><path fill="#000" fill-rule="evenodd" d="M10 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM5.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM16 11a1 1 0 0 1 1-1h5a1 1 0 1 1 0 2h-5a1 1 0 0 1-1-1ZM3.678 19h12.644c-.71-2.909-3.092-5-6.322-5s-5.613 2.091-6.322 5Zm-2.174.906C1.917 15.521 5.242 12 10 12c4.758 0 8.083 3.521 8.496 7.906A1 1 0 0 1 17.5 21h-15a1 1 0 0 1-.996-1.094Z" clip-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 466 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 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.679 19c.709-2.902 3.079-5 6.321-5a6.69 6.69 0 0 1 2.612.51 1 1 0 0 0 .776-1.844A8.687 8.687 0 0 0 12 12c-4.3 0-7.447 2.884-8.304 6.696-.29 1.29.767 2.304 1.902 2.304H11a1 1 0 1 0 0-2H5.679Zm14.835-4.857a1 1 0 0 1 .344 1.371l-3 5a1 1 0 0 1-1.458.286l-2-1.5a1 1 0 0 1 1.2-1.6l1.113.835 2.43-4.05a1 1 0 0 1 1.372-.342Z" clip-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 547 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 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.679 19c.709-2.902 3.079-5 6.321-5 .302 0 .595.018.878.053a1 1 0 0 0 .243-1.985A9.235 9.235 0 0 0 12 12c-4.3 0-7.447 2.884-8.304 6.696-.29 1.29.767 2.304 1.902 2.304H12a1 1 0 1 0 0-2H5.679Zm9.614-3.707a1 1 0 0 1 1.414 0L18 16.586l1.293-1.293a1 1 0 0 1 1.414 1.414L19.414 18l1.293 1.293a1 1 0 0 1-1.414 1.414L18 19.414l-1.293 1.293a1 1 0 0 1-1.414-1.414L16.586 18l-1.293-1.293a1 1 0 0 1 0-1.414Z" clip-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 625 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="M4 4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H4Zm8 12.5c1.253 0 2.197.609 2.674 1.5H9.326c.477-.891 1.42-1.5 2.674-1.5Zm0-2c2.404 0 4.235 1.475 4.822 3.5H20V6H4v12h3.178c.587-2.025 2.418-3.5 4.822-3.5Zm-1.25-3.75a1.25 1.25 0 1 1 2.5 0 1.25 1.25 0 0 1-2.5 0ZM12 7.5a3.25 3.25 0 1 0 0 6.5 3.25 3.25 0 0 0 0-6.5Zm5.75 2a1.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: 527 B |
@@ -181,7 +181,6 @@
|
|||||||
"react-responsive": "^9.0.2",
|
"react-responsive": "^9.0.2",
|
||||||
"rn-fetch-blob": "^0.12.0",
|
"rn-fetch-blob": "^0.12.0",
|
||||||
"sentry-expo": "~7.0.1",
|
"sentry-expo": "~7.0.1",
|
||||||
"statsig-react": "^1.36.0",
|
|
||||||
"statsig-react-native-expo": "^4.6.1",
|
"statsig-react-native-expo": "^4.6.1",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"tlds": "^1.234.0",
|
"tlds": "^1.234.0",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import {Platform} from 'react-native'
|
||||||
import {web, native} from '#/alf/util/platform'
|
import {web, native} from '#/alf/util/platform'
|
||||||
import * as tokens from '#/alf/tokens'
|
import * as tokens from '#/alf/tokens'
|
||||||
|
|
||||||
@@ -6,7 +7,7 @@ export const atoms = {
|
|||||||
* Positioning
|
* Positioning
|
||||||
*/
|
*/
|
||||||
fixed: {
|
fixed: {
|
||||||
position: 'fixed',
|
position: Platform.select({web: 'fixed', native: 'absolute'}) as 'absolute',
|
||||||
},
|
},
|
||||||
absolute: {
|
absolute: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -114,6 +115,12 @@ export const atoms = {
|
|||||||
flex_row: {
|
flex_row: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
},
|
},
|
||||||
|
flex_col_reverse: {
|
||||||
|
flexDirection: 'column-reverse',
|
||||||
|
},
|
||||||
|
flex_row_reverse: {
|
||||||
|
flexDirection: 'row-reverse',
|
||||||
|
},
|
||||||
flex_wrap: {
|
flex_wrap: {
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ export function Outer({
|
|||||||
control,
|
control,
|
||||||
onClose,
|
onClose,
|
||||||
nativeOptions,
|
nativeOptions,
|
||||||
|
testID,
|
||||||
}: React.PropsWithChildren<DialogOuterProps>) {
|
}: React.PropsWithChildren<DialogOuterProps>) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const sheet = React.useRef<BottomSheet>(null)
|
const sheet = React.useRef<BottomSheet>(null)
|
||||||
@@ -145,7 +146,8 @@ export function Outer({
|
|||||||
accessibilityViewIsModal
|
accessibilityViewIsModal
|
||||||
// Android
|
// Android
|
||||||
importantForAccessibility="yes"
|
importantForAccessibility="yes"
|
||||||
style={[a.absolute, a.inset_0]}>
|
style={[a.absolute, a.inset_0]}
|
||||||
|
testID={testID}>
|
||||||
<BottomSheet
|
<BottomSheet
|
||||||
enableDynamicSizing={!hasSnapPoints}
|
enableDynamicSizing={!hasSnapPoints}
|
||||||
enablePanDownToClose
|
enablePanDownToClose
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export type DialogOuterProps = {
|
|||||||
sheet?: Omit<BottomSheetProps, 'children'>
|
sheet?: Omit<BottomSheetProps, 'children'>
|
||||||
}
|
}
|
||||||
webOptions?: {}
|
webOptions?: {}
|
||||||
|
testID?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type DialogInnerPropsBase<T> = React.PropsWithChildren<ViewStyleProp> & T
|
type DialogInnerPropsBase<T> = React.PropsWithChildren<ViewStyleProp> & T
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {GestureResponderEvent} from 'react-native'
|
import {GestureResponderEvent} from 'react-native'
|
||||||
import {
|
import {useLinkProps, StackActions} from '@react-navigation/native'
|
||||||
useLinkProps,
|
|
||||||
useNavigation,
|
|
||||||
StackActions,
|
|
||||||
} from '@react-navigation/native'
|
|
||||||
import {sanitizeUrl} from '@braintree/sanitize-url'
|
import {sanitizeUrl} from '@braintree/sanitize-url'
|
||||||
|
|
||||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||||
import {isWeb} from '#/platform/detection'
|
import {isWeb} from '#/platform/detection'
|
||||||
import {useTheme, web, flatten, TextStyleProp, atoms as a} from '#/alf'
|
import {useTheme, web, flatten, TextStyleProp, atoms as a} from '#/alf'
|
||||||
import {Button, ButtonProps} from '#/components/Button'
|
import {Button, ButtonProps} from '#/components/Button'
|
||||||
import {AllNavigatorParams, NavigationProp} from '#/lib/routes/types'
|
import {AllNavigatorParams} from '#/lib/routes/types'
|
||||||
import {
|
import {
|
||||||
convertBskyAppUrlIfNeeded,
|
convertBskyAppUrlIfNeeded,
|
||||||
isExternalUrl,
|
isExternalUrl,
|
||||||
@@ -21,6 +17,7 @@ import {useModalControls} from '#/state/modals'
|
|||||||
import {router} from '#/routes'
|
import {router} from '#/routes'
|
||||||
import {Text, TextProps} from '#/components/Typography'
|
import {Text, TextProps} from '#/components/Typography'
|
||||||
import {useOpenLink} from 'state/preferences/in-app-browser'
|
import {useOpenLink} from 'state/preferences/in-app-browser'
|
||||||
|
import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Only available within a `Link`, since that inherits from `Button`.
|
* Only available within a `Link`, since that inherits from `Button`.
|
||||||
@@ -74,7 +71,7 @@ export function useLink({
|
|||||||
}: BaseLinkProps & {
|
}: BaseLinkProps & {
|
||||||
displayText: string
|
displayText: string
|
||||||
}) {
|
}) {
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigationDeduped()
|
||||||
const {href} = useLinkProps<AllNavigatorParams>({
|
const {href} = useLinkProps<AllNavigatorParams>({
|
||||||
to:
|
to:
|
||||||
typeof to === 'string' ? convertBskyAppUrlIfNeeded(sanitizeUrl(to)) : to,
|
typeof to === 'string' ? convertBskyAppUrlIfNeeded(sanitizeUrl(to)) : to,
|
||||||
@@ -231,6 +228,7 @@ export function InlineLink({
|
|||||||
onPress: outerOnPress,
|
onPress: outerOnPress,
|
||||||
download,
|
download,
|
||||||
selectable,
|
selectable,
|
||||||
|
label,
|
||||||
...rest
|
...rest
|
||||||
}: InlineLinkProps) {
|
}: InlineLinkProps) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
@@ -258,7 +256,8 @@ export function InlineLink({
|
|||||||
return (
|
return (
|
||||||
<Text
|
<Text
|
||||||
selectable={selectable}
|
selectable={selectable}
|
||||||
label={href}
|
accessibilityHint=""
|
||||||
|
accessibilityLabel={label || href}
|
||||||
{...rest}
|
{...rest}
|
||||||
style={[
|
style={[
|
||||||
{color: t.palette.primary_500},
|
{color: t.palette.primary_500},
|
||||||
|
|||||||
@@ -8,9 +8,8 @@ import {cleanError} from 'lib/strings/errors'
|
|||||||
import {Button} from '#/components/Button'
|
import {Button} from '#/components/Button'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {StackActions} from '@react-navigation/native'
|
import {StackActions} from '@react-navigation/native'
|
||||||
import {useNavigation} from '@react-navigation/core'
|
|
||||||
import {NavigationProp} from 'lib/routes/types'
|
|
||||||
import {router} from '#/routes'
|
import {router} from '#/routes'
|
||||||
|
import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped'
|
||||||
|
|
||||||
export function ListFooter({
|
export function ListFooter({
|
||||||
isFetching,
|
isFetching,
|
||||||
@@ -142,7 +141,7 @@ export function ListMaybePlaceholder({
|
|||||||
notFoundType?: 'page' | 'results'
|
notFoundType?: 'page' | 'results'
|
||||||
onRetry?: () => Promise<unknown>
|
onRetry?: () => Promise<unknown>
|
||||||
}) {
|
}) {
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigationDeduped()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {gtMobile, gtTablet} = useBreakpoints()
|
const {gtMobile, gtTablet} = useBreakpoints()
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View, Pressable} from 'react-native'
|
import {View, Pressable, ViewStyle, StyleProp} from 'react-native'
|
||||||
import flattenReactChildren from 'react-keyed-flatten-children'
|
import flattenReactChildren from 'react-keyed-flatten-children'
|
||||||
|
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
@@ -16,6 +16,10 @@ import {
|
|||||||
ItemTextProps,
|
ItemTextProps,
|
||||||
ItemIconProps,
|
ItemIconProps,
|
||||||
} from '#/components/Menu/types'
|
} from '#/components/Menu/types'
|
||||||
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {isNative} from 'platform/detection'
|
||||||
|
|
||||||
export {useDialogControl as useMenuControl} from '#/components/Dialog'
|
export {useDialogControl as useMenuControl} from '#/components/Dialog'
|
||||||
|
|
||||||
@@ -68,7 +72,13 @@ export function Trigger({children, label}: TriggerProps) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Outer({children}: React.PropsWithChildren<{}>) {
|
export function Outer({
|
||||||
|
children,
|
||||||
|
showCancel,
|
||||||
|
}: React.PropsWithChildren<{
|
||||||
|
showCancel?: boolean
|
||||||
|
style?: StyleProp<ViewStyle>
|
||||||
|
}>) {
|
||||||
const context = React.useContext(Context)
|
const context = React.useContext(Context)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -78,7 +88,10 @@ export function Outer({children}: React.PropsWithChildren<{}>) {
|
|||||||
{/* Re-wrap with context since Dialogs are portal-ed to root */}
|
{/* Re-wrap with context since Dialogs are portal-ed to root */}
|
||||||
<Context.Provider value={context}>
|
<Context.Provider value={context}>
|
||||||
<Dialog.ScrollableInner label="Menu TODO">
|
<Dialog.ScrollableInner label="Menu TODO">
|
||||||
<View style={[a.gap_lg]}>{children}</View>
|
<View style={[a.gap_lg]}>
|
||||||
|
{children}
|
||||||
|
{isNative && showCancel && <Cancel />}
|
||||||
|
</View>
|
||||||
<View style={{height: a.gap_lg.gap}} />
|
<View style={{height: a.gap_lg.gap}} />
|
||||||
</Dialog.ScrollableInner>
|
</Dialog.ScrollableInner>
|
||||||
</Context.Provider>
|
</Context.Provider>
|
||||||
@@ -185,6 +198,22 @@ export function Group({children, style}: GroupProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Cancel() {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const {control} = React.useContext(Context)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
label={_(msg`Close this dialog`)}
|
||||||
|
size="small"
|
||||||
|
variant="ghost"
|
||||||
|
color="secondary"
|
||||||
|
onPress={() => control.close()}>
|
||||||
|
<ButtonText>Cancel</ButtonText>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function Divider() {
|
export function Divider() {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
|
/* eslint-disable react/prop-types */
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View, Pressable} from 'react-native'
|
import {View, Pressable, ViewStyle, StyleProp} from 'react-native'
|
||||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||||
@@ -14,8 +18,10 @@ import {
|
|||||||
GroupProps,
|
GroupProps,
|
||||||
ItemTextProps,
|
ItemTextProps,
|
||||||
ItemIconProps,
|
ItemIconProps,
|
||||||
|
RadixPassThroughTriggerProps,
|
||||||
} from '#/components/Menu/types'
|
} from '#/components/Menu/types'
|
||||||
import {Context} from '#/components/Menu/context'
|
import {Context} from '#/components/Menu/context'
|
||||||
|
import {Portal} from '#/components/Portal'
|
||||||
|
|
||||||
export function useMenuControl(): Dialog.DialogControlProps {
|
export function useMenuControl(): Dialog.DialogControlProps {
|
||||||
const id = React.useId()
|
const id = React.useId()
|
||||||
@@ -47,6 +53,7 @@ export function Root({
|
|||||||
}: React.PropsWithChildren<{
|
}: React.PropsWithChildren<{
|
||||||
control?: Dialog.DialogOuterProps['control']
|
control?: Dialog.DialogOuterProps['control']
|
||||||
}>) {
|
}>) {
|
||||||
|
const {_} = useLingui()
|
||||||
const defaultControl = useMenuControl()
|
const defaultControl = useMenuControl()
|
||||||
const context = React.useMemo<ContextType>(
|
const context = React.useMemo<ContextType>(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -67,6 +74,18 @@ export function Root({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Context.Provider value={context}>
|
<Context.Provider value={context}>
|
||||||
|
{context.control.isOpen && (
|
||||||
|
<Portal>
|
||||||
|
<Pressable
|
||||||
|
style={[a.fixed, a.inset_0, a.z_50]}
|
||||||
|
onPress={() => context.control.close()}
|
||||||
|
accessibilityHint=""
|
||||||
|
accessibilityLabel={_(
|
||||||
|
msg`Context menu backdrop, click to close the menu.`,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Portal>
|
||||||
|
)}
|
||||||
<DropdownMenu.Root
|
<DropdownMenu.Root
|
||||||
open={context.control.isOpen}
|
open={context.control.isOpen}
|
||||||
onOpenChange={onOpenChange}>
|
onOpenChange={onOpenChange}>
|
||||||
@@ -76,7 +95,24 @@ export function Root({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Trigger({children, label, style}: TriggerProps) {
|
const RadixTriggerPassThrough = React.forwardRef(
|
||||||
|
(
|
||||||
|
props: {
|
||||||
|
children: (
|
||||||
|
props: RadixPassThroughTriggerProps & {
|
||||||
|
ref: React.Ref<any>
|
||||||
|
},
|
||||||
|
) => React.ReactNode
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
// @ts-expect-error Radix provides no types of this stuff
|
||||||
|
return props.children({...props, ref})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
RadixTriggerPassThrough.displayName = 'RadixTriggerPassThrough'
|
||||||
|
|
||||||
|
export function Trigger({children, label}: TriggerProps) {
|
||||||
const {control} = React.useContext(Context)
|
const {control} = React.useContext(Context)
|
||||||
const {
|
const {
|
||||||
state: hovered,
|
state: hovered,
|
||||||
@@ -87,18 +123,9 @@ export function Trigger({children, label, style}: TriggerProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu.Trigger asChild>
|
<DropdownMenu.Trigger asChild>
|
||||||
<Pressable
|
<RadixTriggerPassThrough>
|
||||||
accessibilityHint=""
|
{props =>
|
||||||
accessibilityLabel={label}
|
children({
|
||||||
onFocus={onFocus}
|
|
||||||
onBlur={onBlur}
|
|
||||||
style={flatten([style])}
|
|
||||||
onPointerDown={() => control.open()}
|
|
||||||
{...web({
|
|
||||||
onMouseEnter,
|
|
||||||
onMouseLeave,
|
|
||||||
})}>
|
|
||||||
{children({
|
|
||||||
isNative: false,
|
isNative: false,
|
||||||
control,
|
control,
|
||||||
state: {
|
state: {
|
||||||
@@ -106,14 +133,32 @@ export function Trigger({children, label, style}: TriggerProps) {
|
|||||||
focused,
|
focused,
|
||||||
pressed: false,
|
pressed: false,
|
||||||
},
|
},
|
||||||
props: {},
|
props: {
|
||||||
})}
|
...props,
|
||||||
</Pressable>
|
// disable on web, use `onPress`
|
||||||
|
onPointerDown: () => false,
|
||||||
|
onPress: () =>
|
||||||
|
control.isOpen ? control.close() : control.open(),
|
||||||
|
onFocus: onFocus,
|
||||||
|
onBlur: onBlur,
|
||||||
|
onMouseEnter,
|
||||||
|
onMouseLeave,
|
||||||
|
accessibilityLabel: label,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</RadixTriggerPassThrough>
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Outer({children}: React.PropsWithChildren<{}>) {
|
export function Outer({
|
||||||
|
children,
|
||||||
|
style,
|
||||||
|
}: React.PropsWithChildren<{
|
||||||
|
showCancel?: boolean
|
||||||
|
style?: StyleProp<ViewStyle>
|
||||||
|
}>) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -125,6 +170,7 @@ export function Outer({children}: React.PropsWithChildren<{}>) {
|
|||||||
a.p_xs,
|
a.p_xs,
|
||||||
t.name === 'light' ? t.atoms.bg : t.atoms.bg_contrast_25,
|
t.name === 'light' ? t.atoms.bg : t.atoms.bg_contrast_25,
|
||||||
t.atoms.shadow_md,
|
t.atoms.shadow_md,
|
||||||
|
style,
|
||||||
]}>
|
]}>
|
||||||
{children}
|
{children}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {GestureResponderEvent, PressableProps} from 'react-native'
|
import {
|
||||||
|
GestureResponderEvent,
|
||||||
|
PressableProps,
|
||||||
|
AccessibilityProps,
|
||||||
|
} from 'react-native'
|
||||||
|
|
||||||
import {Props as SVGIconProps} from '#/components/icons/common'
|
import {Props as SVGIconProps} from '#/components/icons/common'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
@@ -9,7 +13,23 @@ export type ContextType = {
|
|||||||
control: Dialog.DialogOuterProps['control']
|
control: Dialog.DialogOuterProps['control']
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TriggerProps = ViewStyleProp & {
|
export type RadixPassThroughTriggerProps = {
|
||||||
|
id: string
|
||||||
|
type: 'button'
|
||||||
|
disabled: boolean
|
||||||
|
['data-disabled']: boolean
|
||||||
|
['data-state']: string
|
||||||
|
['aria-controls']?: string
|
||||||
|
['aria-haspopup']?: boolean
|
||||||
|
['aria-expanded']?: AccessibilityProps['aria-expanded']
|
||||||
|
onKeyDown: (e: React.KeyboardEvent) => void
|
||||||
|
/**
|
||||||
|
* Radix provides this, but we override on web to use `onPress` instead,
|
||||||
|
* which is less sensitive while scrolling.
|
||||||
|
*/
|
||||||
|
onPointerDown: PressableProps['onPointerDown']
|
||||||
|
}
|
||||||
|
export type TriggerProps = {
|
||||||
children(props: TriggerChildProps): React.ReactNode
|
children(props: TriggerChildProps): React.ReactNode
|
||||||
label: string
|
label: string
|
||||||
}
|
}
|
||||||
@@ -52,7 +72,14 @@ export type TriggerChildProps =
|
|||||||
*/
|
*/
|
||||||
pressed: false
|
pressed: false
|
||||||
}
|
}
|
||||||
props: {}
|
props: RadixPassThroughTriggerProps & {
|
||||||
|
onPress: () => void
|
||||||
|
onFocus: () => void
|
||||||
|
onBlur: () => void
|
||||||
|
onMouseEnter: () => void
|
||||||
|
onMouseLeave: () => void
|
||||||
|
accessibilityLabel: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ItemProps = React.PropsWithChildren<
|
export type ItemProps = React.PropsWithChildren<
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View, PressableProps} from 'react-native'
|
import {View} 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, useBreakpoints} 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, ButtonColor, ButtonText} from '#/components/Button'
|
||||||
|
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
|
|
||||||
@@ -22,8 +22,10 @@ const Context = React.createContext<{
|
|||||||
export function Outer({
|
export function Outer({
|
||||||
children,
|
children,
|
||||||
control,
|
control,
|
||||||
|
testID,
|
||||||
}: React.PropsWithChildren<{
|
}: React.PropsWithChildren<{
|
||||||
control: Dialog.DialogOuterProps['control']
|
control: Dialog.DialogOuterProps['control']
|
||||||
|
testID?: string
|
||||||
}>) {
|
}>) {
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
const titleId = React.useId()
|
const titleId = React.useId()
|
||||||
@@ -35,7 +37,7 @@ export function Outer({
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.Outer control={control}>
|
<Dialog.Outer control={control} testID={testID}>
|
||||||
<Context.Provider value={context}>
|
<Context.Provider value={context}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
|
|
||||||
@@ -80,7 +82,9 @@ export function Actions({children}: React.PropsWithChildren<{}>) {
|
|||||||
a.w_full,
|
a.w_full,
|
||||||
a.gap_sm,
|
a.gap_sm,
|
||||||
a.justify_end,
|
a.justify_end,
|
||||||
gtMobile ? [a.flex_row] : [a.flex_col, a.pt_md, a.pb_4xl],
|
gtMobile
|
||||||
|
? [a.flex_row, a.flex_row_reverse, a.justify_start]
|
||||||
|
: [a.flex_col, a.pt_md, a.pb_4xl],
|
||||||
]}>
|
]}>
|
||||||
{children}
|
{children}
|
||||||
</View>
|
</View>
|
||||||
@@ -89,18 +93,29 @@ export function Actions({children}: React.PropsWithChildren<{}>) {
|
|||||||
|
|
||||||
export function Cancel({
|
export function Cancel({
|
||||||
children,
|
children,
|
||||||
}: React.PropsWithChildren<{onPress?: PressableProps['onPress']}>) {
|
cta,
|
||||||
|
}: React.PropsWithChildren<{
|
||||||
|
/**
|
||||||
|
* Optional i18n string, used in lieu of `children` for simple buttons. If
|
||||||
|
* undefined (and `children` is undefined), it will default to "Cancel".
|
||||||
|
*/
|
||||||
|
cta?: string
|
||||||
|
}>) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
const {close} = Dialog.useDialogContext()
|
const {close} = Dialog.useDialogContext()
|
||||||
|
const onPress = React.useCallback(() => {
|
||||||
|
close()
|
||||||
|
}, [close])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
variant="solid"
|
variant="solid"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
size={gtMobile ? 'small' : 'medium'}
|
size={gtMobile ? 'small' : 'medium'}
|
||||||
label={_(msg`Cancel`)}
|
label={cta || _(msg`Cancel`)}
|
||||||
onPress={() => close()}>
|
onPress={onPress}>
|
||||||
{children}
|
{children ? children : <ButtonText>{cta || _(msg`Cancel`)}</ButtonText>}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -108,22 +123,70 @@ export function Cancel({
|
|||||||
export function Action({
|
export function Action({
|
||||||
children,
|
children,
|
||||||
onPress,
|
onPress,
|
||||||
}: React.PropsWithChildren<{onPress?: () => void}>) {
|
color = 'primary',
|
||||||
|
cta,
|
||||||
|
testID,
|
||||||
|
}: React.PropsWithChildren<{
|
||||||
|
onPress: () => void
|
||||||
|
color?: ButtonColor
|
||||||
|
/**
|
||||||
|
* Optional i18n string, used in lieu of `children` for simple buttons. If
|
||||||
|
* undefined (and `children` is undefined), it will default to "Confirm".
|
||||||
|
*/
|
||||||
|
cta?: string
|
||||||
|
testID?: string
|
||||||
|
}>) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
const {close} = Dialog.useDialogContext()
|
const {close} = Dialog.useDialogContext()
|
||||||
const handleOnPress = React.useCallback(() => {
|
const handleOnPress = React.useCallback(() => {
|
||||||
close()
|
close()
|
||||||
onPress?.()
|
onPress()
|
||||||
}, [close, onPress])
|
}, [close, onPress])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
variant="solid"
|
variant="solid"
|
||||||
color="primary"
|
color={color}
|
||||||
size={gtMobile ? 'small' : 'medium'}
|
size={gtMobile ? 'small' : 'medium'}
|
||||||
label={_(msg`Confirm`)}
|
label={cta || _(msg`Confirm`)}
|
||||||
onPress={handleOnPress}>
|
onPress={handleOnPress}
|
||||||
{children}
|
testID={testID}>
|
||||||
|
{children ? children : <ButtonText>{cta || _(msg`Confirm`)}</ButtonText>}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function Basic({
|
||||||
|
control,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
cancelButtonCta,
|
||||||
|
confirmButtonCta,
|
||||||
|
onConfirm,
|
||||||
|
confirmButtonColor,
|
||||||
|
}: React.PropsWithChildren<{
|
||||||
|
control: Dialog.DialogOuterProps['control']
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
cancelButtonCta?: string
|
||||||
|
confirmButtonCta?: string
|
||||||
|
onConfirm: () => void
|
||||||
|
confirmButtonColor?: ButtonColor
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<Outer control={control} testID="confirmModal">
|
||||||
|
<Title>{title}</Title>
|
||||||
|
<Description>{description}</Description>
|
||||||
|
<Actions>
|
||||||
|
<Action
|
||||||
|
cta={confirmButtonCta}
|
||||||
|
onPress={onConfirm}
|
||||||
|
color={confirmButtonColor}
|
||||||
|
testID="confirmBtn"
|
||||||
|
/>
|
||||||
|
<Cancel cta={cancelButtonCta} />
|
||||||
|
</Actions>
|
||||||
|
</Outer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
import {UITextView} from 'react-native-ui-text-view'
|
import {UITextView} from 'react-native-ui-text-view'
|
||||||
|
|
||||||
import {useTheme, atoms, web, flatten} from '#/alf'
|
import {useTheme, atoms, web, flatten} from '#/alf'
|
||||||
import {isIOS} from '#/platform/detection'
|
import {isIOS, isNative} from '#/platform/detection'
|
||||||
|
|
||||||
export type TextProps = RNTextProps & {
|
export type TextProps = RNTextProps & {
|
||||||
/**
|
/**
|
||||||
@@ -45,11 +45,11 @@ export function normalizeTextStyles(styles: StyleProp<TextStyle>) {
|
|||||||
const fontSize = s.fontSize || atoms.text_md.fontSize
|
const fontSize = s.fontSize || atoms.text_md.fontSize
|
||||||
|
|
||||||
if (s?.lineHeight) {
|
if (s?.lineHeight) {
|
||||||
if (s.lineHeight <= 2) {
|
if (s.lineHeight !== 0 && s.lineHeight <= 2) {
|
||||||
s.lineHeight = Math.round(fontSize * s.lineHeight)
|
s.lineHeight = Math.round(fontSize * s.lineHeight)
|
||||||
}
|
}
|
||||||
} else {
|
} else if (!isNative) {
|
||||||
s.lineHeight = fontSize
|
s.lineHeight = s.fontSize
|
||||||
}
|
}
|
||||||
|
|
||||||
return s
|
return s
|
||||||
|
|||||||
@@ -254,9 +254,9 @@ function MutedWordsInner({}: {control: Dialog.DialogOuterProps['control']}) {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{isNative && <View style={{height: 20}} />}
|
{isNative && <View style={{height: 20}} />}
|
||||||
|
</View>
|
||||||
|
|
||||||
<Dialog.Close />
|
<Dialog.Close />
|
||||||
</View>
|
|
||||||
</Dialog.ScrollableInner>
|
</Dialog.ScrollableInner>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -277,29 +277,16 @@ function MutedWordRow({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Prompt.Outer control={control}>
|
<Prompt.Basic
|
||||||
<Prompt.Title>
|
control={control}
|
||||||
<Trans>Are you sure?</Trans>
|
title={_(msg`Are you sure?`)}
|
||||||
</Prompt.Title>
|
description={_(
|
||||||
<Prompt.Description>
|
msg`This will delete ${word.value} from your muted words. You can always add it back later.`,
|
||||||
<Trans>
|
)}
|
||||||
This will delete {word.value} from your muted words. You can always
|
onConfirm={remove}
|
||||||
add it back later.
|
confirmButtonCta={_(msg`Remove`)}
|
||||||
</Trans>
|
confirmButtonColor="negative"
|
||||||
</Prompt.Description>
|
/>
|
||||||
<Prompt.Actions>
|
|
||||||
<Prompt.Cancel>
|
|
||||||
<ButtonText>
|
|
||||||
<Trans>Nevermind</Trans>
|
|
||||||
</ButtonText>
|
|
||||||
</Prompt.Cancel>
|
|
||||||
<Prompt.Action onPress={remove}>
|
|
||||||
<ButtonText>
|
|
||||||
<Trans>Remove</Trans>
|
|
||||||
</ButtonText>
|
|
||||||
</Prompt.Action>
|
|
||||||
</Prompt.Actions>
|
|
||||||
</Prompt.Outer>
|
|
||||||
|
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const Camera_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M8.371 3.89A2 2 0 0 1 10.035 3h3.93a2 2 0 0 1 1.664.89L17.035 6H20a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2.965L8.37 3.89ZM13.965 5h-3.93L8.63 7.11A2 2 0 0 1 6.965 8H4v11h16V8h-2.965a2 2 0 0 1-1.664-.89L13.965 5ZM12 11a2 2 0 1 0 0 4 2 2 0 0 0 0-4Zm-4 2a4 4 0 1 1 8 0 4 4 0 0 1-8 0Z',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const Camera_Filled_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M8.371 3.89A2 2 0 0 1 10.035 3h3.93a2 2 0 0 1 1.664.89L17.035 6H20a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2.965L8.37 3.89ZM12 9a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7Z',
|
||||||
|
})
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import {createSinglePathSVG} from './TEMPLATE'
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
export const DotGrid1x3Horizontal_Stroke2_Corner0_Rounded = createSinglePathSVG(
|
export const DotGrid_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
{
|
|
||||||
path: 'M2 12a2 2 0 1 1 4 0 2 2 0 0 1-4 0Zm16 0a2 2 0 1 1 4 0 2 2 0 0 1-4 0Zm-6-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z',
|
path: 'M2 12a2 2 0 1 1 4 0 2 2 0 0 1-4 0Zm16 0a2 2 0 1 1 4 0 2 2 0 0 1-4 0Zm-6-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z',
|
||||||
},
|
})
|
||||||
)
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const Flag_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M4 4a2 2 0 0 1 2-2h13.131c1.598 0 2.55 1.78 1.665 3.11L18.202 9l2.594 3.89c.886 1.33-.067 3.11-1.665 3.11H6v5a1 1 0 1 1-2 0V4Zm2 10h13.131l-2.593-3.89a2 2 0 0 1 0-2.22L19.13 4H6v10Z',
|
||||||
|
})
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const PeopleRemove2_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M10 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM5.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM16 11a1 1 0 0 1 1-1h5a1 1 0 1 1 0 2h-5a1 1 0 0 1-1-1ZM3.678 19h12.644c-.71-2.909-3.092-5-6.322-5s-5.613 2.091-6.322 5Zm-2.174.906C1.917 15.521 5.242 12 10 12c4.758 0 8.083 3.521 8.496 7.906A1 1 0 0 1 17.5 21h-15a1 1 0 0 1-.996-1.094Z',
|
||||||
|
})
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const PersonCheck_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.679 19c.709-2.902 3.079-5 6.321-5a6.69 6.69 0 0 1 2.612.51 1 1 0 0 0 .776-1.844A8.687 8.687 0 0 0 12 12c-4.3 0-7.447 2.884-8.304 6.696-.29 1.29.767 2.304 1.902 2.304H11a1 1 0 1 0 0-2H5.679Zm14.835-4.857a1 1 0 0 1 .344 1.371l-3 5a1 1 0 0 1-1.458.286l-2-1.5a1 1 0 0 1 1.2-1.6l1.113.835 2.43-4.05a1 1 0 0 1 1.372-.342Z',
|
||||||
|
})
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const PersonX_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.679 19c.709-2.902 3.079-5 6.321-5 .302 0 .595.018.878.053a1 1 0 0 0 .243-1.985A9.235 9.235 0 0 0 12 12c-4.3 0-7.447 2.884-8.304 6.696-.29 1.29.767 2.304 1.902 2.304H12a1 1 0 1 0 0-2H5.679Zm9.614-3.707a1 1 0 0 1 1.414 0L18 16.586l1.293-1.293a1 1 0 0 1 1.414 1.414L19.414 18l1.293 1.293a1 1 0 0 1-1.414 1.414L18 19.414l-1.293 1.293a1 1 0 0 1-1.414-1.414L16.586 18l-1.293-1.293a1 1 0 0 1 0-1.414Z',
|
||||||
|
})
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const StreamingLive_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M4 4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H4Zm8 12.5c1.253 0 2.197.609 2.674 1.5H9.326c.477-.891 1.42-1.5 2.674-1.5Zm0-2c2.404 0 4.235 1.475 4.822 3.5H20V6H4v12h3.178c.587-2.025 2.418-3.5 4.822-3.5Zm-1.25-3.75a1.25 1.25 0 1 1 2.5 0 1.25 1.25 0 0 1-2.5 0ZM12 7.5a3.25 3.25 0 1 0 0 6.5 3.25 3.25 0 0 0 0-6.5Zm5.75 2a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Z',
|
||||||
|
})
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export const useDedupe = () => {
|
||||||
|
const canDo = React.useRef(true)
|
||||||
|
|
||||||
|
return React.useRef((cb: () => unknown) => {
|
||||||
|
if (canDo.current) {
|
||||||
|
canDo.current = false
|
||||||
|
setTimeout(() => {
|
||||||
|
canDo.current = true
|
||||||
|
}, 250)
|
||||||
|
cb()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}).current
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {useNavigation} from '@react-navigation/core'
|
||||||
|
import {AllNavigatorParams, NavigationProp} from 'lib/routes/types'
|
||||||
|
import type {NavigationAction} from '@react-navigation/routers'
|
||||||
|
import {NavigationState} from '@react-navigation/native'
|
||||||
|
import {useDedupe} from 'lib/hooks/useDedupe'
|
||||||
|
|
||||||
|
export type DebouncedNavigationProp = Pick<
|
||||||
|
NavigationProp,
|
||||||
|
| 'popToTop'
|
||||||
|
| 'push'
|
||||||
|
| 'navigate'
|
||||||
|
| 'canGoBack'
|
||||||
|
| 'replace'
|
||||||
|
| 'dispatch'
|
||||||
|
| 'goBack'
|
||||||
|
| 'getState'
|
||||||
|
>
|
||||||
|
|
||||||
|
export function useNavigationDeduped() {
|
||||||
|
const navigation = useNavigation<NavigationProp>()
|
||||||
|
const dedupe = useDedupe()
|
||||||
|
|
||||||
|
return React.useMemo(
|
||||||
|
(): DebouncedNavigationProp => ({
|
||||||
|
// Types from @react-navigation/routers/lib/typescript/src/StackRouter.ts
|
||||||
|
push: <RouteName extends keyof AllNavigatorParams>(
|
||||||
|
...args: undefined extends AllNavigatorParams[RouteName]
|
||||||
|
?
|
||||||
|
| [screen: RouteName]
|
||||||
|
| [screen: RouteName, params: AllNavigatorParams[RouteName]]
|
||||||
|
: [screen: RouteName, params: AllNavigatorParams[RouteName]]
|
||||||
|
) => {
|
||||||
|
dedupe(() => navigation.push(...args))
|
||||||
|
},
|
||||||
|
// Types from @react-navigation/core/src/types.tsx
|
||||||
|
navigate: <RouteName extends keyof AllNavigatorParams>(
|
||||||
|
...args: RouteName extends unknown
|
||||||
|
? undefined extends AllNavigatorParams[RouteName]
|
||||||
|
?
|
||||||
|
| [screen: RouteName]
|
||||||
|
| [screen: RouteName, params: AllNavigatorParams[RouteName]]
|
||||||
|
: [screen: RouteName, params: AllNavigatorParams[RouteName]]
|
||||||
|
: never
|
||||||
|
) => {
|
||||||
|
dedupe(() => navigation.navigate(...args))
|
||||||
|
},
|
||||||
|
// Types from @react-navigation/routers/lib/typescript/src/StackRouter.ts
|
||||||
|
replace: <RouteName extends keyof AllNavigatorParams>(
|
||||||
|
...args: undefined extends AllNavigatorParams[RouteName]
|
||||||
|
?
|
||||||
|
| [screen: RouteName]
|
||||||
|
| [screen: RouteName, params: AllNavigatorParams[RouteName]]
|
||||||
|
: [screen: RouteName, params: AllNavigatorParams[RouteName]]
|
||||||
|
) => {
|
||||||
|
dedupe(() => navigation.replace(...args))
|
||||||
|
},
|
||||||
|
dispatch: (
|
||||||
|
action:
|
||||||
|
| NavigationAction
|
||||||
|
| ((state: NavigationState) => NavigationAction),
|
||||||
|
) => {
|
||||||
|
dedupe(() => navigation.dispatch(action))
|
||||||
|
},
|
||||||
|
popToTop: () => {
|
||||||
|
dedupe(() => navigation.popToTop())
|
||||||
|
},
|
||||||
|
goBack: () => {
|
||||||
|
dedupe(() => navigation.goBack())
|
||||||
|
},
|
||||||
|
canGoBack: () => {
|
||||||
|
return navigation.canGoBack()
|
||||||
|
},
|
||||||
|
getState: () => {
|
||||||
|
return navigation.getState()
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[dedupe, navigation],
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,25 +2,9 @@ import * as Updates from 'expo-updates'
|
|||||||
import {useCallback, useEffect} from 'react'
|
import {useCallback, useEffect} from 'react'
|
||||||
import {AppState} from 'react-native'
|
import {AppState} from 'react-native'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {useModalControls} from '#/state/modals'
|
|
||||||
import {t} from '@lingui/macro'
|
|
||||||
|
|
||||||
export function useOTAUpdate() {
|
export function useOTAUpdate() {
|
||||||
const {openModal} = useModalControls()
|
|
||||||
|
|
||||||
// HELPER FUNCTIONS
|
// HELPER FUNCTIONS
|
||||||
const showUpdatePopup = useCallback(() => {
|
|
||||||
openModal({
|
|
||||||
name: 'confirm',
|
|
||||||
title: t`Update Available`,
|
|
||||||
message: t`A new version of the app is available. Please update to continue using the app.`,
|
|
||||||
onPressConfirm: async () => {
|
|
||||||
Updates.reloadAsync().catch(err => {
|
|
||||||
throw err
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}, [openModal])
|
|
||||||
const checkForUpdate = useCallback(async () => {
|
const checkForUpdate = useCallback(async () => {
|
||||||
logger.debug('useOTAUpdate: Checking for update...')
|
logger.debug('useOTAUpdate: Checking for update...')
|
||||||
try {
|
try {
|
||||||
@@ -32,16 +16,13 @@ export function useOTAUpdate() {
|
|||||||
}
|
}
|
||||||
// Otherwise fetch the update in the background, so even if the user rejects switching to latest version it will be done automatically on next relaunch.
|
// Otherwise fetch the update in the background, so even if the user rejects switching to latest version it will be done automatically on next relaunch.
|
||||||
await Updates.fetchUpdateAsync()
|
await Updates.fetchUpdateAsync()
|
||||||
// show a popup modal
|
|
||||||
showUpdatePopup()
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('useOTAUpdate: Error while checking for update', {
|
logger.error('useOTAUpdate: Error while checking for update', {
|
||||||
message: e,
|
message: e,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [showUpdatePopup])
|
}, [])
|
||||||
const updateEventListener = useCallback(
|
const updateEventListener = useCallback((event: Updates.UpdateEvent) => {
|
||||||
(event: Updates.UpdateEvent) => {
|
|
||||||
logger.debug('useOTAUpdate: Listening for update...')
|
logger.debug('useOTAUpdate: Listening for update...')
|
||||||
if (event.type === Updates.UpdateEventType.ERROR) {
|
if (event.type === Updates.UpdateEventType.ERROR) {
|
||||||
logger.error('useOTAUpdate: Error while listening for update', {
|
logger.error('useOTAUpdate: Error while listening for update', {
|
||||||
@@ -53,11 +34,8 @@ export function useOTAUpdate() {
|
|||||||
} else if (event.type === Updates.UpdateEventType.UPDATE_AVAILABLE) {
|
} else if (event.type === Updates.UpdateEventType.UPDATE_AVAILABLE) {
|
||||||
// Handle update available
|
// Handle update available
|
||||||
// open modal, ask for user confirmation, and reload the app
|
// open modal, ask for user confirmation, and reload the app
|
||||||
showUpdatePopup()
|
|
||||||
}
|
}
|
||||||
},
|
}, [])
|
||||||
[showUpdatePopup],
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// ADD EVENT LISTENERS
|
// ADD EVENT LISTENERS
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ interface ReportOptions {
|
|||||||
account: ReportOption[]
|
account: ReportOption[]
|
||||||
post: ReportOption[]
|
post: ReportOption[]
|
||||||
list: ReportOption[]
|
list: ReportOption[]
|
||||||
|
feedgen: ReportOption[]
|
||||||
other: ReportOption[]
|
other: ReportOption[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import {Share} from 'react-native'
|
|||||||
*/
|
*/
|
||||||
export async function shareUrl(url: string) {
|
export async function shareUrl(url: string) {
|
||||||
if (isAndroid) {
|
if (isAndroid) {
|
||||||
Share.share({message: url})
|
await Share.share({message: url})
|
||||||
} else if (isIOS) {
|
} else if (isIOS) {
|
||||||
Share.share({url})
|
await Share.share({url})
|
||||||
} else {
|
} else {
|
||||||
// React Native Share is not supported by web. Web Share API
|
// React Native Share is not supported by web. Web Share API
|
||||||
// has increasing but not full support, so default to clipboard
|
// has increasing but not full support, so default to clipboard
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import {
|
|
||||||
Statsig,
|
|
||||||
StatsigProvider,
|
|
||||||
useGate as useStatsigGate,
|
|
||||||
} from 'statsig-react'
|
|
||||||
import {useSession} from '../../state/session'
|
|
||||||
import {sha256} from 'js-sha256'
|
|
||||||
|
|
||||||
const statsigOptions = {
|
|
||||||
environment: {
|
|
||||||
tier: process.env.NODE_ENV === 'development' ? 'development' : 'production',
|
|
||||||
},
|
|
||||||
// Don't block on waiting for network. The fetched config will kick in on next load.
|
|
||||||
// This ensures the UI is always consistent and doesn't update mid-session.
|
|
||||||
// Note this makes cold load (no local storage) and private mode return `false` for all gates.
|
|
||||||
initTimeoutMs: 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
export function logEvent(
|
|
||||||
eventName: string,
|
|
||||||
value?: string | number | null,
|
|
||||||
metadata?: Record<string, string> | null,
|
|
||||||
) {
|
|
||||||
Statsig.logEvent(eventName, value, metadata)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useGate(gateName: string) {
|
|
||||||
const {isLoading, value} = useStatsigGate(gateName)
|
|
||||||
if (isLoading) {
|
|
||||||
// This should not happen because of waitForInitialization={true}.
|
|
||||||
console.error('Did not expected isLoading to ever be true.')
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
function toStatsigUser(did: string | undefined) {
|
|
||||||
let userID: string | undefined
|
|
||||||
if (did) {
|
|
||||||
userID = sha256(did)
|
|
||||||
}
|
|
||||||
return {userID}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Provider({children}: {children: React.ReactNode}) {
|
|
||||||
const {currentAccount} = useSession()
|
|
||||||
const currentStatsigUser = React.useMemo(
|
|
||||||
() => toStatsigUser(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 (
|
|
||||||
<StatsigProvider
|
|
||||||
sdkKey="client-SXJakO39w9vIhl3D44u8UupyzFl4oZ2qPIkjwcvuPsV"
|
|
||||||
mountKey={currentStatsigUser.userID}
|
|
||||||
user={currentStatsigUser}
|
|
||||||
// This isn't really blocking due to short initTimeoutMs above.
|
|
||||||
// However, it ensures `isLoading` is always `false`.
|
|
||||||
waitForInitialization={true}
|
|
||||||
options={statsigOptions}>
|
|
||||||
{children}
|
|
||||||
</StatsigProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -238,13 +238,14 @@ function FeedCardInner({feed}: {feed: FeedSourceInfo; config: FeedConfig}) {
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={[a.pt_2xs, a.flex_grow]}>
|
<View style={[a.pt_2xs, a.flex_1, a.flex_grow]}>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
a.text_md,
|
a.text_md,
|
||||||
a.font_bold,
|
a.font_bold,
|
||||||
ctx.selected && styles.textSelected,
|
ctx.selected && styles.textSelected,
|
||||||
]}>
|
]}
|
||||||
|
numberOfLines={1}>
|
||||||
{feed.displayName}
|
{feed.displayName}
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
|
|||||||
@@ -1,290 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import {View} from 'react-native'
|
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
|
||||||
import {AppBskyActorDefs} from '@atproto/api'
|
|
||||||
import {msg} from '@lingui/macro'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
|
|
||||||
import {isWeb} from 'platform/detection'
|
|
||||||
import {useModalControls} from '#/state/modals'
|
|
||||||
import {
|
|
||||||
RQKEY as profileQueryKey,
|
|
||||||
useProfileMuteMutationQueue,
|
|
||||||
useProfileBlockMutationQueue,
|
|
||||||
} from '#/state/queries/profile'
|
|
||||||
import {useAnalytics} from 'lib/analytics/analytics'
|
|
||||||
import {makeProfileLink} from 'lib/routes/links'
|
|
||||||
import {toShareUrl} from 'lib/strings/url-helpers'
|
|
||||||
import {shareUrl} from 'lib/sharing'
|
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {useSession} from '#/state/session'
|
|
||||||
import {Shadow} from '#/state/cache/types'
|
|
||||||
|
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
|
||||||
import * as Toast from 'view/com/util/Toast'
|
|
||||||
import {NativeDropdown, DropdownItem} from 'view/com/util/forms/NativeDropdown'
|
|
||||||
import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
|
|
||||||
import {DotGrid1x3Horizontal_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid'
|
|
||||||
|
|
||||||
export function ProfileHeaderDropdownBtn({
|
|
||||||
profile,
|
|
||||||
}: {
|
|
||||||
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
|
|
||||||
}) {
|
|
||||||
const t = useTheme()
|
|
||||||
const {currentAccount, hasSession} = useSession()
|
|
||||||
const {_} = useLingui()
|
|
||||||
const {openModal} = useModalControls()
|
|
||||||
const {track} = useAnalytics()
|
|
||||||
const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile)
|
|
||||||
const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
|
|
||||||
const queryClient = useQueryClient()
|
|
||||||
const reportDialogControl = useReportDialogControl()
|
|
||||||
|
|
||||||
const invalidateProfileQuery = React.useCallback(() => {
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: profileQueryKey(profile.did),
|
|
||||||
})
|
|
||||||
}, [queryClient, profile.did])
|
|
||||||
|
|
||||||
const onPressShare = React.useCallback(() => {
|
|
||||||
track('ProfileHeader:ShareButtonClicked')
|
|
||||||
shareUrl(toShareUrl(makeProfileLink(profile)))
|
|
||||||
}, [track, profile])
|
|
||||||
|
|
||||||
const onPressAddRemoveLists = React.useCallback(() => {
|
|
||||||
track('ProfileHeader:AddToListsButtonClicked')
|
|
||||||
openModal({
|
|
||||||
name: 'user-add-remove-lists',
|
|
||||||
subject: profile.did,
|
|
||||||
handle: profile.handle,
|
|
||||||
displayName: profile.displayName || profile.handle,
|
|
||||||
onAdd: invalidateProfileQuery,
|
|
||||||
onRemove: invalidateProfileQuery,
|
|
||||||
})
|
|
||||||
}, [track, profile, openModal, invalidateProfileQuery])
|
|
||||||
|
|
||||||
const onPressMuteAccount = React.useCallback(async () => {
|
|
||||||
track('ProfileHeader:MuteAccountButtonClicked')
|
|
||||||
try {
|
|
||||||
await queueMute()
|
|
||||||
Toast.show(_(msg`Account muted`))
|
|
||||||
} catch (e: any) {
|
|
||||||
if (e?.name !== 'AbortError') {
|
|
||||||
logger.error('Failed to mute account', {message: e})
|
|
||||||
Toast.show(_(msg`There was an issue! ${e.toString()}`))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [track, queueMute, _])
|
|
||||||
|
|
||||||
const onPressUnmuteAccount = React.useCallback(async () => {
|
|
||||||
track('ProfileHeader:UnmuteAccountButtonClicked')
|
|
||||||
try {
|
|
||||||
await queueUnmute()
|
|
||||||
Toast.show(_(msg`Account unmuted`))
|
|
||||||
} catch (e: any) {
|
|
||||||
if (e?.name !== 'AbortError') {
|
|
||||||
logger.error('Failed to unmute account', {message: e})
|
|
||||||
Toast.show(_(msg`There was an issue! ${e.toString()}`))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [track, queueUnmute, _])
|
|
||||||
|
|
||||||
const onPressBlockAccount = React.useCallback(async () => {
|
|
||||||
track('ProfileHeader:BlockAccountButtonClicked')
|
|
||||||
openModal({
|
|
||||||
name: 'confirm',
|
|
||||||
title: _(msg`Block Account`),
|
|
||||||
message: _(
|
|
||||||
msg`Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`,
|
|
||||||
),
|
|
||||||
onPressConfirm: async () => {
|
|
||||||
try {
|
|
||||||
await queueBlock()
|
|
||||||
Toast.show(_(msg`Account blocked`))
|
|
||||||
} catch (e: any) {
|
|
||||||
if (e?.name !== 'AbortError') {
|
|
||||||
logger.error('Failed to block account', {message: e})
|
|
||||||
Toast.show(_(msg`There was an issue! ${e.toString()}`))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}, [track, queueBlock, openModal, _])
|
|
||||||
|
|
||||||
const onPressUnblockAccount = React.useCallback(async () => {
|
|
||||||
track('ProfileHeader:UnblockAccountButtonClicked')
|
|
||||||
openModal({
|
|
||||||
name: 'confirm',
|
|
||||||
title: _(msg`Unblock Account`),
|
|
||||||
message: _(
|
|
||||||
msg`The account will be able to interact with you after unblocking.`,
|
|
||||||
),
|
|
||||||
onPressConfirm: async () => {
|
|
||||||
try {
|
|
||||||
await queueUnblock()
|
|
||||||
Toast.show(_(msg`Account unblocked`))
|
|
||||||
} catch (e: any) {
|
|
||||||
if (e?.name !== 'AbortError') {
|
|
||||||
logger.error('Failed to unblock account', {message: e})
|
|
||||||
Toast.show(_(msg`There was an issue! ${e.toString()}`))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}, [track, queueUnblock, openModal, _])
|
|
||||||
|
|
||||||
const onPressReportAccount = React.useCallback(() => {
|
|
||||||
track('ProfileHeader:ReportAccountButtonClicked')
|
|
||||||
reportDialogControl.open()
|
|
||||||
}, [track, reportDialogControl])
|
|
||||||
|
|
||||||
const isMe = React.useMemo(
|
|
||||||
() => currentAccount?.did === profile.did,
|
|
||||||
[currentAccount, profile],
|
|
||||||
)
|
|
||||||
const dropdownItems: DropdownItem[] = React.useMemo(() => {
|
|
||||||
let items: DropdownItem[] = []
|
|
||||||
|
|
||||||
if (hasSession && profile.associated?.labeler) {
|
|
||||||
items.push({
|
|
||||||
testID: 'profileHeaderDropdownFollowBtn',
|
|
||||||
label: _(msg`Follow Account`),
|
|
||||||
onPress: () => {}, // TODO
|
|
||||||
icon: {
|
|
||||||
ios: {
|
|
||||||
name: 'plus',
|
|
||||||
},
|
|
||||||
android: '',
|
|
||||||
web: 'plus',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
items.push({
|
|
||||||
testID: 'profileHeaderDropdownShareBtn',
|
|
||||||
label: isWeb ? _(msg`Copy Link to Profile`) : _(msg`Share`),
|
|
||||||
onPress: onPressShare,
|
|
||||||
icon: {
|
|
||||||
ios: {
|
|
||||||
name: 'square.and.arrow.up',
|
|
||||||
},
|
|
||||||
android: 'ic_menu_share',
|
|
||||||
web: 'share',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if (hasSession) {
|
|
||||||
items.push({label: 'separator'})
|
|
||||||
items.push({
|
|
||||||
testID: 'profileHeaderDropdownListAddRemoveBtn',
|
|
||||||
label: _(msg`Add to Lists`),
|
|
||||||
onPress: onPressAddRemoveLists,
|
|
||||||
icon: {
|
|
||||||
ios: {
|
|
||||||
name: 'list.bullet',
|
|
||||||
},
|
|
||||||
android: 'ic_menu_add',
|
|
||||||
web: 'list',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if (!isMe) {
|
|
||||||
if (!profile.viewer?.blocking) {
|
|
||||||
if (!profile.viewer?.mutedByList) {
|
|
||||||
items.push({
|
|
||||||
testID: 'profileHeaderDropdownMuteBtn',
|
|
||||||
label: profile.viewer?.muted
|
|
||||||
? _(msg`Unmute Account`)
|
|
||||||
: _(msg`Mute Account`),
|
|
||||||
onPress: profile.viewer?.muted
|
|
||||||
? onPressUnmuteAccount
|
|
||||||
: onPressMuteAccount,
|
|
||||||
icon: {
|
|
||||||
ios: {
|
|
||||||
name: 'speaker.slash',
|
|
||||||
},
|
|
||||||
android: 'ic_lock_silent_mode',
|
|
||||||
web: 'comment-slash',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!profile.viewer?.blockingByList) {
|
|
||||||
items.push({
|
|
||||||
testID: 'profileHeaderDropdownBlockBtn',
|
|
||||||
label: profile.viewer?.blocking
|
|
||||||
? _(msg`Unblock Account`)
|
|
||||||
: _(msg`Block Account`),
|
|
||||||
onPress: profile.viewer?.blocking
|
|
||||||
? onPressUnblockAccount
|
|
||||||
: onPressBlockAccount,
|
|
||||||
icon: {
|
|
||||||
ios: {
|
|
||||||
name: 'person.fill.xmark',
|
|
||||||
},
|
|
||||||
android: 'ic_menu_close_clear_cancel',
|
|
||||||
web: 'user-slash',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
items.push({
|
|
||||||
testID: 'profileHeaderDropdownReportBtn',
|
|
||||||
label: _(msg`Report Account`),
|
|
||||||
onPress: onPressReportAccount,
|
|
||||||
icon: {
|
|
||||||
ios: {
|
|
||||||
name: 'exclamationmark.triangle',
|
|
||||||
},
|
|
||||||
android: 'ic_menu_report_image',
|
|
||||||
web: 'circle-exclamation',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return items
|
|
||||||
}, [
|
|
||||||
isMe,
|
|
||||||
hasSession,
|
|
||||||
profile.associated?.labeler,
|
|
||||||
profile.viewer?.muted,
|
|
||||||
profile.viewer?.mutedByList,
|
|
||||||
profile.viewer?.blocking,
|
|
||||||
profile.viewer?.blockingByList,
|
|
||||||
onPressShare,
|
|
||||||
onPressUnmuteAccount,
|
|
||||||
onPressMuteAccount,
|
|
||||||
onPressUnblockAccount,
|
|
||||||
onPressBlockAccount,
|
|
||||||
onPressReportAccount,
|
|
||||||
onPressAddRemoveLists,
|
|
||||||
_,
|
|
||||||
])
|
|
||||||
|
|
||||||
return dropdownItems?.length ? (
|
|
||||||
<>
|
|
||||||
<ReportDialog
|
|
||||||
control={reportDialogControl}
|
|
||||||
params={{type: 'account', did: profile.did}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<NativeDropdown
|
|
||||||
testID="profileHeaderDropdownBtn"
|
|
||||||
items={dropdownItems}
|
|
||||||
accessibilityLabel={_(msg`More options`)}
|
|
||||||
accessibilityHint="">
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
{
|
|
||||||
height: 40,
|
|
||||||
width: 40,
|
|
||||||
},
|
|
||||||
t.atoms.bg_contrast_25,
|
|
||||||
a.flex_row,
|
|
||||||
a.align_center,
|
|
||||||
a.justify_center,
|
|
||||||
a.rounded_full,
|
|
||||||
]}>
|
|
||||||
<Ellipsis width={20} fill={t.atoms.text_contrast_medium.color} />
|
|
||||||
</View>
|
|
||||||
</NativeDropdown>
|
|
||||||
</>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
@@ -29,7 +29,7 @@ import {Button, ButtonText} from '#/components/Button'
|
|||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {ProfileHeaderShell} from './Shell'
|
import {ProfileHeaderShell} from './Shell'
|
||||||
import {ProfileHeaderDropdownBtn} from './DropdownBtn'
|
import {ProfileMenu} from '#/view/com/profile/ProfileMenu'
|
||||||
import {ProfileHeaderDisplayName} from './DisplayName'
|
import {ProfileHeaderDisplayName} from './DisplayName'
|
||||||
import {ProfileHeaderHandle} from './Handle'
|
import {ProfileHeaderHandle} from './Handle'
|
||||||
import {ProfileHeaderMetrics} from './Metrics'
|
import {ProfileHeaderMetrics} from './Metrics'
|
||||||
@@ -200,7 +200,7 @@ let ProfileHeaderLabeler = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
<ProfileHeaderDropdownBtn profile={profile} />
|
<ProfileMenu profile={profile} />
|
||||||
</View>
|
</View>
|
||||||
<View style={[a.flex_col, a.gap_xs, a.pb_md]}>
|
<View style={[a.flex_col, a.gap_xs, a.pb_md]}>
|
||||||
<ProfileHeaderDisplayName profile={profile} moderation={moderation} />
|
<ProfileHeaderDisplayName profile={profile} moderation={moderation} />
|
||||||
|
|||||||
@@ -26,12 +26,13 @@ import {atoms as a, useTheme} from '#/alf'
|
|||||||
import {Button, ButtonText, ButtonIcon} from '#/components/Button'
|
import {Button, ButtonText, ButtonIcon} from '#/components/Button'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {ProfileHeaderShell} from './Shell'
|
import {ProfileHeaderShell} from './Shell'
|
||||||
import {ProfileHeaderDropdownBtn} from './DropdownBtn'
|
import {ProfileMenu} from '#/view/com/profile/ProfileMenu'
|
||||||
import {ProfileHeaderDisplayName} from './DisplayName'
|
import {ProfileHeaderDisplayName} from './DisplayName'
|
||||||
import {ProfileHeaderHandle} from './Handle'
|
import {ProfileHeaderHandle} from './Handle'
|
||||||
import {ProfileHeaderMetrics} from './Metrics'
|
import {ProfileHeaderMetrics} from './Metrics'
|
||||||
import {ProfileHeaderSuggestedFollows} from '#/view/com/profile/ProfileHeaderSuggestedFollows'
|
import {ProfileHeaderSuggestedFollows} from '#/view/com/profile/ProfileHeaderSuggestedFollows'
|
||||||
import {RichText} from '#/components/RichText'
|
import {RichText} from '#/components/RichText'
|
||||||
|
import * as Prompt from '#/components/Prompt'
|
||||||
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||||
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||||
|
|
||||||
@@ -64,6 +65,7 @@ let ProfileHeaderStandard = ({
|
|||||||
const [showSuggestedFollows, setShowSuggestedFollows] = React.useState(false)
|
const [showSuggestedFollows, setShowSuggestedFollows] = React.useState(false)
|
||||||
const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile)
|
const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile)
|
||||||
const [_queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
|
const [_queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
|
||||||
|
const unblockPromptControl = Prompt.usePromptControl()
|
||||||
const requireAuth = useRequireAuth()
|
const requireAuth = useRequireAuth()
|
||||||
|
|
||||||
const onPressEditProfile = React.useCallback(() => {
|
const onPressEditProfile = React.useCallback(() => {
|
||||||
@@ -118,15 +120,8 @@ let ProfileHeaderStandard = ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const onPressUnblockAccount = React.useCallback(async () => {
|
const unblockAccount = React.useCallback(async () => {
|
||||||
track('ProfileHeader:UnblockAccountButtonClicked')
|
track('ProfileHeader:UnblockAccountButtonClicked')
|
||||||
openModal({
|
|
||||||
name: 'confirm',
|
|
||||||
title: _(msg`Unblock Account`),
|
|
||||||
message: _(
|
|
||||||
msg`The account will be able to interact with you after unblocking.`,
|
|
||||||
),
|
|
||||||
onPressConfirm: async () => {
|
|
||||||
try {
|
try {
|
||||||
await queueUnblock()
|
await queueUnblock()
|
||||||
Toast.show(_(msg`Account unblocked`))
|
Toast.show(_(msg`Account unblocked`))
|
||||||
@@ -136,9 +131,7 @@ let ProfileHeaderStandard = ({
|
|||||||
Toast.show(_(msg`There was an issue! ${e.toString()}`))
|
Toast.show(_(msg`There was an issue! ${e.toString()}`))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}, [_, queueUnblock, track])
|
||||||
})
|
|
||||||
}, [track, queueUnblock, openModal, _])
|
|
||||||
|
|
||||||
const isMe = React.useMemo(
|
const isMe = React.useMemo(
|
||||||
() => currentAccount?.did === profile.did,
|
() => currentAccount?.did === profile.did,
|
||||||
@@ -177,7 +170,7 @@ let ProfileHeaderStandard = ({
|
|||||||
variant="solid"
|
variant="solid"
|
||||||
label={_(msg`Unblock`)}
|
label={_(msg`Unblock`)}
|
||||||
disabled={!hasSession}
|
disabled={!hasSession}
|
||||||
onPress={onPressUnblockAccount}
|
onPress={() => unblockPromptControl.open()}
|
||||||
style={a.rounded_full}>
|
style={a.rounded_full}>
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
<Trans context="action">Unblock</Trans>
|
<Trans context="action">Unblock</Trans>
|
||||||
@@ -236,7 +229,7 @@ let ProfileHeaderStandard = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
<ProfileHeaderDropdownBtn profile={profile} />
|
<ProfileMenu profile={profile} />
|
||||||
</View>
|
</View>
|
||||||
<View style={[a.flex_col, a.gap_xs, a.pb_sm]}>
|
<View style={[a.flex_col, a.gap_xs, a.pb_sm]}>
|
||||||
<ProfileHeaderDisplayName profile={profile} moderation={moderation} />
|
<ProfileHeaderDisplayName profile={profile} moderation={moderation} />
|
||||||
@@ -271,6 +264,18 @@ let ProfileHeaderStandard = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<Prompt.Basic
|
||||||
|
control={unblockPromptControl}
|
||||||
|
title={_(msg`Unblock Account?`)}
|
||||||
|
description={_(
|
||||||
|
msg`The account will be able to interact with you after unblocking.`,
|
||||||
|
)}
|
||||||
|
onConfirm={unblockAccount}
|
||||||
|
confirmButtonCta={
|
||||||
|
profile.viewer?.blocking ? _(msg`Unblock`) : _(msg`Block`)
|
||||||
|
}
|
||||||
|
confirmButtonColor="negative"
|
||||||
|
/>
|
||||||
</ProfileHeaderShell>
|
</ProfileHeaderShell>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api'
|
import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api'
|
||||||
import {StyleProp, ViewStyle} from 'react-native'
|
|
||||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||||
|
|
||||||
import {ImageModel} from '#/state/models/media/image'
|
import {ImageModel} from '#/state/models/media/image'
|
||||||
@@ -9,17 +8,6 @@ import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
|||||||
import {EmbedPlayerSource} from '#/lib/strings/embed-player'
|
import {EmbedPlayerSource} from '#/lib/strings/embed-player'
|
||||||
import {ThreadgateSetting} from '../queries/threadgate'
|
import {ThreadgateSetting} from '../queries/threadgate'
|
||||||
|
|
||||||
export interface ConfirmModal {
|
|
||||||
name: 'confirm'
|
|
||||||
title: string
|
|
||||||
message: string | (() => JSX.Element)
|
|
||||||
onPressConfirm: () => void | Promise<void>
|
|
||||||
onPressCancel?: () => void | Promise<void>
|
|
||||||
confirmBtnText?: string
|
|
||||||
confirmBtnStyle?: StyleProp<ViewStyle>
|
|
||||||
cancelBtnText?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EditProfileModal {
|
export interface EditProfileModal {
|
||||||
name: 'edit-profile'
|
name: 'edit-profile'
|
||||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||||
@@ -189,7 +177,6 @@ export type Modal =
|
|||||||
| InviteCodesModal
|
| InviteCodesModal
|
||||||
|
|
||||||
// Generic
|
// Generic
|
||||||
| ConfirmModal
|
|
||||||
| LinkWarningModal
|
| LinkWarningModal
|
||||||
| EmbedConsentModal
|
| EmbedConsentModal
|
||||||
| InAppBrowserConsentModal
|
| InAppBrowserConsentModal
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ import {SuggestedLanguage} from './select-language/SuggestedLanguage'
|
|||||||
import {insertMentionAt} from 'lib/strings/mention-manip'
|
import {insertMentionAt} from 'lib/strings/mention-manip'
|
||||||
import {Trans, msg} from '@lingui/macro'
|
import {Trans, msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useModals, useModalControls} from '#/state/modals'
|
import {useModals} from '#/state/modals'
|
||||||
import {useRequireAltTextEnabled} from '#/state/preferences'
|
import {useRequireAltTextEnabled} from '#/state/preferences'
|
||||||
import {
|
import {
|
||||||
useLanguagePrefs,
|
useLanguagePrefs,
|
||||||
@@ -63,6 +63,8 @@ import {emitPostCreated} from '#/state/events'
|
|||||||
import {ThreadgateSetting} from '#/state/queries/threadgate'
|
import {ThreadgateSetting} from '#/state/queries/threadgate'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {ComposerReplyTo} from 'view/com/composer/ComposerReplyTo'
|
import {ComposerReplyTo} from 'view/com/composer/ComposerReplyTo'
|
||||||
|
import * as Prompt from '#/components/Prompt'
|
||||||
|
import {useDialogStateControlContext} from 'state/dialogs'
|
||||||
|
|
||||||
type Props = ComposerOpts
|
type Props = ComposerOpts
|
||||||
export const ComposePost = observer(function ComposePost({
|
export const ComposePost = observer(function ComposePost({
|
||||||
@@ -76,8 +78,7 @@ export const ComposePost = observer(function ComposePost({
|
|||||||
}: Props) {
|
}: Props) {
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const {data: currentProfile} = useProfileQuery({did: currentAccount!.did})
|
const {data: currentProfile} = useProfileQuery({did: currentAccount!.did})
|
||||||
const {isModalActive, activeModals} = useModals()
|
const {isModalActive} = useModals()
|
||||||
const {openModal, closeModal} = useModalControls()
|
|
||||||
const {closeComposer} = useComposerControls()
|
const {closeComposer} = useComposerControls()
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
@@ -87,6 +88,9 @@ export const ComposePost = observer(function ComposePost({
|
|||||||
const langPrefs = useLanguagePrefs()
|
const langPrefs = useLanguagePrefs()
|
||||||
const setLangPrefs = useLanguagePrefsApi()
|
const setLangPrefs = useLanguagePrefsApi()
|
||||||
const textInput = useRef<TextInputRef>(null)
|
const textInput = useRef<TextInputRef>(null)
|
||||||
|
const discardPromptControl = Prompt.usePromptControl()
|
||||||
|
const {closeAllDialogs} = useDialogStateControlContext()
|
||||||
|
|
||||||
const [isKeyboardVisible] = useIsKeyboardVisible({iosUseWillEvents: true})
|
const [isKeyboardVisible] = useIsKeyboardVisible({iosUseWillEvents: true})
|
||||||
const [isProcessing, setIsProcessing] = useState(false)
|
const [isProcessing, setIsProcessing] = useState(false)
|
||||||
const [processingState, setProcessingState] = useState('')
|
const [processingState, setProcessingState] = useState('')
|
||||||
@@ -134,27 +138,21 @@ export const ComposePost = observer(function ComposePost({
|
|||||||
|
|
||||||
const onPressCancel = useCallback(() => {
|
const onPressCancel = useCallback(() => {
|
||||||
if (graphemeLength > 0 || !gallery.isEmpty) {
|
if (graphemeLength > 0 || !gallery.isEmpty) {
|
||||||
if (activeModals.some(modal => modal.name === 'confirm')) {
|
closeAllDialogs()
|
||||||
closeModal()
|
|
||||||
}
|
|
||||||
if (Keyboard) {
|
if (Keyboard) {
|
||||||
Keyboard.dismiss()
|
Keyboard.dismiss()
|
||||||
}
|
}
|
||||||
openModal({
|
discardPromptControl.open()
|
||||||
name: 'confirm',
|
|
||||||
title: _(msg`Discard draft`),
|
|
||||||
onPressConfirm: onClose,
|
|
||||||
onPressCancel: () => {
|
|
||||||
closeModal()
|
|
||||||
},
|
|
||||||
message: _(msg`Are you sure you'd like to discard this draft?`),
|
|
||||||
confirmBtnText: _(msg`Discard`),
|
|
||||||
confirmBtnStyle: {backgroundColor: colors.red4},
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
}, [openModal, closeModal, activeModals, onClose, graphemeLength, gallery, _])
|
}, [
|
||||||
|
graphemeLength,
|
||||||
|
gallery.isEmpty,
|
||||||
|
closeAllDialogs,
|
||||||
|
discardPromptControl,
|
||||||
|
onClose,
|
||||||
|
])
|
||||||
// android back button
|
// android back button
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isAndroid) {
|
if (!isAndroid) {
|
||||||
@@ -488,6 +486,15 @@ export const ComposePost = observer(function ComposePost({
|
|||||||
<CharProgress count={graphemeLength} />
|
<CharProgress count={graphemeLength} />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
<Prompt.Basic
|
||||||
|
control={discardPromptControl}
|
||||||
|
title={_(msg`Discard draft?`)}
|
||||||
|
description={_(msg`Are you sure you'd like to discard this draft?`)}
|
||||||
|
onConfirm={onClose}
|
||||||
|
confirmButtonCta={_(msg`Discard`)}
|
||||||
|
confirmButtonColor="negative"
|
||||||
|
/>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, {useCallback} from 'react'
|
import React, {useCallback} from 'react'
|
||||||
import {TouchableOpacity, StyleSheet} from 'react-native'
|
import {TouchableOpacity, StyleSheet} from 'react-native'
|
||||||
|
import * as MediaLibrary from 'expo-media-library'
|
||||||
import {
|
import {
|
||||||
FontAwesomeIcon,
|
FontAwesomeIcon,
|
||||||
FontAwesomeIconStyle,
|
FontAwesomeIconStyle,
|
||||||
@@ -24,6 +25,8 @@ export function OpenCameraBtn({gallery}: Props) {
|
|||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {requestCameraAccessIfNeeded} = useCameraPermission()
|
const {requestCameraAccessIfNeeded} = useCameraPermission()
|
||||||
|
const [mediaPermissionRes, requestMediaPermission] =
|
||||||
|
MediaLibrary.usePermissions()
|
||||||
|
|
||||||
const onPressTakePicture = useCallback(async () => {
|
const onPressTakePicture = useCallback(async () => {
|
||||||
track('Composer:CameraOpened')
|
track('Composer:CameraOpened')
|
||||||
@@ -31,6 +34,9 @@ export function OpenCameraBtn({gallery}: Props) {
|
|||||||
if (!(await requestCameraAccessIfNeeded())) {
|
if (!(await requestCameraAccessIfNeeded())) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!mediaPermissionRes?.granted && mediaPermissionRes?.canAskAgain) {
|
||||||
|
await requestMediaPermission()
|
||||||
|
}
|
||||||
|
|
||||||
const img = await openCamera({
|
const img = await openCamera({
|
||||||
width: POST_IMG_MAX.width,
|
width: POST_IMG_MAX.width,
|
||||||
@@ -38,12 +44,23 @@ export function OpenCameraBtn({gallery}: Props) {
|
|||||||
freeStyleCropEnabled: true,
|
freeStyleCropEnabled: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// If we don't have permissions it's fine, we just wont save it. The post itself will still have access to
|
||||||
|
// the image even without these permissions
|
||||||
|
if (mediaPermissionRes) {
|
||||||
|
await MediaLibrary.createAssetAsync(img.path)
|
||||||
|
}
|
||||||
gallery.add(img)
|
gallery.add(img)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// ignore
|
// ignore
|
||||||
logger.warn('Error using camera', {error: err})
|
logger.warn('Error using camera', {error: err})
|
||||||
}
|
}
|
||||||
}, [gallery, track, requestCameraAccessIfNeeded])
|
}, [
|
||||||
|
gallery,
|
||||||
|
track,
|
||||||
|
requestCameraAccessIfNeeded,
|
||||||
|
mediaPermissionRes,
|
||||||
|
requestMediaPermission,
|
||||||
|
])
|
||||||
|
|
||||||
const shouldShowCameraButton = isNative || isMobileWeb
|
const shouldShowCameraButton = isNative || isMobileWeb
|
||||||
if (!shouldShowCameraButton) {
|
if (!shouldShowCameraButton) {
|
||||||
|
|||||||
@@ -6,14 +6,11 @@ import {RichText} from '#/components/RichText'
|
|||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {s} from 'lib/styles'
|
import {s} from 'lib/styles'
|
||||||
import {UserAvatar} from '../util/UserAvatar'
|
import {UserAvatar} from '../util/UserAvatar'
|
||||||
import {useNavigation} from '@react-navigation/native'
|
|
||||||
import {NavigationProp} from 'lib/routes/types'
|
|
||||||
import {pluralize} from 'lib/strings/helpers'
|
import {pluralize} from 'lib/strings/helpers'
|
||||||
import {AtUri} from '@atproto/api'
|
import {AtUri} from '@atproto/api'
|
||||||
import * as Toast from 'view/com/util/Toast'
|
import * as Toast from 'view/com/util/Toast'
|
||||||
import {sanitizeHandle} from 'lib/strings/handles'
|
import {sanitizeHandle} from 'lib/strings/handles'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {useModalControls} from '#/state/modals'
|
|
||||||
import {Trans, msg} from '@lingui/macro'
|
import {Trans, msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {
|
import {
|
||||||
@@ -26,6 +23,8 @@ import {
|
|||||||
import {useFeedSourceInfoQuery, FeedSourceInfo} from '#/state/queries/feed'
|
import {useFeedSourceInfoQuery, FeedSourceInfo} from '#/state/queries/feed'
|
||||||
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||||
import {useTheme} from '#/alf'
|
import {useTheme} from '#/alf'
|
||||||
|
import * as Prompt from '#/components/Prompt'
|
||||||
|
import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped'
|
||||||
|
|
||||||
export function FeedSourceCard({
|
export function FeedSourceCard({
|
||||||
feedUri,
|
feedUri,
|
||||||
@@ -86,8 +85,8 @@ export function FeedSourceCardLoaded({
|
|||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const removePromptControl = Prompt.usePromptControl()
|
||||||
const {openModal} = useModalControls()
|
const navigation = useNavigationDeduped()
|
||||||
|
|
||||||
const {isPending: isSavePending, mutateAsync: saveFeed} =
|
const {isPending: isSavePending, mutateAsync: saveFeed} =
|
||||||
useSaveFeedMutation()
|
useSaveFeedMutation()
|
||||||
@@ -97,27 +96,9 @@ export function FeedSourceCardLoaded({
|
|||||||
|
|
||||||
const isSaved = Boolean(preferences?.feeds?.saved?.includes(feed?.uri || ''))
|
const isSaved = Boolean(preferences?.feeds?.saved?.includes(feed?.uri || ''))
|
||||||
|
|
||||||
const onToggleSaved = React.useCallback(async () => {
|
const onSave = React.useCallback(async () => {
|
||||||
// Only feeds can be un/saved, lists are handled elsewhere
|
if (!feed) return
|
||||||
if (feed?.type !== 'feed') return
|
|
||||||
|
|
||||||
if (isSaved) {
|
|
||||||
openModal({
|
|
||||||
name: 'confirm',
|
|
||||||
title: _(msg`Remove from my feeds`),
|
|
||||||
message: _(msg`Remove ${feed?.displayName} from my feeds?`),
|
|
||||||
onPressConfirm: async () => {
|
|
||||||
try {
|
|
||||||
await removeFeed({uri: feed.uri})
|
|
||||||
// await item.unsave()
|
|
||||||
Toast.show(_(msg`Removed from my feeds`))
|
|
||||||
} catch (e) {
|
|
||||||
Toast.show(_(msg`There was an issue contacting your server`))
|
|
||||||
logger.error('Failed to unsave feed', {message: e})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
try {
|
try {
|
||||||
if (pinOnSave) {
|
if (pinOnSave) {
|
||||||
await pinFeed({uri: feed.uri})
|
await pinFeed({uri: feed.uri})
|
||||||
@@ -129,8 +110,31 @@ export function FeedSourceCardLoaded({
|
|||||||
Toast.show(_(msg`There was an issue contacting your server`))
|
Toast.show(_(msg`There was an issue contacting your server`))
|
||||||
logger.error('Failed to save feed', {message: e})
|
logger.error('Failed to save feed', {message: e})
|
||||||
}
|
}
|
||||||
|
}, [_, feed, pinFeed, pinOnSave, saveFeed])
|
||||||
|
|
||||||
|
const onUnsave = React.useCallback(async () => {
|
||||||
|
if (!feed) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await removeFeed({uri: feed.uri})
|
||||||
|
// await item.unsave()
|
||||||
|
Toast.show(_(msg`Removed from my feeds`))
|
||||||
|
} catch (e) {
|
||||||
|
Toast.show(_(msg`There was an issue contacting your server`))
|
||||||
|
logger.error('Failed to unsave feed', {message: e})
|
||||||
}
|
}
|
||||||
}, [isSaved, openModal, feed, removeFeed, saveFeed, _, pinOnSave, pinFeed])
|
}, [_, feed, removeFeed])
|
||||||
|
|
||||||
|
const onToggleSaved = React.useCallback(async () => {
|
||||||
|
// Only feeds can be un/saved, lists are handled elsewhere
|
||||||
|
if (feed?.type !== 'feed') return
|
||||||
|
|
||||||
|
if (isSaved) {
|
||||||
|
removePromptControl.open()
|
||||||
|
} else {
|
||||||
|
await onSave()
|
||||||
|
}
|
||||||
|
}, [feed?.type, isSaved, removePromptControl, onSave])
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* LOAD STATE
|
* LOAD STATE
|
||||||
@@ -168,25 +172,7 @@ export function FeedSourceCardLoaded({
|
|||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel={_(msg`Remove from my feeds`)}
|
accessibilityLabel={_(msg`Remove from my feeds`)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
onPress={() => {
|
onPress={onToggleSaved}
|
||||||
openModal({
|
|
||||||
name: 'confirm',
|
|
||||||
title: _(msg`Remove from my feeds`),
|
|
||||||
message: _(msg`Remove this feed from my feeds?`),
|
|
||||||
onPressConfirm: async () => {
|
|
||||||
try {
|
|
||||||
await removeFeed({uri: feedUri})
|
|
||||||
// await item.unsave()
|
|
||||||
Toast.show(_(msg`Removed from my feeds`))
|
|
||||||
} catch (e) {
|
|
||||||
Toast.show(
|
|
||||||
_(msg`There was an issue contacting your server`),
|
|
||||||
)
|
|
||||||
logger.error('Failed to unsave feed', {message: e})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
hitSlop={15}
|
hitSlop={15}
|
||||||
style={styles.btn}>
|
style={styles.btn}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
@@ -200,6 +186,7 @@ export function FeedSourceCardLoaded({
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<Pressable
|
<Pressable
|
||||||
testID={`feed-${feed.displayName}`}
|
testID={`feed-${feed.displayName}`}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
@@ -242,7 +229,9 @@ export function FeedSourceCardLoaded({
|
|||||||
disabled={isSavePending || isPinPending || isRemovePending}
|
disabled={isSavePending || isPinPending || isRemovePending}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel={
|
accessibilityLabel={
|
||||||
isSaved ? _(msg`Remove from my feeds`) : _(msg`Add to my feeds`)
|
isSaved
|
||||||
|
? _(msg`Remove from my feeds`)
|
||||||
|
: _(msg`Add to my feeds`)
|
||||||
}
|
}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
onPress={onToggleSaved}
|
onPress={onToggleSaved}
|
||||||
@@ -283,6 +272,18 @@ export function FeedSourceCardLoaded({
|
|||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
||||||
|
<Prompt.Basic
|
||||||
|
control={removePromptControl}
|
||||||
|
title={_(msg`Remove from my feeds?`)}
|
||||||
|
description={_(
|
||||||
|
msg`Are you sure you want to remove ${feed.displayName} from your feeds?`,
|
||||||
|
)}
|
||||||
|
onConfirm={onUnsave}
|
||||||
|
confirmButtonCta={_(msg`Remove`)}
|
||||||
|
confirmButtonColor="negative"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,132 +0,0 @@
|
|||||||
import React, {useState} from 'react'
|
|
||||||
import {
|
|
||||||
ActivityIndicator,
|
|
||||||
StyleSheet,
|
|
||||||
TouchableOpacity,
|
|
||||||
View,
|
|
||||||
} from 'react-native'
|
|
||||||
import {Text} from '../util/text/Text'
|
|
||||||
import {s, colors} from 'lib/styles'
|
|
||||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
|
||||||
import {cleanError} from 'lib/strings/errors'
|
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
|
||||||
import {isWeb} from 'platform/detection'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
import {Trans, msg} from '@lingui/macro'
|
|
||||||
import type {ConfirmModal} from '#/state/modals'
|
|
||||||
import {useModalControls} from '#/state/modals'
|
|
||||||
|
|
||||||
export const snapPoints = ['50%']
|
|
||||||
|
|
||||||
export function Component({
|
|
||||||
title,
|
|
||||||
message,
|
|
||||||
onPressConfirm,
|
|
||||||
onPressCancel,
|
|
||||||
confirmBtnText,
|
|
||||||
confirmBtnStyle,
|
|
||||||
cancelBtnText,
|
|
||||||
}: ConfirmModal) {
|
|
||||||
const pal = usePalette('default')
|
|
||||||
const {_} = useLingui()
|
|
||||||
const {closeModal} = useModalControls()
|
|
||||||
const [isProcessing, setIsProcessing] = useState<boolean>(false)
|
|
||||||
const [error, setError] = useState<string>('')
|
|
||||||
const onPress = async () => {
|
|
||||||
setError('')
|
|
||||||
setIsProcessing(true)
|
|
||||||
try {
|
|
||||||
await onPressConfirm()
|
|
||||||
closeModal()
|
|
||||||
return
|
|
||||||
} catch (e: any) {
|
|
||||||
setError(cleanError(e))
|
|
||||||
setIsProcessing(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<View testID="confirmModal" style={[pal.view, styles.container]}>
|
|
||||||
<Text type="title-xl" style={[pal.text, styles.title]}>
|
|
||||||
{title}
|
|
||||||
</Text>
|
|
||||||
{typeof message === 'string' ? (
|
|
||||||
<Text type="xl" style={[pal.textLight, styles.description]}>
|
|
||||||
{message}
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
message()
|
|
||||||
)}
|
|
||||||
{error ? (
|
|
||||||
<View style={s.mt10}>
|
|
||||||
<ErrorMessage message={error} />
|
|
||||||
</View>
|
|
||||||
) : undefined}
|
|
||||||
<View style={s.flex1} />
|
|
||||||
{isProcessing ? (
|
|
||||||
<View style={[styles.btn, s.mt10]}>
|
|
||||||
<ActivityIndicator />
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<TouchableOpacity
|
|
||||||
testID="confirmBtn"
|
|
||||||
onPress={onPress}
|
|
||||||
style={[styles.btn, confirmBtnStyle]}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={_(msg({message: 'Confirm', context: 'action'}))}
|
|
||||||
accessibilityHint="">
|
|
||||||
<Text style={[s.white, s.bold, s.f18]}>
|
|
||||||
{confirmBtnText ?? <Trans context="action">Confirm</Trans>}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
{onPressCancel === undefined ? null : (
|
|
||||||
<TouchableOpacity
|
|
||||||
testID="cancelBtn"
|
|
||||||
onPress={onPressCancel}
|
|
||||||
style={[styles.btnCancel, s.mt10]}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={_(msg({message: 'Cancel', context: 'action'}))}
|
|
||||||
accessibilityHint="">
|
|
||||||
<Text type="button-lg" style={pal.textLight}>
|
|
||||||
{cancelBtnText ?? <Trans context="action">Cancel</Trans>}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
padding: 10,
|
|
||||||
paddingBottom: isWeb ? 0 : 60,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
textAlign: 'center',
|
|
||||||
marginBottom: 12,
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
textAlign: 'center',
|
|
||||||
paddingHorizontal: 22,
|
|
||||||
marginBottom: 10,
|
|
||||||
},
|
|
||||||
btn: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
borderRadius: 32,
|
|
||||||
padding: 14,
|
|
||||||
marginTop: 22,
|
|
||||||
marginHorizontal: 44,
|
|
||||||
backgroundColor: colors.blue3,
|
|
||||||
},
|
|
||||||
btnCancel: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
borderRadius: 32,
|
|
||||||
padding: 14,
|
|
||||||
marginHorizontal: 20,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -6,7 +6,6 @@ import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop'
|
|||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
|
||||||
import {useModals, useModalControls} from '#/state/modals'
|
import {useModals, useModalControls} from '#/state/modals'
|
||||||
import * as ConfirmModal from './Confirm'
|
|
||||||
import * as EditProfileModal from './EditProfile'
|
import * as EditProfileModal from './EditProfile'
|
||||||
import * as RepostModal from './Repost'
|
import * as RepostModal from './Repost'
|
||||||
import * as SelfLabelModal from './SelfLabel'
|
import * as SelfLabelModal from './SelfLabel'
|
||||||
@@ -62,10 +61,7 @@ export function ModalsContainer() {
|
|||||||
|
|
||||||
let snapPoints: (string | number)[] = DEFAULT_SNAPPOINTS
|
let snapPoints: (string | number)[] = DEFAULT_SNAPPOINTS
|
||||||
let element
|
let element
|
||||||
if (activeModal?.name === 'confirm') {
|
if (activeModal?.name === 'edit-profile') {
|
||||||
snapPoints = ConfirmModal.snapPoints
|
|
||||||
element = <ConfirmModal.Component {...activeModal} />
|
|
||||||
} else if (activeModal?.name === 'edit-profile') {
|
|
||||||
snapPoints = EditProfileModal.snapPoints
|
snapPoints = EditProfileModal.snapPoints
|
||||||
element = <EditProfileModal.Component {...activeModal} />
|
element = <EditProfileModal.Component {...activeModal} />
|
||||||
} else if (activeModal?.name === 'create-or-edit-list') {
|
} else if (activeModal?.name === 'create-or-edit-list') {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
|
|||||||
|
|
||||||
import {useModals, useModalControls} from '#/state/modals'
|
import {useModals, useModalControls} from '#/state/modals'
|
||||||
import type {Modal as ModalIface} from '#/state/modals'
|
import type {Modal as ModalIface} from '#/state/modals'
|
||||||
import * as ConfirmModal from './Confirm'
|
|
||||||
import * as EditProfileModal from './EditProfile'
|
import * as EditProfileModal from './EditProfile'
|
||||||
import * as CreateOrEditListModal from './CreateOrEditList'
|
import * as CreateOrEditListModal from './CreateOrEditList'
|
||||||
import * as UserAddRemoveLists from './UserAddRemoveLists'
|
import * as UserAddRemoveLists from './UserAddRemoveLists'
|
||||||
@@ -74,9 +73,7 @@ function Modal({modal}: {modal: ModalIface}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let element
|
let element
|
||||||
if (modal.name === 'confirm') {
|
if (modal.name === 'edit-profile') {
|
||||||
element = <ConfirmModal.Component {...modal} />
|
|
||||||
} else if (modal.name === 'edit-profile') {
|
|
||||||
element = <EditProfileModal.Component {...modal} />
|
element = <EditProfileModal.Component {...modal} />
|
||||||
} else if (modal.name === 'create-or-edit-list') {
|
} else if (modal.name === 'create-or-edit-list') {
|
||||||
element = <CreateOrEditListModal.Component {...modal} />
|
element = <CreateOrEditListModal.Component {...modal} />
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ import {usePalette} from 'lib/hooks/usePalette'
|
|||||||
import {useNavigation} from '@react-navigation/native'
|
import {useNavigation} from '@react-navigation/native'
|
||||||
import {NavigationProp} from 'lib/routes/types'
|
import {NavigationProp} from 'lib/routes/types'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {useModalControls} from '#/state/modals'
|
|
||||||
import {msg as msgLingui, Trans} from '@lingui/macro'
|
import {msg as msgLingui, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {FeedDescriptor} from '#/state/queries/post-feed'
|
import {FeedDescriptor} from '#/state/queries/post-feed'
|
||||||
import {EmptyState} from '../util/EmptyState'
|
import {EmptyState} from '../util/EmptyState'
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
import {useRemoveFeedMutation} from '#/state/queries/preferences'
|
import {useRemoveFeedMutation} from '#/state/queries/preferences'
|
||||||
|
import * as Prompt from '#/components/Prompt'
|
||||||
|
|
||||||
export enum KnownError {
|
export enum KnownError {
|
||||||
Block = 'Block',
|
Block = 'Block',
|
||||||
@@ -118,19 +118,18 @@ function FeedgenErrorMessage({
|
|||||||
)
|
)
|
||||||
const [_, uri] = feedDesc.split('|')
|
const [_, uri] = feedDesc.split('|')
|
||||||
const [ownerDid] = safeParseFeedgenUri(uri)
|
const [ownerDid] = safeParseFeedgenUri(uri)
|
||||||
const {openModal, closeModal} = useModalControls()
|
const removePromptControl = Prompt.usePromptControl()
|
||||||
const {mutateAsync: removeFeed} = useRemoveFeedMutation()
|
const {mutateAsync: removeFeed} = useRemoveFeedMutation()
|
||||||
|
|
||||||
const onViewProfile = React.useCallback(() => {
|
const onViewProfile = React.useCallback(() => {
|
||||||
navigation.navigate('Profile', {name: ownerDid})
|
navigation.navigate('Profile', {name: ownerDid})
|
||||||
}, [navigation, ownerDid])
|
}, [navigation, ownerDid])
|
||||||
|
|
||||||
|
const onPressRemoveFeed = React.useCallback(() => {
|
||||||
|
removePromptControl.open()
|
||||||
|
}, [removePromptControl])
|
||||||
|
|
||||||
const onRemoveFeed = React.useCallback(async () => {
|
const onRemoveFeed = React.useCallback(async () => {
|
||||||
openModal({
|
|
||||||
name: 'confirm',
|
|
||||||
title: _l(msgLingui`Remove feed`),
|
|
||||||
message: _l(msgLingui`Remove this feed from your saved feeds?`),
|
|
||||||
async onPressConfirm() {
|
|
||||||
try {
|
try {
|
||||||
await removeFeed({uri})
|
await removeFeed({uri})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -141,12 +140,7 @@ function FeedgenErrorMessage({
|
|||||||
)
|
)
|
||||||
logger.error('Failed to remove feed', {message: err})
|
logger.error('Failed to remove feed', {message: err})
|
||||||
}
|
}
|
||||||
},
|
}, [uri, removeFeed, _l])
|
||||||
onPressCancel() {
|
|
||||||
closeModal()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}, [openModal, closeModal, uri, removeFeed, _l])
|
|
||||||
|
|
||||||
const cta = React.useMemo(() => {
|
const cta = React.useMemo(() => {
|
||||||
switch (knownError) {
|
switch (knownError) {
|
||||||
@@ -179,6 +173,7 @@ function FeedgenErrorMessage({
|
|||||||
}, [knownError, onViewProfile, onRemoveFeed, _l])
|
}, [knownError, onViewProfile, onRemoveFeed, _l])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
pal.border,
|
pal.border,
|
||||||
@@ -200,6 +195,16 @@ function FeedgenErrorMessage({
|
|||||||
|
|
||||||
{cta}
|
{cta}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
<Prompt.Basic
|
||||||
|
control={removePromptControl}
|
||||||
|
title={_l(msgLingui`Remove feed?`)}
|
||||||
|
description={_l(msgLingui`Remove this feed from your saved feeds`)}
|
||||||
|
onConfirm={onPressRemoveFeed}
|
||||||
|
confirmButtonCta={_l(msgLingui`Remove`)}
|
||||||
|
confirmButtonColor="negative"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,344 @@
|
|||||||
|
import React, {memo} from 'react'
|
||||||
|
import {TouchableOpacity} from 'react-native'
|
||||||
|
import {AppBskyActorDefs} from '@atproto/api'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
import * as Toast from 'view/com/util/Toast'
|
||||||
|
import {EventStopper} from 'view/com/util/EventStopper'
|
||||||
|
import {useSession} from 'state/session'
|
||||||
|
import * as Menu from '#/components/Menu'
|
||||||
|
import {useTheme} from '#/alf'
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {HITSLOP_10} from 'lib/constants'
|
||||||
|
import {shareUrl} from 'lib/sharing'
|
||||||
|
import {toShareUrl} from 'lib/strings/url-helpers'
|
||||||
|
import {makeProfileLink} from 'lib/routes/links'
|
||||||
|
import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
|
import {useModalControls} from 'state/modals'
|
||||||
|
import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
|
||||||
|
import {
|
||||||
|
RQKEY as profileQueryKey,
|
||||||
|
useProfileBlockMutationQueue,
|
||||||
|
useProfileFollowMutationQueue,
|
||||||
|
useProfileMuteMutationQueue,
|
||||||
|
} from 'state/queries/profile'
|
||||||
|
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
|
||||||
|
import {ListSparkle_Stroke2_Corner0_Rounded as List} from '#/components/icons/ListSparkle'
|
||||||
|
import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
|
||||||
|
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
|
||||||
|
import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag'
|
||||||
|
import {PersonCheck_Stroke2_Corner0_Rounded as PersonCheck} from '#/components/icons/PersonCheck'
|
||||||
|
import {PersonX_Stroke2_Corner0_Rounded as PersonX} from '#/components/icons/PersonX'
|
||||||
|
import {PeopleRemove2_Stroke2_Corner0_Rounded as UserMinus} from '#/components/icons/PeopleRemove2'
|
||||||
|
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||||
|
import {logger} from '#/logger'
|
||||||
|
import {Shadow} from 'state/cache/types'
|
||||||
|
import * as Prompt from '#/components/Prompt'
|
||||||
|
|
||||||
|
let ProfileMenu = ({
|
||||||
|
profile,
|
||||||
|
}: {
|
||||||
|
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
|
||||||
|
}): React.ReactNode => {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const {currentAccount, hasSession} = useSession()
|
||||||
|
const t = useTheme()
|
||||||
|
// TODO ALF this
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const {track} = useAnalytics()
|
||||||
|
const {openModal} = useModalControls()
|
||||||
|
const reportDialogControl = useReportDialogControl()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const isSelf = currentAccount?.did === profile.did
|
||||||
|
|
||||||
|
const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile)
|
||||||
|
const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
|
||||||
|
const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile)
|
||||||
|
|
||||||
|
const blockPromptControl = Prompt.usePromptControl()
|
||||||
|
|
||||||
|
const invalidateProfileQuery = React.useCallback(() => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: profileQueryKey(profile.did),
|
||||||
|
})
|
||||||
|
}, [queryClient, profile.did])
|
||||||
|
|
||||||
|
const onPressShare = React.useCallback(() => {
|
||||||
|
track('ProfileHeader:ShareButtonClicked')
|
||||||
|
shareUrl(toShareUrl(makeProfileLink(profile)))
|
||||||
|
}, [track, profile])
|
||||||
|
|
||||||
|
const onPressAddRemoveLists = React.useCallback(() => {
|
||||||
|
track('ProfileHeader:AddToListsButtonClicked')
|
||||||
|
openModal({
|
||||||
|
name: 'user-add-remove-lists',
|
||||||
|
subject: profile.did,
|
||||||
|
handle: profile.handle,
|
||||||
|
displayName: profile.displayName || profile.handle,
|
||||||
|
onAdd: invalidateProfileQuery,
|
||||||
|
onRemove: invalidateProfileQuery,
|
||||||
|
})
|
||||||
|
}, [track, profile, openModal, invalidateProfileQuery])
|
||||||
|
|
||||||
|
const onPressMuteAccount = React.useCallback(async () => {
|
||||||
|
if (profile.viewer?.muted) {
|
||||||
|
track('ProfileHeader:UnmuteAccountButtonClicked')
|
||||||
|
try {
|
||||||
|
await queueUnmute()
|
||||||
|
Toast.show(_(msg`Account unmuted`))
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e?.name !== 'AbortError') {
|
||||||
|
logger.error('Failed to unmute account', {message: e})
|
||||||
|
Toast.show(_(msg`There was an issue! ${e.toString()}`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
track('ProfileHeader:MuteAccountButtonClicked')
|
||||||
|
try {
|
||||||
|
await queueMute()
|
||||||
|
Toast.show(_(msg`Account muted`))
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e?.name !== 'AbortError') {
|
||||||
|
logger.error('Failed to mute account', {message: e})
|
||||||
|
Toast.show(_(msg`There was an issue! ${e.toString()}`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [profile.viewer?.muted, track, queueUnmute, _, queueMute])
|
||||||
|
|
||||||
|
const blockAccount = React.useCallback(async () => {
|
||||||
|
if (profile.viewer?.blocking) {
|
||||||
|
track('ProfileHeader:UnblockAccountButtonClicked')
|
||||||
|
try {
|
||||||
|
await queueUnblock()
|
||||||
|
Toast.show(_(msg`Account unblocked`))
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e?.name !== 'AbortError') {
|
||||||
|
logger.error('Failed to unblock account', {message: e})
|
||||||
|
Toast.show(_(msg`There was an issue! ${e.toString()}`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
track('ProfileHeader:BlockAccountButtonClicked')
|
||||||
|
try {
|
||||||
|
await queueBlock()
|
||||||
|
Toast.show(_(msg`Account blocked`))
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e?.name !== 'AbortError') {
|
||||||
|
logger.error('Failed to block account', {message: e})
|
||||||
|
Toast.show(_(msg`There was an issue! ${e.toString()}`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [profile.viewer?.blocking, track, _, queueUnblock, queueBlock])
|
||||||
|
|
||||||
|
const onPressFollowAccount = React.useCallback(async () => {
|
||||||
|
track('ProfileHeader:FollowButtonClicked')
|
||||||
|
try {
|
||||||
|
await queueFollow()
|
||||||
|
Toast.show(_(msg`Account followed`))
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e?.name !== 'AbortError') {
|
||||||
|
logger.error('Failed to follow account', {message: e})
|
||||||
|
Toast.show(_(msg`There was an issue! ${e.toString()}`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [_, queueFollow, track])
|
||||||
|
|
||||||
|
const onPressUnfollowAccount = React.useCallback(async () => {
|
||||||
|
track('ProfileHeader:UnfollowButtonClicked')
|
||||||
|
try {
|
||||||
|
await queueUnfollow()
|
||||||
|
Toast.show(_(msg`Account unfollowed`))
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e?.name !== 'AbortError') {
|
||||||
|
logger.error('Failed to unfollow account', {message: e})
|
||||||
|
Toast.show(_(msg`There was an issue! ${e.toString()}`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [_, queueUnfollow, track])
|
||||||
|
|
||||||
|
const onPressReportAccount = React.useCallback(() => {
|
||||||
|
track('ProfileHeader:ReportAccountButtonClicked')
|
||||||
|
reportDialogControl.open()
|
||||||
|
}, [track, reportDialogControl])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EventStopper onKeyDown={false}>
|
||||||
|
<Menu.Root>
|
||||||
|
<Menu.Trigger label={_(`More options`)}>
|
||||||
|
{({props}) => {
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
{...props}
|
||||||
|
hitSlop={HITSLOP_10}
|
||||||
|
testID="profileHeaderDropdownBtn"
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
paddingVertical: 10,
|
||||||
|
borderRadius: 50,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
},
|
||||||
|
pal.btn,
|
||||||
|
]}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="ellipsis"
|
||||||
|
size={20}
|
||||||
|
style={t.atoms.text}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</Menu.Trigger>
|
||||||
|
|
||||||
|
<Menu.Outer style={{minWidth: 170}}>
|
||||||
|
<Menu.Group>
|
||||||
|
<Menu.Item
|
||||||
|
testID="profileHeaderDropdownShareBtn"
|
||||||
|
label={_(msg`Share`)}
|
||||||
|
onPress={onPressShare}>
|
||||||
|
<Menu.ItemText>
|
||||||
|
<Trans>Share</Trans>
|
||||||
|
</Menu.ItemText>
|
||||||
|
<Menu.ItemIcon icon={Share} />
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Group>
|
||||||
|
{hasSession && (
|
||||||
|
<>
|
||||||
|
<Menu.Divider />
|
||||||
|
<Menu.Group>
|
||||||
|
{!!profile.associated?.labeler &&
|
||||||
|
!(profile.viewer?.blocking || profile.viewer?.blockedBy) && (
|
||||||
|
<Menu.Item
|
||||||
|
testID="profileHeaderDropdownFollowBtn"
|
||||||
|
label={_(msg`Follow Account`)}
|
||||||
|
onPress={onPressFollowAccount}>
|
||||||
|
<Menu.ItemText>
|
||||||
|
<Trans>Follow Account</Trans>
|
||||||
|
</Menu.ItemText>
|
||||||
|
<Menu.ItemIcon icon={Plus} />
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
<Menu.Item
|
||||||
|
testID="profileHeaderDropdownListAddRemoveBtn"
|
||||||
|
label={_(msg`Add to Lists`)}
|
||||||
|
onPress={onPressAddRemoveLists}>
|
||||||
|
<Menu.ItemText>
|
||||||
|
<Trans>Add to Lists</Trans>
|
||||||
|
</Menu.ItemText>
|
||||||
|
<Menu.ItemIcon icon={List} />
|
||||||
|
</Menu.Item>
|
||||||
|
{!isSelf && (
|
||||||
|
<>
|
||||||
|
{profile.viewer?.following &&
|
||||||
|
(profile.viewer.blocking || profile.viewer.blockedBy) && (
|
||||||
|
<Menu.Item
|
||||||
|
testID="profileHeaderDropdownUnfollowBtn"
|
||||||
|
label={_(msg`Unfollow Account`)}
|
||||||
|
onPress={onPressUnfollowAccount}>
|
||||||
|
<Menu.ItemText>
|
||||||
|
<Trans>Unfollow Account</Trans>
|
||||||
|
</Menu.ItemText>
|
||||||
|
<Menu.ItemIcon icon={UserMinus} />
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
{!profile.viewer?.blocking &&
|
||||||
|
!profile.viewer?.mutedByList && (
|
||||||
|
<Menu.Item
|
||||||
|
testID="profileHeaderDropdownMuteBtn"
|
||||||
|
label={
|
||||||
|
profile.viewer?.muted
|
||||||
|
? _(msg`Unmute Account`)
|
||||||
|
: _(msg`Mute Account`)
|
||||||
|
}
|
||||||
|
onPress={onPressMuteAccount}>
|
||||||
|
<Menu.ItemText>
|
||||||
|
{profile.viewer?.muted ? (
|
||||||
|
<Trans>Unmute Account</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>Mute Account</Trans>
|
||||||
|
)}
|
||||||
|
</Menu.ItemText>
|
||||||
|
<Menu.ItemIcon
|
||||||
|
icon={profile.viewer?.muted ? Unmute : Mute}
|
||||||
|
/>
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
{!profile.viewer?.blockingByList && (
|
||||||
|
<Menu.Item
|
||||||
|
testID="profileHeaderDropdownBlockBtn"
|
||||||
|
label={
|
||||||
|
profile.viewer
|
||||||
|
? _(msg`Unblock Account`)
|
||||||
|
: _(msg`Block Account`)
|
||||||
|
}
|
||||||
|
onPress={() => blockPromptControl.open()}>
|
||||||
|
<Menu.ItemText>
|
||||||
|
{profile.viewer?.blocking ? (
|
||||||
|
<Trans>Unblock Account</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>Block Account</Trans>
|
||||||
|
)}
|
||||||
|
</Menu.ItemText>
|
||||||
|
<Menu.ItemIcon
|
||||||
|
icon={
|
||||||
|
profile.viewer?.blocking ? PersonCheck : PersonX
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
<Menu.Item
|
||||||
|
testID="profileHeaderDropdownReportBtn"
|
||||||
|
label={_(msg`Report Account`)}
|
||||||
|
onPress={onPressReportAccount}>
|
||||||
|
<Menu.ItemText>
|
||||||
|
<Trans>Report Account</Trans>
|
||||||
|
</Menu.ItemText>
|
||||||
|
<Menu.ItemIcon icon={Flag} />
|
||||||
|
</Menu.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Menu.Group>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Menu.Outer>
|
||||||
|
</Menu.Root>
|
||||||
|
|
||||||
|
<ReportDialog
|
||||||
|
control={reportDialogControl}
|
||||||
|
params={{type: 'account', did: profile.did}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Prompt.Basic
|
||||||
|
control={blockPromptControl}
|
||||||
|
title={
|
||||||
|
profile.viewer?.blocking
|
||||||
|
? _(msg`Unblock Account?`)
|
||||||
|
: _(msg`Block Account?`)
|
||||||
|
}
|
||||||
|
description={
|
||||||
|
profile.viewer?.blocking
|
||||||
|
? _(
|
||||||
|
msg`The account will be able to interact with you after unblocking.`,
|
||||||
|
)
|
||||||
|
: _(
|
||||||
|
msg`Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onConfirm={blockAccount}
|
||||||
|
confirmButtonCta={
|
||||||
|
profile.viewer?.blocking ? _(msg`Unblock`) : _(msg`Block`)
|
||||||
|
}
|
||||||
|
confirmButtonColor="negative"
|
||||||
|
/>
|
||||||
|
</EventStopper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ProfileMenu = memo(ProfileMenu)
|
||||||
|
export {ProfileMenu}
|
||||||
@@ -11,14 +11,9 @@ import {
|
|||||||
TouchableWithoutFeedback,
|
TouchableWithoutFeedback,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {
|
import {useLinkProps, StackActions} from '@react-navigation/native'
|
||||||
useLinkProps,
|
|
||||||
useNavigation,
|
|
||||||
StackActions,
|
|
||||||
} from '@react-navigation/native'
|
|
||||||
import {Text} from './text/Text'
|
import {Text} from './text/Text'
|
||||||
import {TypographyVariant} from 'lib/ThemeContext'
|
import {TypographyVariant} from 'lib/ThemeContext'
|
||||||
import {NavigationProp} from 'lib/routes/types'
|
|
||||||
import {router} from '../../../routes'
|
import {router} from '../../../routes'
|
||||||
import {
|
import {
|
||||||
convertBskyAppUrlIfNeeded,
|
convertBskyAppUrlIfNeeded,
|
||||||
@@ -32,6 +27,10 @@ import FixedTouchableHighlight from '../pager/FixedTouchableHighlight'
|
|||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
import {useOpenLink} from '#/state/preferences/in-app-browser'
|
import {useOpenLink} from '#/state/preferences/in-app-browser'
|
||||||
import {WebAuxClickWrapper} from 'view/com/util/WebAuxClickWrapper'
|
import {WebAuxClickWrapper} from 'view/com/util/WebAuxClickWrapper'
|
||||||
|
import {
|
||||||
|
DebouncedNavigationProp,
|
||||||
|
useNavigationDeduped,
|
||||||
|
} from 'lib/hooks/useNavigationDeduped'
|
||||||
|
|
||||||
type Event =
|
type Event =
|
||||||
| React.MouseEvent<HTMLAnchorElement, MouseEvent>
|
| React.MouseEvent<HTMLAnchorElement, MouseEvent>
|
||||||
@@ -67,7 +66,7 @@ export const Link = memo(function Link({
|
|||||||
...props
|
...props
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const {closeModal} = useModalControls()
|
const {closeModal} = useModalControls()
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigationDeduped()
|
||||||
const anchorHref = asAnchor ? sanitizeUrl(href) : undefined
|
const anchorHref = asAnchor ? sanitizeUrl(href) : undefined
|
||||||
const openLink = useOpenLink()
|
const openLink = useOpenLink()
|
||||||
|
|
||||||
@@ -179,7 +178,7 @@ export const TextLink = memo(function TextLink({
|
|||||||
navigationAction?: 'push' | 'replace' | 'navigate'
|
navigationAction?: 'push' | 'replace' | 'navigate'
|
||||||
} & TextProps) {
|
} & TextProps) {
|
||||||
const {...props} = useLinkProps({to: sanitizeUrl(href)})
|
const {...props} = useLinkProps({to: sanitizeUrl(href)})
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigationDeduped()
|
||||||
const {openModal, closeModal} = useModalControls()
|
const {openModal, closeModal} = useModalControls()
|
||||||
const openLink = useOpenLink()
|
const openLink = useOpenLink()
|
||||||
|
|
||||||
@@ -341,7 +340,7 @@ const EXEMPT_PATHS = ['/robots.txt', '/security.txt', '/.well-known/']
|
|||||||
// -prf
|
// -prf
|
||||||
function onPressInner(
|
function onPressInner(
|
||||||
closeModal = () => {},
|
closeModal = () => {},
|
||||||
navigation: NavigationProp,
|
navigation: DebouncedNavigationProp,
|
||||||
href: string,
|
href: string,
|
||||||
navigationAction: 'push' | 'replace' | 'navigate' = 'push',
|
navigationAction: 'push' | 'replace' | 'navigate' = 'push',
|
||||||
openLink: (href: string) => void,
|
openLink: (href: string) => void,
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import React, {memo, useMemo} from 'react'
|
import React, {memo, useMemo} from 'react'
|
||||||
import {Image, StyleSheet, View} from 'react-native'
|
import {Image, StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||||
import Svg, {Circle, Rect, Path} from 'react-native-svg'
|
import Svg, {Circle, Rect, Path} from 'react-native-svg'
|
||||||
|
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import {HighPriorityImage} from 'view/com/util/images/Image'
|
|
||||||
import {ModerationUI} from '@atproto/api'
|
import {ModerationUI} from '@atproto/api'
|
||||||
|
|
||||||
|
import {HighPriorityImage} from 'view/com/util/images/Image'
|
||||||
import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
|
import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
|
||||||
import {
|
import {
|
||||||
usePhotoLibraryPermission,
|
usePhotoLibraryPermission,
|
||||||
@@ -11,12 +15,16 @@ import {
|
|||||||
} from 'lib/hooks/usePermissions'
|
} from 'lib/hooks/usePermissions'
|
||||||
import {colors} from 'lib/styles'
|
import {colors} from 'lib/styles'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {isWeb, isAndroid} from 'platform/detection'
|
import {isWeb, isAndroid, isNative} from 'platform/detection'
|
||||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
|
||||||
import {UserPreviewLink} from './UserPreviewLink'
|
import {UserPreviewLink} from './UserPreviewLink'
|
||||||
import {DropdownItem, NativeDropdown} from './forms/NativeDropdown'
|
import * as Menu from '#/components/Menu'
|
||||||
import {useLingui} from '@lingui/react'
|
import {
|
||||||
import {msg} from '@lingui/macro'
|
Camera_Stroke2_Corner0_Rounded as Camera,
|
||||||
|
Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled,
|
||||||
|
} from '#/components/icons/Camera'
|
||||||
|
import {StreamingLive_Stroke2_Corner0_Rounded as Library} from '#/components/icons/StreamingLive'
|
||||||
|
import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
|
||||||
|
import {useTheme} from '#/alf'
|
||||||
|
|
||||||
export type UserAvatarType = 'user' | 'algo' | 'list' | 'labeler'
|
export type UserAvatarType = 'user' | 'algo' | 'list' | 'labeler'
|
||||||
|
|
||||||
@@ -219,6 +227,7 @@ let EditableUserAvatar = ({
|
|||||||
avatar,
|
avatar,
|
||||||
onSelectNewAvatar,
|
onSelectNewAvatar,
|
||||||
}: EditableUserAvatarProps): React.ReactNode => {
|
}: EditableUserAvatarProps): React.ReactNode => {
|
||||||
|
const t = useTheme()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {requestCameraAccessIfNeeded} = useCameraPermission()
|
const {requestCameraAccessIfNeeded} = useCameraPermission()
|
||||||
@@ -239,20 +248,7 @@ let EditableUserAvatar = ({
|
|||||||
}
|
}
|
||||||
}, [type, size])
|
}, [type, size])
|
||||||
|
|
||||||
const dropdownItems = useMemo(
|
const onOpenCamera = React.useCallback(async () => {
|
||||||
() =>
|
|
||||||
[
|
|
||||||
!isWeb && {
|
|
||||||
testID: 'changeAvatarCameraBtn',
|
|
||||||
label: _(msg`Camera`),
|
|
||||||
icon: {
|
|
||||||
ios: {
|
|
||||||
name: 'camera',
|
|
||||||
},
|
|
||||||
android: 'ic_menu_camera',
|
|
||||||
web: 'camera',
|
|
||||||
},
|
|
||||||
onPress: async () => {
|
|
||||||
if (!(await requestCameraAccessIfNeeded())) {
|
if (!(await requestCameraAccessIfNeeded())) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -264,19 +260,9 @@ let EditableUserAvatar = ({
|
|||||||
cropperCircleOverlay: true,
|
cropperCircleOverlay: true,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
},
|
}, [onSelectNewAvatar, requestCameraAccessIfNeeded])
|
||||||
},
|
|
||||||
{
|
const onOpenLibrary = React.useCallback(async () => {
|
||||||
testID: 'changeAvatarLibraryBtn',
|
|
||||||
label: _(msg`Library`),
|
|
||||||
icon: {
|
|
||||||
ios: {
|
|
||||||
name: 'photo.on.rectangle.angled',
|
|
||||||
},
|
|
||||||
android: 'ic_menu_gallery',
|
|
||||||
web: 'gallery',
|
|
||||||
},
|
|
||||||
onPress: async () => {
|
|
||||||
if (!(await requestPhotoAccessIfNeeded())) {
|
if (!(await requestPhotoAccessIfNeeded())) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -298,41 +284,17 @@ let EditableUserAvatar = ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
onSelectNewAvatar(croppedImage)
|
onSelectNewAvatar(croppedImage)
|
||||||
},
|
}, [onSelectNewAvatar, requestPhotoAccessIfNeeded])
|
||||||
},
|
|
||||||
!!avatar && {
|
const onRemoveAvatar = React.useCallback(() => {
|
||||||
label: 'separator',
|
|
||||||
},
|
|
||||||
!!avatar && {
|
|
||||||
testID: 'changeAvatarRemoveBtn',
|
|
||||||
label: _(msg`Remove`),
|
|
||||||
icon: {
|
|
||||||
ios: {
|
|
||||||
name: 'trash',
|
|
||||||
},
|
|
||||||
android: 'ic_delete',
|
|
||||||
web: ['far', 'trash-can'],
|
|
||||||
},
|
|
||||||
onPress: async () => {
|
|
||||||
onSelectNewAvatar(null)
|
onSelectNewAvatar(null)
|
||||||
},
|
}, [onSelectNewAvatar])
|
||||||
},
|
|
||||||
].filter(Boolean) as DropdownItem[],
|
|
||||||
[
|
|
||||||
avatar,
|
|
||||||
onSelectNewAvatar,
|
|
||||||
requestCameraAccessIfNeeded,
|
|
||||||
requestPhotoAccessIfNeeded,
|
|
||||||
_,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NativeDropdown
|
<Menu.Root>
|
||||||
testID="changeAvatarBtn"
|
<Menu.Trigger label={_(msg`Edit avatar`)}>
|
||||||
items={dropdownItems}
|
{({props}) => (
|
||||||
accessibilityLabel={_(msg`Image options`)}
|
<TouchableOpacity {...props} activeOpacity={0.8}>
|
||||||
accessibilityHint="">
|
|
||||||
{avatar ? (
|
{avatar ? (
|
||||||
<HighPriorityImage
|
<HighPriorityImage
|
||||||
testID="userAvatarImage"
|
testID="userAvatarImage"
|
||||||
@@ -344,13 +306,57 @@ let EditableUserAvatar = ({
|
|||||||
<DefaultAvatar type={type} size={size} />
|
<DefaultAvatar type={type} size={size} />
|
||||||
)}
|
)}
|
||||||
<View style={[styles.editButtonContainer, pal.btn]}>
|
<View style={[styles.editButtonContainer, pal.btn]}>
|
||||||
<FontAwesomeIcon
|
<CameraFilled height={14} width={14} style={t.atoms.text} />
|
||||||
icon="camera"
|
|
||||||
size={12}
|
|
||||||
color={pal.text.color as string}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
</NativeDropdown>
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</Menu.Trigger>
|
||||||
|
<Menu.Outer showCancel>
|
||||||
|
<Menu.Group>
|
||||||
|
{isNative && (
|
||||||
|
<Menu.Item
|
||||||
|
testID="changeAvatarCameraBtn"
|
||||||
|
label={_(msg`Upload from Camera`)}
|
||||||
|
onPress={onOpenCamera}>
|
||||||
|
<Menu.ItemText>
|
||||||
|
<Trans>Upload from Camera</Trans>
|
||||||
|
</Menu.ItemText>
|
||||||
|
<Menu.ItemIcon icon={Camera} />
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Menu.Item
|
||||||
|
testID="changeAvatarLibraryBtn"
|
||||||
|
label={_(msg`Upload from Library`)}
|
||||||
|
onPress={onOpenLibrary}>
|
||||||
|
<Menu.ItemText>
|
||||||
|
{isNative ? (
|
||||||
|
<Trans>Upload from Library</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>Upload from Files</Trans>
|
||||||
|
)}
|
||||||
|
</Menu.ItemText>
|
||||||
|
<Menu.ItemIcon icon={Library} />
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Group>
|
||||||
|
{!!avatar && (
|
||||||
|
<>
|
||||||
|
<Menu.Divider />
|
||||||
|
<Menu.Group>
|
||||||
|
<Menu.Item
|
||||||
|
testID="changeAvatarRemoveBtn"
|
||||||
|
label={_(`Remove Avatar`)}
|
||||||
|
onPress={onRemoveAvatar}>
|
||||||
|
<Menu.ItemText>
|
||||||
|
<Trans>Remove Avatar</Trans>
|
||||||
|
</Menu.ItemText>
|
||||||
|
<Menu.ItemIcon icon={Trash} />
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Group>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Menu.Outer>
|
||||||
|
</Menu.Root>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
EditableUserAvatar = memo(EditableUserAvatar)
|
EditableUserAvatar = memo(EditableUserAvatar)
|
||||||
|
|||||||
@@ -1,21 +1,29 @@
|
|||||||
import React, {useMemo} from 'react'
|
import React from 'react'
|
||||||
import {StyleSheet, View} from 'react-native'
|
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
|
||||||
import {ModerationUI} from '@atproto/api'
|
import {ModerationUI} from '@atproto/api'
|
||||||
import {Image} from 'expo-image'
|
import {Image} from 'expo-image'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
|
||||||
import {colors} from 'lib/styles'
|
import {colors} from 'lib/styles'
|
||||||
import {useTheme} from 'lib/ThemeContext'
|
import {useTheme} from 'lib/ThemeContext'
|
||||||
|
import {useTheme as useAlfTheme} from '#/alf'
|
||||||
import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
|
import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
|
||||||
import {
|
import {
|
||||||
usePhotoLibraryPermission,
|
usePhotoLibraryPermission,
|
||||||
useCameraPermission,
|
useCameraPermission,
|
||||||
} from 'lib/hooks/usePermissions'
|
} from 'lib/hooks/usePermissions'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {isWeb, isAndroid} from 'platform/detection'
|
import {isAndroid, isNative} from 'platform/detection'
|
||||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||||
import {NativeDropdown, DropdownItem} from './forms/NativeDropdown'
|
import {EventStopper} from 'view/com/util/EventStopper'
|
||||||
|
import * as Menu from '#/components/Menu'
|
||||||
|
import {
|
||||||
|
Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled,
|
||||||
|
Camera_Stroke2_Corner0_Rounded as Camera,
|
||||||
|
} from '#/components/icons/Camera'
|
||||||
|
import {StreamingLive_Stroke2_Corner0_Rounded as Library} from '#/components/icons/StreamingLive'
|
||||||
|
import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
|
||||||
|
|
||||||
export function UserBanner({
|
export function UserBanner({
|
||||||
banner,
|
banner,
|
||||||
@@ -28,24 +36,12 @@ export function UserBanner({
|
|||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
const t = useAlfTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {requestCameraAccessIfNeeded} = useCameraPermission()
|
const {requestCameraAccessIfNeeded} = useCameraPermission()
|
||||||
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
|
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
|
||||||
|
|
||||||
const dropdownItems: DropdownItem[] = useMemo(
|
const onOpenCamera = React.useCallback(async () => {
|
||||||
() =>
|
|
||||||
[
|
|
||||||
!isWeb && {
|
|
||||||
testID: 'changeBannerCameraBtn',
|
|
||||||
label: _(msg`Camera`),
|
|
||||||
icon: {
|
|
||||||
ios: {
|
|
||||||
name: 'camera',
|
|
||||||
},
|
|
||||||
android: 'ic_menu_camera',
|
|
||||||
web: 'camera',
|
|
||||||
},
|
|
||||||
onPress: async () => {
|
|
||||||
if (!(await requestCameraAccessIfNeeded())) {
|
if (!(await requestCameraAccessIfNeeded())) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -55,19 +51,9 @@ export function UserBanner({
|
|||||||
height: 1000,
|
height: 1000,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
},
|
}, [onSelectNewBanner, requestCameraAccessIfNeeded])
|
||||||
},
|
|
||||||
{
|
const onOpenLibrary = React.useCallback(async () => {
|
||||||
testID: 'changeBannerLibraryBtn',
|
|
||||||
label: _(msg`Library`),
|
|
||||||
icon: {
|
|
||||||
ios: {
|
|
||||||
name: 'photo.on.rectangle.angled',
|
|
||||||
},
|
|
||||||
android: 'ic_menu_gallery',
|
|
||||||
web: 'gallery',
|
|
||||||
},
|
|
||||||
onPress: async () => {
|
|
||||||
if (!(await requestPhotoAccessIfNeeded())) {
|
if (!(await requestPhotoAccessIfNeeded())) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -84,39 +70,19 @@ export function UserBanner({
|
|||||||
height: 1000,
|
height: 1000,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
},
|
}, [onSelectNewBanner, requestPhotoAccessIfNeeded])
|
||||||
},
|
|
||||||
!!banner && {
|
const onRemoveBanner = React.useCallback(() => {
|
||||||
testID: 'changeBannerRemoveBtn',
|
|
||||||
label: _(msg`Remove`),
|
|
||||||
icon: {
|
|
||||||
ios: {
|
|
||||||
name: 'trash',
|
|
||||||
},
|
|
||||||
android: 'ic_delete',
|
|
||||||
web: ['far', 'trash-can'],
|
|
||||||
},
|
|
||||||
onPress: () => {
|
|
||||||
onSelectNewBanner?.(null)
|
onSelectNewBanner?.(null)
|
||||||
},
|
}, [onSelectNewBanner])
|
||||||
},
|
|
||||||
].filter(Boolean) as DropdownItem[],
|
|
||||||
[
|
|
||||||
banner,
|
|
||||||
onSelectNewBanner,
|
|
||||||
requestCameraAccessIfNeeded,
|
|
||||||
requestPhotoAccessIfNeeded,
|
|
||||||
_,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
// setUserBanner is only passed as prop on the EditProfile component
|
// setUserBanner is only passed as prop on the EditProfile component
|
||||||
return onSelectNewBanner ? (
|
return onSelectNewBanner ? (
|
||||||
<NativeDropdown
|
<EventStopper onKeyDown={false}>
|
||||||
testID="changeBannerBtn"
|
<Menu.Root>
|
||||||
items={dropdownItems}
|
<Menu.Trigger label={_(msg`Edit avatar`)}>
|
||||||
accessibilityLabel={_(msg`Image options`)}
|
{({props}) => (
|
||||||
accessibilityHint="">
|
<TouchableOpacity {...props} activeOpacity={0.8}>
|
||||||
{banner ? (
|
{banner ? (
|
||||||
<Image
|
<Image
|
||||||
testID="userBannerImage"
|
testID="userBannerImage"
|
||||||
@@ -132,14 +98,58 @@ export function UserBanner({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<View style={[styles.editButtonContainer, pal.btn]}>
|
<View style={[styles.editButtonContainer, pal.btn]}>
|
||||||
<FontAwesomeIcon
|
<CameraFilled height={14} width={14} style={t.atoms.text} />
|
||||||
icon="camera"
|
|
||||||
size={12}
|
|
||||||
style={{color: colors.white}}
|
|
||||||
color={pal.text.color as string}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
</NativeDropdown>
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</Menu.Trigger>
|
||||||
|
<Menu.Outer showCancel>
|
||||||
|
<Menu.Group>
|
||||||
|
{isNative && (
|
||||||
|
<Menu.Item
|
||||||
|
testID="changeBannerCameraBtn"
|
||||||
|
label={_(msg`Upload from Camera`)}
|
||||||
|
onPress={onOpenCamera}>
|
||||||
|
<Menu.ItemText>
|
||||||
|
<Trans>Upload from Camera</Trans>
|
||||||
|
</Menu.ItemText>
|
||||||
|
<Menu.ItemIcon icon={Camera} />
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Menu.Item
|
||||||
|
testID="changeBannerLibraryBtn"
|
||||||
|
label={_(msg`Upload from Library`)}
|
||||||
|
onPress={onOpenLibrary}>
|
||||||
|
<Menu.ItemText>
|
||||||
|
{isNative ? (
|
||||||
|
<Trans>Upload from Library</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>Upload from Files</Trans>
|
||||||
|
)}
|
||||||
|
</Menu.ItemText>
|
||||||
|
<Menu.ItemIcon icon={Library} />
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Group>
|
||||||
|
{!!banner && (
|
||||||
|
<>
|
||||||
|
<Menu.Divider />
|
||||||
|
<Menu.Group>
|
||||||
|
<Menu.Item
|
||||||
|
testID="changeBannerRemoveBtn"
|
||||||
|
label={_(`Remove Banner`)}
|
||||||
|
onPress={onRemoveBanner}>
|
||||||
|
<Menu.ItemText>
|
||||||
|
<Trans>Remove Banner</Trans>
|
||||||
|
</Menu.ItemText>
|
||||||
|
<Menu.ItemIcon icon={Trash} />
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Group>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Menu.Outer>
|
||||||
|
</Menu.Root>
|
||||||
|
</EventStopper>
|
||||||
) : banner &&
|
) : banner &&
|
||||||
!((moderation?.blur && isAndroid) /* android crashes with blur */) ? (
|
!((moderation?.blur && isAndroid) /* android crashes with blur */) ? (
|
||||||
<Image
|
<Image
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
import React, {memo} from 'react'
|
import React, {memo} from 'react'
|
||||||
import {
|
import {StyleProp, ViewStyle, Pressable, PressableProps} from 'react-native'
|
||||||
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'
|
||||||
@@ -20,7 +14,8 @@ import {useTheme} from 'lib/ThemeContext'
|
|||||||
import {shareUrl} from 'lib/sharing'
|
import {shareUrl} from 'lib/sharing'
|
||||||
import * as Toast from '../Toast'
|
import * as Toast from '../Toast'
|
||||||
import {EventStopper} from '../EventStopper'
|
import {EventStopper} from '../EventStopper'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useDialogControl} from '#/components/Dialog'
|
||||||
|
import * as Prompt from '#/components/Prompt'
|
||||||
import {makeProfileLink} from '#/lib/routes/links'
|
import {makeProfileLink} from '#/lib/routes/links'
|
||||||
import {CommonNavigatorParams} from '#/lib/routes/types'
|
import {CommonNavigatorParams} from '#/lib/routes/types'
|
||||||
import {getCurrentRoute} from 'lib/routes/helpers'
|
import {getCurrentRoute} from 'lib/routes/helpers'
|
||||||
@@ -36,8 +31,8 @@ import {useLingui} from '@lingui/react'
|
|||||||
import {useSession} from '#/state/session'
|
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 {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
|
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
|
||||||
|
import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
|
||||||
|
|
||||||
import {atoms as a, useTheme as useAlf} from '#/alf'
|
import {atoms as a, useTheme as useAlf} from '#/alf'
|
||||||
import * as Menu from '#/components/Menu'
|
import * as Menu from '#/components/Menu'
|
||||||
@@ -75,7 +70,6 @@ let PostDropdownBtn = ({
|
|||||||
const alf = useAlf()
|
const alf = useAlf()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const defaultCtrlColor = theme.palette.default.postCtrl
|
const defaultCtrlColor = theme.palette.default.postCtrl
|
||||||
const {openModal} = useModalControls()
|
|
||||||
const langPrefs = useLanguagePrefs()
|
const langPrefs = useLanguagePrefs()
|
||||||
const mutedThreads = useMutedThreads()
|
const mutedThreads = useMutedThreads()
|
||||||
const toggleThreadMute = useToggleThreadMute()
|
const toggleThreadMute = useToggleThreadMute()
|
||||||
@@ -83,9 +77,11 @@ let PostDropdownBtn = ({
|
|||||||
const hiddenPosts = useHiddenPosts()
|
const hiddenPosts = useHiddenPosts()
|
||||||
const {hidePost} = useHiddenPostsApi()
|
const {hidePost} = useHiddenPostsApi()
|
||||||
const openLink = useOpenLink()
|
const openLink = useOpenLink()
|
||||||
const reportDialogControl = useReportDialogControl()
|
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
const {mutedWordsDialogControl} = useGlobalDialogsControlContext()
|
const {mutedWordsDialogControl} = useGlobalDialogsControlContext()
|
||||||
|
const reportDialogControl = useReportDialogControl()
|
||||||
|
const deletePromptControl = useDialogControl()
|
||||||
|
const hidePromptControl = useDialogControl()
|
||||||
|
|
||||||
const rootUri = record.reply?.root?.uri || postUri
|
const rootUri = record.reply?.root?.uri || postUri
|
||||||
const isThreadMuted = mutedThreads.includes(rootUri)
|
const isThreadMuted = mutedThreads.includes(rootUri)
|
||||||
@@ -169,40 +165,22 @@ let PostDropdownBtn = ({
|
|||||||
}, [postUri, hidePost])
|
}, [postUri, hidePost])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<ReportDialog
|
|
||||||
control={reportDialogControl}
|
|
||||||
params={{
|
|
||||||
type: 'post',
|
|
||||||
uri: postUri,
|
|
||||||
cid: postCid,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EventStopper onKeyDown={false}>
|
<EventStopper onKeyDown={false}>
|
||||||
<Menu.Root>
|
<Menu.Root>
|
||||||
<Menu.Trigger label={_(msg`Open post options menu`)}>
|
<Menu.Trigger label={_(msg`Open post options menu`)}>
|
||||||
{({props, state}) => {
|
{({props, state}) => {
|
||||||
const styles = [
|
return (
|
||||||
style,
|
|
||||||
a.rounded_full,
|
|
||||||
(state.hovered || state.pressed) && [alf.atoms.bg_contrast_25],
|
|
||||||
]
|
|
||||||
return isWeb ? (
|
|
||||||
<View {...props} testID={testID} style={styles}>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon="ellipsis"
|
|
||||||
size={20}
|
|
||||||
color={defaultCtrlColor}
|
|
||||||
style={{pointerEvents: 'none'}}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<Pressable
|
<Pressable
|
||||||
{...props}
|
{...props}
|
||||||
hitSlop={hitSlop}
|
hitSlop={hitSlop}
|
||||||
testID={testID}
|
testID={testID}
|
||||||
style={styles}>
|
style={[
|
||||||
|
style,
|
||||||
|
a.rounded_full,
|
||||||
|
(state.hovered || state.pressed) && [
|
||||||
|
alf.atoms.bg_contrast_50,
|
||||||
|
],
|
||||||
|
]}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="ellipsis"
|
icon="ellipsis"
|
||||||
size={20}
|
size={20}
|
||||||
@@ -254,9 +232,7 @@ let PostDropdownBtn = ({
|
|||||||
<Menu.Item
|
<Menu.Item
|
||||||
testID="postDropdownMuteThreadBtn"
|
testID="postDropdownMuteThreadBtn"
|
||||||
label={
|
label={
|
||||||
isThreadMuted
|
isThreadMuted ? _(msg`Unmute thread`) : _(msg`Mute thread`)
|
||||||
? _(msg`Unmute thread`)
|
|
||||||
: _(msg`Mute thread`)
|
|
||||||
}
|
}
|
||||||
onPress={onToggleThreadMute}>
|
onPress={onToggleThreadMute}>
|
||||||
<Menu.ItemText>
|
<Menu.ItemText>
|
||||||
@@ -282,16 +258,7 @@ let PostDropdownBtn = ({
|
|||||||
<Menu.Item
|
<Menu.Item
|
||||||
testID="postDropdownHideBtn"
|
testID="postDropdownHideBtn"
|
||||||
label={_(msg`Hide post`)}
|
label={_(msg`Hide post`)}
|
||||||
onPress={() => {
|
onPress={hidePromptControl.open}>
|
||||||
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.ItemText>{_(msg`Hide post`)}</Menu.ItemText>
|
||||||
<Menu.ItemIcon icon={EyeSlash} position="right" />
|
<Menu.ItemIcon icon={EyeSlash} position="right" />
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
@@ -317,14 +284,7 @@ let PostDropdownBtn = ({
|
|||||||
<Menu.Item
|
<Menu.Item
|
||||||
testID="postDropdownDeleteBtn"
|
testID="postDropdownDeleteBtn"
|
||||||
label={_(msg`Delete post`)}
|
label={_(msg`Delete post`)}
|
||||||
onPress={() => {
|
onPress={deletePromptControl.open}>
|
||||||
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.ItemText>{_(msg`Delete post`)}</Menu.ItemText>
|
||||||
<Menu.ItemIcon icon={Trash} position="right" />
|
<Menu.ItemIcon icon={Trash} position="right" />
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
@@ -332,8 +292,35 @@ let PostDropdownBtn = ({
|
|||||||
</Menu.Group>
|
</Menu.Group>
|
||||||
</Menu.Outer>
|
</Menu.Outer>
|
||||||
</Menu.Root>
|
</Menu.Root>
|
||||||
|
|
||||||
|
<Prompt.Basic
|
||||||
|
control={deletePromptControl}
|
||||||
|
title={_(msg`Delete this post?`)}
|
||||||
|
description={_(
|
||||||
|
msg`If you remove this post, you won't be able to recover it.`,
|
||||||
|
)}
|
||||||
|
onConfirm={onDeletePost}
|
||||||
|
confirmButtonCta={_(msg`Delete`)}
|
||||||
|
confirmButtonColor="negative"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Prompt.Basic
|
||||||
|
control={hidePromptControl}
|
||||||
|
title={_(msg`Hide this post?`)}
|
||||||
|
description={_(msg`This post will be hidden from feeds.`)}
|
||||||
|
onConfirm={onHidePost}
|
||||||
|
confirmButtonCta={_(msg`Hide`)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ReportDialog
|
||||||
|
control={reportDialogControl}
|
||||||
|
params={{
|
||||||
|
type: 'post',
|
||||||
|
uri: postUri,
|
||||||
|
cid: postCid,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</EventStopper>
|
</EventStopper>
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ const styles = StyleSheet.create({
|
|||||||
btn: {
|
btn: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
flexGrow: 1,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderLeftWidth: 0,
|
borderLeftWidth: 0,
|
||||||
paddingHorizontal: 10,
|
paddingHorizontal: 10,
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
|
import Animated from 'react-native-reanimated'
|
||||||
|
import {useMediaQuery} from 'react-responsive'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
import {colors} from 'lib/styles'
|
import {colors} from 'lib/styles'
|
||||||
import {HITSLOP_20} from 'lib/constants'
|
import {HITSLOP_20} from 'lib/constants'
|
||||||
import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
|
import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
|
||||||
import Animated from 'react-native-reanimated'
|
|
||||||
const AnimatedTouchableOpacity =
|
const AnimatedTouchableOpacity =
|
||||||
Animated.createAnimatedComponent(TouchableOpacity)
|
Animated.createAnimatedComponent(TouchableOpacity)
|
||||||
import {isWeb} from 'platform/detection'
|
import {isWeb} from 'platform/detection'
|
||||||
@@ -26,6 +27,9 @@ export function LoadLatestBtn({
|
|||||||
const {isDesktop, isTablet, isMobile, isTabletOrMobile} = useWebMediaQueries()
|
const {isDesktop, isTablet, isMobile, isTabletOrMobile} = useWebMediaQueries()
|
||||||
const {fabMinimalShellTransform} = useMinimalShellMode()
|
const {fabMinimalShellTransform} = useMinimalShellMode()
|
||||||
|
|
||||||
|
// move button inline if it starts overlapping the left nav
|
||||||
|
const isTallViewport = useMediaQuery({minHeight: 700})
|
||||||
|
|
||||||
// Adjust height of the fab if we have a session only on mobile web. If we don't have a session, we want to adjust
|
// Adjust height of the fab if we have a session only on mobile web. If we don't have a session, we want to adjust
|
||||||
// it on both tablet and mobile since we are showing the bottom bar (see createNativeStackNavigatorWithAuth)
|
// it on both tablet and mobile since we are showing the bottom bar (see createNativeStackNavigatorWithAuth)
|
||||||
const showBottomBar = hasSession ? isMobile : isTabletOrMobile
|
const showBottomBar = hasSession ? isMobile : isTabletOrMobile
|
||||||
@@ -34,8 +38,11 @@ export function LoadLatestBtn({
|
|||||||
<AnimatedTouchableOpacity
|
<AnimatedTouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.loadLatest,
|
styles.loadLatest,
|
||||||
isDesktop && styles.loadLatestDesktop,
|
isDesktop &&
|
||||||
isTablet && styles.loadLatestTablet,
|
(isTallViewport
|
||||||
|
? styles.loadLatestOutOfLine
|
||||||
|
: styles.loadLatestInline),
|
||||||
|
isTablet && styles.loadLatestInline,
|
||||||
pal.borderDark,
|
pal.borderDark,
|
||||||
pal.view,
|
pal.view,
|
||||||
showBottomBar && fabMinimalShellTransform,
|
showBottomBar && fabMinimalShellTransform,
|
||||||
@@ -65,11 +72,11 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
loadLatestTablet: {
|
loadLatestInline: {
|
||||||
// @ts-ignore web only
|
// @ts-ignore web only
|
||||||
left: 'calc(50vw - 282px)',
|
left: 'calc(50vw - 282px)',
|
||||||
},
|
},
|
||||||
loadLatestDesktop: {
|
loadLatestOutOfLine: {
|
||||||
// @ts-ignore web only
|
// @ts-ignore web only
|
||||||
left: 'calc(50vw - 382px)',
|
left: 'calc(50vw - 382px)',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ import {
|
|||||||
} from '#/state/queries/app-passwords'
|
} from '#/state/queries/app-passwords'
|
||||||
import {ErrorScreen} from '../com/util/error/ErrorScreen'
|
import {ErrorScreen} from '../com/util/error/ErrorScreen'
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
|
import * as Prompt from '#/components/Prompt'
|
||||||
|
import {useDialogControl} from '#/components/Dialog'
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppPasswords'>
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppPasswords'>
|
||||||
export function AppPasswords({}: Props) {
|
export function AppPasswords({}: Props) {
|
||||||
@@ -212,23 +214,18 @@ function AppPassword({
|
|||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {openModal} = useModalControls()
|
const control = useDialogControl()
|
||||||
const {contentLanguages} = useLanguagePrefs()
|
const {contentLanguages} = useLanguagePrefs()
|
||||||
const deleteMutation = useAppPasswordDeleteMutation()
|
const deleteMutation = useAppPasswordDeleteMutation()
|
||||||
|
|
||||||
const onDelete = React.useCallback(async () => {
|
const onDelete = React.useCallback(async () => {
|
||||||
openModal({
|
|
||||||
name: 'confirm',
|
|
||||||
title: _(msg`Delete app password`),
|
|
||||||
message: _(
|
|
||||||
msg`Are you sure you want to delete the app password "${name}"?`,
|
|
||||||
),
|
|
||||||
async onPressConfirm() {
|
|
||||||
await deleteMutation.mutateAsync({name})
|
await deleteMutation.mutateAsync({name})
|
||||||
Toast.show(_(msg`App password deleted`))
|
Toast.show(_(msg`App password deleted`))
|
||||||
},
|
}, [deleteMutation, name, _])
|
||||||
})
|
|
||||||
}, [deleteMutation, openModal, name, _])
|
const onPress = React.useCallback(() => {
|
||||||
|
control.open()
|
||||||
|
}, [control])
|
||||||
|
|
||||||
const primaryLocale =
|
const primaryLocale =
|
||||||
contentLanguages.length > 0 ? contentLanguages[0] : 'en-US'
|
contentLanguages.length > 0 ? contentLanguages[0] : 'en-US'
|
||||||
@@ -237,7 +234,7 @@ function AppPassword({
|
|||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID={testID}
|
testID={testID}
|
||||||
style={[styles.item, pal.border]}
|
style={[styles.item, pal.border]}
|
||||||
onPress={onDelete}
|
onPress={onPress}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel={_(msg`Delete app password`)}
|
accessibilityLabel={_(msg`Delete app password`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
@@ -260,6 +257,17 @@ function AppPassword({
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<FontAwesomeIcon icon={['far', 'trash-can']} style={styles.trashIcon} />
|
<FontAwesomeIcon icon={['far', 'trash-can']} style={styles.trashIcon} />
|
||||||
|
|
||||||
|
<Prompt.Basic
|
||||||
|
control={control}
|
||||||
|
title={_(msg`Delete app password?`)}
|
||||||
|
description={_(
|
||||||
|
msg`Are you sure you want to delete the app password "${name}"?`,
|
||||||
|
)}
|
||||||
|
onConfirm={onDelete}
|
||||||
|
confirmButtonCta={_(msg`Delete`)}
|
||||||
|
confirmButtonColor="negative"
|
||||||
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import React, {useMemo, useCallback} from 'react'
|
import React, {useMemo, useCallback} from 'react'
|
||||||
import {Dimensions, StyleSheet, View} from 'react-native'
|
import {StyleSheet, View, Pressable} from 'react-native'
|
||||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||||
import {useIsFocused, useNavigation} from '@react-navigation/native'
|
import {useIsFocused, useNavigation} from '@react-navigation/native'
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {HeartIcon, HeartIconSolid} from 'lib/icons'
|
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
|
||||||
import {CommonNavigatorParams} from 'lib/routes/types'
|
import {CommonNavigatorParams} from 'lib/routes/types'
|
||||||
import {makeRecordUri} from 'lib/strings/url-helpers'
|
import {makeRecordUri} from 'lib/strings/url-helpers'
|
||||||
import {s} from 'lib/styles'
|
import {s} from 'lib/styles'
|
||||||
@@ -13,7 +11,7 @@ import {FeedDescriptor} from '#/state/queries/post-feed'
|
|||||||
import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
|
import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
|
||||||
import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader'
|
import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader'
|
||||||
import {Feed} from 'view/com/posts/Feed'
|
import {Feed} from 'view/com/posts/Feed'
|
||||||
import {TextLink} from 'view/com/util/Link'
|
import {InlineLink} from '#/components/Link'
|
||||||
import {ListRef} from 'view/com/util/List'
|
import {ListRef} from 'view/com/util/List'
|
||||||
import {Button} from 'view/com/util/forms/Button'
|
import {Button} from 'view/com/util/forms/Button'
|
||||||
import {Text} from 'view/com/util/text/Text'
|
import {Text} from 'view/com/util/text/Text'
|
||||||
@@ -29,15 +27,10 @@ import {shareUrl} from 'lib/sharing'
|
|||||||
import {toShareUrl} from 'lib/strings/url-helpers'
|
import {toShareUrl} from 'lib/strings/url-helpers'
|
||||||
import {Haptics} from 'lib/haptics'
|
import {Haptics} from 'lib/haptics'
|
||||||
import {useAnalytics} from 'lib/analytics/analytics'
|
import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
import {NativeDropdown, DropdownItem} from 'view/com/util/forms/NativeDropdown'
|
|
||||||
import {useScrollHandlers} from '#/lib/ScrollContext'
|
|
||||||
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
|
|
||||||
import {makeCustomFeedLink} from 'lib/routes/links'
|
import {makeCustomFeedLink} from 'lib/routes/links'
|
||||||
import {pluralize} from 'lib/strings/helpers'
|
import {pluralize} from 'lib/strings/helpers'
|
||||||
import {CenteredView, ScrollView} from 'view/com/util/Views'
|
import {CenteredView} from 'view/com/util/Views'
|
||||||
import {NavigationProp} from 'lib/routes/types'
|
import {NavigationProp} from 'lib/routes/types'
|
||||||
import {sanitizeHandle} from 'lib/strings/handles'
|
|
||||||
import {makeProfileLink} from 'lib/routes/links'
|
|
||||||
import {ComposeIcon2} from 'lib/icons'
|
import {ComposeIcon2} from 'lib/icons'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {Trans, msg} from '@lingui/macro'
|
import {Trans, msg} from '@lingui/macro'
|
||||||
@@ -59,9 +52,21 @@ import {useComposerControls} from '#/state/shell/composer'
|
|||||||
import {truncateAndInvalidate} from '#/state/queries/util'
|
import {truncateAndInvalidate} from '#/state/queries/util'
|
||||||
import {isNative} from '#/platform/detection'
|
import {isNative} from '#/platform/detection'
|
||||||
import {listenSoftReset} from '#/state/events'
|
import {listenSoftReset} from '#/state/events'
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import * as Menu from '#/components/Menu'
|
||||||
|
import {HITSLOP_20} from '#/lib/constants'
|
||||||
|
import {DotGrid_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid'
|
||||||
|
import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
|
||||||
|
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||||
|
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
||||||
|
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
|
||||||
|
import {
|
||||||
|
Heart2_Stroke2_Corner0_Rounded as HeartOutline,
|
||||||
|
Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled,
|
||||||
|
} from '#/components/icons/Heart2'
|
||||||
|
import {Button as NewButton, ButtonText} from '#/components/Button'
|
||||||
|
|
||||||
const SECTION_TITLES = ['Posts', 'About']
|
const SECTION_TITLES = ['Posts']
|
||||||
|
|
||||||
interface SectionRef {
|
interface SectionRef {
|
||||||
scrollToTop: () => void
|
scrollToTop: () => void
|
||||||
@@ -148,7 +153,7 @@ export function ProfileFeedScreenInner({
|
|||||||
feedInfo: FeedSourceFeedInfo
|
feedInfo: FeedSourceFeedInfo
|
||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const pal = usePalette('default')
|
const t = useTheme()
|
||||||
const {hasSession, currentAccount} = useSession()
|
const {hasSession, currentAccount} = useSession()
|
||||||
const reportDialogControl = useReportDialogControl()
|
const reportDialogControl = useReportDialogControl()
|
||||||
const {openComposer} = useComposerControls()
|
const {openComposer} = useComposerControls()
|
||||||
@@ -200,9 +205,11 @@ export function ProfileFeedScreenInner({
|
|||||||
if (isSaved) {
|
if (isSaved) {
|
||||||
await removeFeed({uri: feedInfo.uri})
|
await removeFeed({uri: feedInfo.uri})
|
||||||
resetRemoveFeed()
|
resetRemoveFeed()
|
||||||
|
Toast.show(_(msg`Removed from your feeds`))
|
||||||
} else {
|
} else {
|
||||||
await saveFeed({uri: feedInfo.uri})
|
await saveFeed({uri: feedInfo.uri})
|
||||||
resetSaveFeed()
|
resetSaveFeed()
|
||||||
|
Toast.show(_(msg`Saved to your feeds`))
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Toast.show(
|
Toast.show(
|
||||||
@@ -258,69 +265,9 @@ export function ProfileFeedScreenInner({
|
|||||||
[feedSectionRef],
|
[feedSectionRef],
|
||||||
)
|
)
|
||||||
|
|
||||||
// render
|
|
||||||
// =
|
|
||||||
|
|
||||||
const dropdownItems: DropdownItem[] = React.useMemo(() => {
|
|
||||||
return [
|
|
||||||
hasSession && {
|
|
||||||
testID: 'feedHeaderDropdownToggleSavedBtn',
|
|
||||||
label: isSaved ? _(msg`Remove from my feeds`) : _(msg`Add to my feeds`),
|
|
||||||
onPress: isSavePending || isRemovePending ? undefined : onToggleSaved,
|
|
||||||
icon: isSaved
|
|
||||||
? {
|
|
||||||
ios: {
|
|
||||||
name: 'trash',
|
|
||||||
},
|
|
||||||
android: 'ic_delete',
|
|
||||||
web: ['far', 'trash-can'],
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
ios: {
|
|
||||||
name: 'plus',
|
|
||||||
},
|
|
||||||
android: '',
|
|
||||||
web: 'plus',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
hasSession && {
|
|
||||||
testID: 'feedHeaderDropdownReportBtn',
|
|
||||||
label: _(msg`Report feed`),
|
|
||||||
onPress: onPressReport,
|
|
||||||
icon: {
|
|
||||||
ios: {
|
|
||||||
name: 'exclamationmark.triangle',
|
|
||||||
},
|
|
||||||
android: 'ic_menu_report_image',
|
|
||||||
web: 'circle-exclamation',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
testID: 'feedHeaderDropdownShareBtn',
|
|
||||||
label: _(msg`Share feed`),
|
|
||||||
onPress: onPressShare,
|
|
||||||
icon: {
|
|
||||||
ios: {
|
|
||||||
name: 'square.and.arrow.up',
|
|
||||||
},
|
|
||||||
android: 'ic_menu_share',
|
|
||||||
web: 'share',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
].filter(Boolean) as DropdownItem[]
|
|
||||||
}, [
|
|
||||||
hasSession,
|
|
||||||
onToggleSaved,
|
|
||||||
onPressReport,
|
|
||||||
onPressShare,
|
|
||||||
isSaved,
|
|
||||||
isSavePending,
|
|
||||||
isRemovePending,
|
|
||||||
_,
|
|
||||||
])
|
|
||||||
|
|
||||||
const renderHeader = useCallback(() => {
|
const renderHeader = useCallback(() => {
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<ProfileSubpageHeader
|
<ProfileSubpageHeader
|
||||||
isLoading={false}
|
isLoading={false}
|
||||||
href={feedInfo.route.href}
|
href={feedInfo.route.href}
|
||||||
@@ -333,55 +280,117 @@ export function ProfileFeedScreenInner({
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
avatarType="algo">
|
avatarType="algo">
|
||||||
|
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
|
||||||
{feedInfo && hasSession && (
|
{feedInfo && hasSession && (
|
||||||
<>
|
<NewButton
|
||||||
<Button
|
|
||||||
disabled={isSavePending || isRemovePending}
|
|
||||||
type="default"
|
|
||||||
label={isSaved ? _(msg`Unsave`) : _(msg`Save`)}
|
|
||||||
onPress={onToggleSaved}
|
|
||||||
style={styles.btn}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
testID={isPinned ? 'unpinBtn' : 'pinBtn'}
|
testID={isPinned ? 'unpinBtn' : 'pinBtn'}
|
||||||
disabled={isPinPending || isUnpinPending}
|
disabled={isPinPending || isUnpinPending}
|
||||||
type={isPinned ? 'default' : 'inverted'}
|
size="small"
|
||||||
label={isPinned ? _(msg`Unpin`) : _(msg`Pin to home`)}
|
variant="solid"
|
||||||
onPress={onTogglePinned}
|
color={isPinned ? 'secondary' : 'primary'}
|
||||||
style={styles.btn}
|
label={isPinned ? _(msg`Unpin from home`) : _(msg`Pin to home`)}
|
||||||
|
onPress={onTogglePinned}>
|
||||||
|
<ButtonText>
|
||||||
|
{isPinned ? _(msg`Unpin`) : _(msg`Pin to Home`)}
|
||||||
|
</ButtonText>
|
||||||
|
</NewButton>
|
||||||
|
)}
|
||||||
|
<Menu.Root>
|
||||||
|
<Menu.Trigger label={_(msg`Open feed options menu`)}>
|
||||||
|
{({props, state}) => {
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
{...props}
|
||||||
|
hitSlop={HITSLOP_20}
|
||||||
|
style={[
|
||||||
|
a.justify_center,
|
||||||
|
a.align_center,
|
||||||
|
a.rounded_full,
|
||||||
|
{height: 36, width: 36},
|
||||||
|
t.atoms.bg_contrast_50,
|
||||||
|
(state.hovered || state.pressed) && [
|
||||||
|
t.atoms.bg_contrast_100,
|
||||||
|
],
|
||||||
|
]}
|
||||||
|
testID="headerDropdownBtn">
|
||||||
|
<Ellipsis
|
||||||
|
size="lg"
|
||||||
|
fill={t.atoms.text_contrast_medium.color}
|
||||||
/>
|
/>
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</Menu.Trigger>
|
||||||
|
|
||||||
|
<Menu.Outer>
|
||||||
|
<Menu.Group>
|
||||||
|
{hasSession && (
|
||||||
|
<>
|
||||||
|
<Menu.Item
|
||||||
|
disabled={isSavePending || isRemovePending}
|
||||||
|
testID="feedHeaderDropdownToggleSavedBtn"
|
||||||
|
label={
|
||||||
|
isSaved
|
||||||
|
? _(msg`Remove from my feeds`)
|
||||||
|
: _(msg`Save to my feeds`)
|
||||||
|
}
|
||||||
|
onPress={onToggleSaved}>
|
||||||
|
<Menu.ItemText>
|
||||||
|
{isSaved
|
||||||
|
? _(msg`Remove from my feeds`)
|
||||||
|
: _(msg`Save to my feeds`)}
|
||||||
|
</Menu.ItemText>
|
||||||
|
<Menu.ItemIcon
|
||||||
|
icon={isSaved ? Trash : Plus}
|
||||||
|
position="right"
|
||||||
|
/>
|
||||||
|
</Menu.Item>
|
||||||
|
|
||||||
|
<Menu.Item
|
||||||
|
testID="feedHeaderDropdownReportBtn"
|
||||||
|
label={_(msg`Report feed`)}
|
||||||
|
onPress={onPressReport}>
|
||||||
|
<Menu.ItemText>{_(msg`Report feed`)}</Menu.ItemText>
|
||||||
|
<Menu.ItemIcon icon={CircleInfo} position="right" />
|
||||||
|
</Menu.Item>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<NativeDropdown
|
|
||||||
testID="headerDropdownBtn"
|
<Menu.Item
|
||||||
items={dropdownItems}
|
testID="feedHeaderDropdownShareBtn"
|
||||||
accessibilityLabel={_(msg`More options`)}
|
label={_(msg`Share feed`)}
|
||||||
accessibilityHint="">
|
onPress={onPressShare}>
|
||||||
<View style={[pal.viewLight, styles.btn]}>
|
<Menu.ItemText>{_(msg`Share feed`)}</Menu.ItemText>
|
||||||
<FontAwesomeIcon
|
<Menu.ItemIcon icon={Share} position="right" />
|
||||||
icon="ellipsis"
|
</Menu.Item>
|
||||||
size={20}
|
</Menu.Group>
|
||||||
color={pal.colors.text}
|
</Menu.Outer>
|
||||||
/>
|
</Menu.Root>
|
||||||
</View>
|
</View>
|
||||||
</NativeDropdown>
|
|
||||||
</ProfileSubpageHeader>
|
</ProfileSubpageHeader>
|
||||||
|
<AboutSection
|
||||||
|
feedOwnerDid={feedInfo.creatorDid}
|
||||||
|
feedRkey={feedInfo.route.params.rkey}
|
||||||
|
feedInfo={feedInfo}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}, [
|
}, [
|
||||||
_,
|
_,
|
||||||
hasSession,
|
hasSession,
|
||||||
pal,
|
|
||||||
feedInfo,
|
feedInfo,
|
||||||
isPinned,
|
isPinned,
|
||||||
onTogglePinned,
|
onTogglePinned,
|
||||||
onToggleSaved,
|
onToggleSaved,
|
||||||
dropdownItems,
|
|
||||||
currentAccount?.did,
|
currentAccount?.did,
|
||||||
isPinPending,
|
isPinPending,
|
||||||
isRemovePending,
|
isRemovePending,
|
||||||
isSavePending,
|
isSavePending,
|
||||||
isSaved,
|
isSaved,
|
||||||
isUnpinPending,
|
isUnpinPending,
|
||||||
|
onPressReport,
|
||||||
|
onPressShare,
|
||||||
|
t,
|
||||||
])
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -408,18 +417,6 @@ export function ProfileFeedScreenInner({
|
|||||||
isFocused={isScreenFocused && isFocused}
|
isFocused={isScreenFocused && isFocused}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{({headerHeight, scrollElRef}) => (
|
|
||||||
<AboutSection
|
|
||||||
feedOwnerDid={feedInfo.creatorDid}
|
|
||||||
feedRkey={feedInfo.route.params.rkey}
|
|
||||||
feedInfo={feedInfo}
|
|
||||||
headerHeight={headerHeight}
|
|
||||||
scrollElRef={
|
|
||||||
scrollElRef as React.MutableRefObject<ScrollView | null>
|
|
||||||
}
|
|
||||||
isOwner={feedInfo.creatorDid === currentAccount?.did}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</PagerWithHeader>
|
</PagerWithHeader>
|
||||||
{hasSession && (
|
{hasSession && (
|
||||||
<FAB
|
<FAB
|
||||||
@@ -508,21 +505,14 @@ function AboutSection({
|
|||||||
feedOwnerDid,
|
feedOwnerDid,
|
||||||
feedRkey,
|
feedRkey,
|
||||||
feedInfo,
|
feedInfo,
|
||||||
headerHeight,
|
|
||||||
scrollElRef,
|
|
||||||
isOwner,
|
|
||||||
}: {
|
}: {
|
||||||
feedOwnerDid: string
|
feedOwnerDid: string
|
||||||
feedRkey: string
|
feedRkey: string
|
||||||
feedInfo: FeedSourceFeedInfo
|
feedInfo: FeedSourceFeedInfo
|
||||||
headerHeight: number
|
|
||||||
scrollElRef: React.MutableRefObject<ScrollView | null>
|
|
||||||
isOwner: boolean
|
|
||||||
}) {
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const scrollHandlers = useScrollHandlers()
|
|
||||||
const onScroll = useAnimatedScrollHandler(scrollHandlers)
|
|
||||||
const [likeUri, setLikeUri] = React.useState(feedInfo.likeUri)
|
const [likeUri, setLikeUri] = React.useState(feedInfo.likeUri)
|
||||||
const {hasSession} = useSession()
|
const {hasSession} = useSession()
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
@@ -558,24 +548,8 @@ function AboutSection({
|
|||||||
}, [likeUri, isLiked, feedInfo, likeFeed, unlikeFeed, track, _])
|
}, [likeUri, isLiked, feedInfo, likeFeed, unlikeFeed, track, _])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView
|
<View style={[styles.aboutSectionContainer]}>
|
||||||
ref={scrollElRef}
|
<View style={[a.pt_sm]}>
|
||||||
onScroll={onScroll}
|
|
||||||
scrollEventThrottle={1}
|
|
||||||
contentContainerStyle={{
|
|
||||||
paddingTop: headerHeight,
|
|
||||||
minHeight: Dimensions.get('window').height * 1.5,
|
|
||||||
}}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
{
|
|
||||||
borderTopWidth: 1,
|
|
||||||
paddingVertical: 20,
|
|
||||||
paddingHorizontal: 20,
|
|
||||||
gap: 12,
|
|
||||||
},
|
|
||||||
pal.border,
|
|
||||||
]}>
|
|
||||||
{feedInfo.description ? (
|
{feedInfo.description ? (
|
||||||
<RichText
|
<RichText
|
||||||
testID="listDescription"
|
testID="listDescription"
|
||||||
@@ -587,50 +561,34 @@ function AboutSection({
|
|||||||
<Trans>No description</Trans>
|
<Trans>No description</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
<View style={{flexDirection: 'row', alignItems: 'center', gap: 10}}>
|
</View>
|
||||||
<Button
|
|
||||||
type="default"
|
<View style={[a.flex_row, a.gap_sm, a.align_center, a.pb_sm]}>
|
||||||
|
<NewButton
|
||||||
|
size="small"
|
||||||
|
variant="solid"
|
||||||
|
color="secondary"
|
||||||
|
shape="round"
|
||||||
|
label={isLiked ? _(msg`Unlike this feed`) : _(msg`Like this feed`)}
|
||||||
testID="toggleLikeBtn"
|
testID="toggleLikeBtn"
|
||||||
accessibilityLabel={_(msg`Like this feed`)}
|
|
||||||
accessibilityHint=""
|
|
||||||
disabled={!hasSession || isLikePending || isUnlikePending}
|
disabled={!hasSession || isLikePending || isUnlikePending}
|
||||||
onPress={onToggleLiked}
|
onPress={onToggleLiked}>
|
||||||
style={{paddingHorizontal: 10}}>
|
|
||||||
{isLiked ? (
|
{isLiked ? (
|
||||||
<HeartIconSolid size={19} style={s.likeColor} />
|
<HeartFilled size="md" fill={s.likeColor.color} />
|
||||||
) : (
|
) : (
|
||||||
<HeartIcon strokeWidth={3} size={19} style={pal.textLight} />
|
<HeartOutline size="md" fill={t.atoms.text_contrast_medium.color} />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</NewButton>
|
||||||
{typeof likeCount === 'number' && (
|
{typeof likeCount === 'number' && (
|
||||||
<TextLink
|
<InlineLink
|
||||||
href={makeCustomFeedLink(feedOwnerDid, feedRkey, 'liked-by')}
|
label={_(msg`View users who like this feed`)}
|
||||||
text={_(
|
to={makeCustomFeedLink(feedOwnerDid, feedRkey, 'liked-by')}
|
||||||
msg`Liked by ${likeCount} ${pluralize(likeCount, 'user')}`,
|
style={[t.atoms.text_contrast_medium, a.font_bold]}>
|
||||||
)}
|
{_(msg`Liked by ${likeCount} ${pluralize(likeCount, 'user')}`)}
|
||||||
style={[pal.textLight, s.semiBold]}
|
</InlineLink>
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<Text type="md" style={[pal.textLight]} numberOfLines={1}>
|
|
||||||
{isOwner ? (
|
|
||||||
<Trans>Created by you</Trans>
|
|
||||||
) : (
|
|
||||||
<Trans>
|
|
||||||
Created by{' '}
|
|
||||||
<TextLink
|
|
||||||
text={sanitizeHandle(feedInfo.creatorHandle, '@')}
|
|
||||||
href={makeProfileLink({
|
|
||||||
did: feedInfo.creatorDid,
|
|
||||||
handle: feedInfo.creatorHandle,
|
|
||||||
})}
|
|
||||||
style={pal.textLight}
|
|
||||||
/>
|
|
||||||
</Trans>
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -650,4 +608,9 @@ const styles = StyleSheet.create({
|
|||||||
paddingVertical: 14,
|
paddingVertical: 14,
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
},
|
},
|
||||||
|
aboutSectionContainer: {
|
||||||
|
paddingVertical: 4,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
gap: 12,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ import {logger} from '#/logger'
|
|||||||
import {useAnalytics} from '#/lib/analytics/analytics'
|
import {useAnalytics} from '#/lib/analytics/analytics'
|
||||||
import {listenSoftReset} from '#/state/events'
|
import {listenSoftReset} from '#/state/events'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import * as Prompt from '#/components/Prompt'
|
||||||
|
import {useDialogControl} from '#/components/Dialog'
|
||||||
|
|
||||||
const SECTION_TITLES_CURATE = ['Posts', 'About']
|
const SECTION_TITLES_CURATE = ['Posts', 'About']
|
||||||
const SECTION_TITLES_MOD = ['About']
|
const SECTION_TITLES_MOD = ['About']
|
||||||
@@ -235,8 +237,8 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
|
|||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const {openModal, closeModal} = useModalControls()
|
|
||||||
const reportDialogControl = useReportDialogControl()
|
const reportDialogControl = useReportDialogControl()
|
||||||
|
const {openModal} = useModalControls()
|
||||||
const listMuteMutation = useListMuteMutation()
|
const listMuteMutation = useListMuteMutation()
|
||||||
const listBlockMutation = useListBlockMutation()
|
const listBlockMutation = useListBlockMutation()
|
||||||
const listDeleteMutation = useListDeleteMutation()
|
const listDeleteMutation = useListDeleteMutation()
|
||||||
@@ -253,6 +255,10 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
|
|||||||
const {mutate: setSavedFeeds} = useSetSaveFeedsMutation()
|
const {mutate: setSavedFeeds} = useSetSaveFeedsMutation()
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
|
|
||||||
|
const deleteListPromptControl = useDialogControl()
|
||||||
|
const subscribeMutePromptControl = useDialogControl()
|
||||||
|
const subscribeBlockPromptControl = useDialogControl()
|
||||||
|
|
||||||
const isPinned = preferences?.feeds?.pinned?.includes(list.uri)
|
const isPinned = preferences?.feeds?.pinned?.includes(list.uri)
|
||||||
const isSaved = preferences?.feeds?.saved?.includes(list.uri)
|
const isSaved = preferences?.feeds?.saved?.includes(list.uri)
|
||||||
|
|
||||||
@@ -271,15 +277,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
|
|||||||
}
|
}
|
||||||
}, [list.uri, isPinned, pinFeed, unpinFeed, _])
|
}, [list.uri, isPinned, pinFeed, unpinFeed, _])
|
||||||
|
|
||||||
const onSubscribeMute = useCallback(() => {
|
const onSubscribeMute = useCallback(async () => {
|
||||||
openModal({
|
|
||||||
name: 'confirm',
|
|
||||||
title: _(msg`Mute these accounts?`),
|
|
||||||
message: _(
|
|
||||||
msg`Muting is private. Muted accounts can interact with you, but you will not see their posts or receive notifications from them.`,
|
|
||||||
),
|
|
||||||
confirmBtnText: _(msg`Mute this List`),
|
|
||||||
async onPressConfirm() {
|
|
||||||
try {
|
try {
|
||||||
await listMuteMutation.mutateAsync({uri: list.uri, mute: true})
|
await listMuteMutation.mutateAsync({uri: list.uri, mute: true})
|
||||||
Toast.show(_(msg`List muted`))
|
Toast.show(_(msg`List muted`))
|
||||||
@@ -291,12 +289,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
}, [list, listMuteMutation, track, _])
|
||||||
onPressCancel() {
|
|
||||||
closeModal()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}, [openModal, closeModal, list, listMuteMutation, track, _])
|
|
||||||
|
|
||||||
const onUnsubscribeMute = useCallback(async () => {
|
const onUnsubscribeMute = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -312,15 +305,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
|
|||||||
}
|
}
|
||||||
}, [list, listMuteMutation, track, _])
|
}, [list, listMuteMutation, track, _])
|
||||||
|
|
||||||
const onSubscribeBlock = useCallback(() => {
|
const onSubscribeBlock = useCallback(async () => {
|
||||||
openModal({
|
|
||||||
name: 'confirm',
|
|
||||||
title: _(msg`Block these accounts?`),
|
|
||||||
message: _(
|
|
||||||
msg`Blocking is public. Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`,
|
|
||||||
),
|
|
||||||
confirmBtnText: _(msg`Block this List`),
|
|
||||||
async onPressConfirm() {
|
|
||||||
try {
|
try {
|
||||||
await listBlockMutation.mutateAsync({uri: list.uri, block: true})
|
await listBlockMutation.mutateAsync({uri: list.uri, block: true})
|
||||||
Toast.show(_(msg`List blocked`))
|
Toast.show(_(msg`List blocked`))
|
||||||
@@ -332,12 +317,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
}, [list, listBlockMutation, track, _])
|
||||||
onPressCancel() {
|
|
||||||
closeModal()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}, [openModal, closeModal, list, listBlockMutation, track, _])
|
|
||||||
|
|
||||||
const onUnsubscribeBlock = useCallback(async () => {
|
const onUnsubscribeBlock = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -360,12 +340,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
|
|||||||
})
|
})
|
||||||
}, [openModal, list])
|
}, [openModal, list])
|
||||||
|
|
||||||
const onPressDelete = useCallback(() => {
|
const onPressDelete = useCallback(async () => {
|
||||||
openModal({
|
|
||||||
name: 'confirm',
|
|
||||||
title: _(msg`Delete List`),
|
|
||||||
message: _(msg`Are you sure?`),
|
|
||||||
async onPressConfirm() {
|
|
||||||
await listDeleteMutation.mutateAsync({uri: list.uri})
|
await listDeleteMutation.mutateAsync({uri: list.uri})
|
||||||
|
|
||||||
if (isSaved || isPinned) {
|
if (isSaved || isPinned) {
|
||||||
@@ -384,10 +359,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
|
|||||||
} else {
|
} else {
|
||||||
navigation.navigate('Home')
|
navigation.navigate('Home')
|
||||||
}
|
}
|
||||||
},
|
|
||||||
})
|
|
||||||
}, [
|
}, [
|
||||||
openModal,
|
|
||||||
list,
|
list,
|
||||||
listDeleteMutation,
|
listDeleteMutation,
|
||||||
navigation,
|
navigation,
|
||||||
@@ -441,7 +413,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
|
|||||||
items.push({
|
items.push({
|
||||||
testID: 'listHeaderDropdownDeleteBtn',
|
testID: 'listHeaderDropdownDeleteBtn',
|
||||||
label: _(msg`Delete List`),
|
label: _(msg`Delete List`),
|
||||||
onPress: onPressDelete,
|
onPress: deleteListPromptControl.open,
|
||||||
icon: {
|
icon: {
|
||||||
ios: {
|
ios: {
|
||||||
name: 'trash',
|
name: 'trash',
|
||||||
@@ -487,7 +459,9 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
|
|||||||
items.push({
|
items.push({
|
||||||
testID: 'listHeaderDropdownMuteBtn',
|
testID: 'listHeaderDropdownMuteBtn',
|
||||||
label: isMuting ? _(msg`Un-mute list`) : _(msg`Mute list`),
|
label: isMuting ? _(msg`Un-mute list`) : _(msg`Mute list`),
|
||||||
onPress: isMuting ? onUnsubscribeMute : onSubscribeMute,
|
onPress: isMuting
|
||||||
|
? onUnsubscribeMute
|
||||||
|
: subscribeMutePromptControl.open,
|
||||||
icon: {
|
icon: {
|
||||||
ios: {
|
ios: {
|
||||||
name: isMuting ? 'eye' : 'eye.slash',
|
name: isMuting ? 'eye' : 'eye.slash',
|
||||||
@@ -502,7 +476,9 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
|
|||||||
items.push({
|
items.push({
|
||||||
testID: 'listHeaderDropdownBlockBtn',
|
testID: 'listHeaderDropdownBlockBtn',
|
||||||
label: isBlocking ? _(msg`Un-block list`) : _(msg`Block list`),
|
label: isBlocking ? _(msg`Un-block list`) : _(msg`Block list`),
|
||||||
onPress: isBlocking ? onUnsubscribeBlock : onSubscribeBlock,
|
onPress: isBlocking
|
||||||
|
? onUnsubscribeBlock
|
||||||
|
: subscribeBlockPromptControl.open,
|
||||||
icon: {
|
icon: {
|
||||||
ios: {
|
ios: {
|
||||||
name: 'person.fill.xmark',
|
name: 'person.fill.xmark',
|
||||||
@@ -515,24 +491,24 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
|
|||||||
}
|
}
|
||||||
return items
|
return items
|
||||||
}, [
|
}, [
|
||||||
isOwner,
|
|
||||||
onPressShare,
|
|
||||||
onPressEdit,
|
|
||||||
onPressDelete,
|
|
||||||
onPressReport,
|
|
||||||
_,
|
_,
|
||||||
|
onPressShare,
|
||||||
|
isOwner,
|
||||||
isModList,
|
isModList,
|
||||||
isPinned,
|
isPinned,
|
||||||
unpinFeed,
|
|
||||||
isPending,
|
|
||||||
list.uri,
|
|
||||||
isCurateList,
|
isCurateList,
|
||||||
isMuting,
|
onPressEdit,
|
||||||
|
deleteListPromptControl.open,
|
||||||
|
onPressReport,
|
||||||
|
isPending,
|
||||||
|
unpinFeed,
|
||||||
|
list.uri,
|
||||||
isBlocking,
|
isBlocking,
|
||||||
|
isMuting,
|
||||||
onUnsubscribeMute,
|
onUnsubscribeMute,
|
||||||
onSubscribeMute,
|
subscribeMutePromptControl.open,
|
||||||
onUnsubscribeBlock,
|
onUnsubscribeBlock,
|
||||||
onSubscribeBlock,
|
subscribeBlockPromptControl.open,
|
||||||
])
|
])
|
||||||
|
|
||||||
const subscribeDropdownItems: DropdownItem[] = useMemo(() => {
|
const subscribeDropdownItems: DropdownItem[] = useMemo(() => {
|
||||||
@@ -540,7 +516,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
|
|||||||
{
|
{
|
||||||
testID: 'subscribeDropdownMuteBtn',
|
testID: 'subscribeDropdownMuteBtn',
|
||||||
label: _(msg`Mute accounts`),
|
label: _(msg`Mute accounts`),
|
||||||
onPress: onSubscribeMute,
|
onPress: subscribeMutePromptControl.open,
|
||||||
icon: {
|
icon: {
|
||||||
ios: {
|
ios: {
|
||||||
name: 'speaker.slash',
|
name: 'speaker.slash',
|
||||||
@@ -552,7 +528,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
|
|||||||
{
|
{
|
||||||
testID: 'subscribeDropdownBlockBtn',
|
testID: 'subscribeDropdownBlockBtn',
|
||||||
label: _(msg`Block accounts`),
|
label: _(msg`Block accounts`),
|
||||||
onPress: onSubscribeBlock,
|
onPress: subscribeBlockPromptControl.open,
|
||||||
icon: {
|
icon: {
|
||||||
ios: {
|
ios: {
|
||||||
name: 'person.fill.xmark',
|
name: 'person.fill.xmark',
|
||||||
@@ -562,7 +538,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}, [onSubscribeMute, onSubscribeBlock, _])
|
}, [_, subscribeMutePromptControl.open, subscribeBlockPromptControl.open])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProfileSubpageHeader
|
<ProfileSubpageHeader
|
||||||
@@ -626,6 +602,38 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
|
|||||||
<FontAwesomeIcon icon="ellipsis" size={20} color={pal.colors.text} />
|
<FontAwesomeIcon icon="ellipsis" size={20} color={pal.colors.text} />
|
||||||
</View>
|
</View>
|
||||||
</NativeDropdown>
|
</NativeDropdown>
|
||||||
|
|
||||||
|
<Prompt.Basic
|
||||||
|
control={deleteListPromptControl}
|
||||||
|
title={_(msg`Delete this list?`)}
|
||||||
|
description={_(
|
||||||
|
msg`If you delete this list, you won't be able to recover it.`,
|
||||||
|
)}
|
||||||
|
onConfirm={onPressDelete}
|
||||||
|
confirmButtonCta={_(msg`Delete`)}
|
||||||
|
confirmButtonColor="negative"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Prompt.Basic
|
||||||
|
control={subscribeMutePromptControl}
|
||||||
|
title={_(msg`Mute these accounts?`)}
|
||||||
|
description={_(
|
||||||
|
msg`Muting is private. Muted accounts can interact with you, but you will not see their posts or receive notifications from them.`,
|
||||||
|
)}
|
||||||
|
onConfirm={onSubscribeMute}
|
||||||
|
confirmButtonCta={_(msg`Mute list`)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Prompt.Basic
|
||||||
|
control={subscribeBlockPromptControl}
|
||||||
|
title={_(msg`Block these accounts?`)}
|
||||||
|
description={_(
|
||||||
|
msg`Blocking is public. Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`,
|
||||||
|
)}
|
||||||
|
onConfirm={onSubscribeBlock}
|
||||||
|
confirmButtonCta={_(msg`Block list`)}
|
||||||
|
confirmButtonColor="negative"
|
||||||
|
/>
|
||||||
</ProfileSubpageHeader>
|
</ProfileSubpageHeader>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export function Dialogs() {
|
|||||||
</Prompt.Description>
|
</Prompt.Description>
|
||||||
<Prompt.Actions>
|
<Prompt.Actions>
|
||||||
<Prompt.Cancel>Cancel</Prompt.Cancel>
|
<Prompt.Cancel>Cancel</Prompt.Cancel>
|
||||||
<Prompt.Action>Confirm</Prompt.Action>
|
<Prompt.Action onPress={() => {}}>Confirm</Prompt.Action>
|
||||||
</Prompt.Actions>
|
</Prompt.Actions>
|
||||||
</Prompt.Outer>
|
</Prompt.Outer>
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export function Menus() {
|
|||||||
<View style={[a.gap_md]}>
|
<View style={[a.gap_md]}>
|
||||||
<View style={[a.flex_row, a.align_start]}>
|
<View style={[a.flex_row, a.align_start]}>
|
||||||
<Menu.Root control={menuControl}>
|
<Menu.Root control={menuControl}>
|
||||||
<Menu.Trigger label="Open basic menu" style={[a.flex_1]}>
|
<Menu.Trigger label="Open basic menu">
|
||||||
{({state, props}) => {
|
{({state, props}) => {
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import {Button} from '#/view/com/util/forms/Button'
|
|||||||
import {s} from 'lib/styles'
|
import {s} from 'lib/styles'
|
||||||
import {Logo} from '#/view/icons/Logo'
|
import {Logo} from '#/view/icons/Logo'
|
||||||
import {Logotype} from '#/view/icons/Logotype'
|
import {Logotype} from '#/view/icons/Logotype'
|
||||||
|
import {useDedupe} from 'lib/hooks/useDedupe'
|
||||||
|
|
||||||
type TabOptions = 'Home' | 'Search' | 'Notifications' | 'MyProfile' | 'Feeds'
|
type TabOptions = 'Home' | 'Search' | 'Notifications' | 'MyProfile' | 'Feeds'
|
||||||
|
|
||||||
@@ -54,6 +55,7 @@ export function BottomBar({navigation}: BottomTabBarProps) {
|
|||||||
const {data: profile} = useProfileQuery({did: currentAccount?.did})
|
const {data: profile} = useProfileQuery({did: currentAccount?.did})
|
||||||
const {requestSwitchToAccount} = useLoggedOutViewControls()
|
const {requestSwitchToAccount} = useLoggedOutViewControls()
|
||||||
const closeAllActiveElements = useCloseAllActiveElements()
|
const closeAllActiveElements = useCloseAllActiveElements()
|
||||||
|
const dedupe = useDedupe()
|
||||||
|
|
||||||
const showSignIn = React.useCallback(() => {
|
const showSignIn = React.useCallback(() => {
|
||||||
closeAllActiveElements()
|
closeAllActiveElements()
|
||||||
@@ -74,12 +76,12 @@ export function BottomBar({navigation}: BottomTabBarProps) {
|
|||||||
if (tabState === TabState.InsideAtRoot) {
|
if (tabState === TabState.InsideAtRoot) {
|
||||||
emitSoftReset()
|
emitSoftReset()
|
||||||
} else if (tabState === TabState.Inside) {
|
} else if (tabState === TabState.Inside) {
|
||||||
navigation.dispatch(StackActions.popToTop())
|
dedupe(() => navigation.dispatch(StackActions.popToTop()))
|
||||||
} else {
|
} else {
|
||||||
navigation.navigate(`${tab}Tab`)
|
dedupe(() => navigation.navigate(`${tab}Tab`))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[track, navigation],
|
[track, navigation, dedupe],
|
||||||
)
|
)
|
||||||
const onPressHome = React.useCallback(() => onPressTab('Home'), [onPressTab])
|
const onPressHome = React.useCallback(() => onPressTab('Home'), [onPressTab])
|
||||||
const onPressSearch = React.useCallback(
|
const onPressSearch = React.useCallback(
|
||||||
|
|||||||
@@ -9501,9 +9501,9 @@ caniuse-api@^3.0.0:
|
|||||||
lodash.uniq "^4.5.0"
|
lodash.uniq "^4.5.0"
|
||||||
|
|
||||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001517, caniuse-lite@^1.0.30001520:
|
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001517, caniuse-lite@^1.0.30001520:
|
||||||
version "1.0.30001522"
|
version "1.0.30001596"
|
||||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001522.tgz#44b87a406c901269adcdb834713e23582dd71856"
|
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001596.tgz"
|
||||||
integrity sha512-TKiyTVZxJGhsTszLuzb+6vUZSjVOAhClszBr2Ta2k9IwtNBT/4dzmL6aywt0HCgEZlmwJzXJd8yNiob6HgwTRg==
|
integrity sha512-zpkZ+kEr6We7w63ORkoJ2pOfBwBkY/bJrG/UZ90qNb45Isblu8wzDgevEOrRL1r9dWayHjYiiyCMEXPn4DweGQ==
|
||||||
|
|
||||||
case-anything@^2.1.13:
|
case-anything@^2.1.13:
|
||||||
version "2.1.13"
|
version "2.1.13"
|
||||||
@@ -15085,11 +15085,6 @@ js-sha256@^0.10.1:
|
|||||||
resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.10.1.tgz#b40104ba1368e823fdd5f41b66b104b15a0da60d"
|
resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.10.1.tgz#b40104ba1368e823fdd5f41b66b104b15a0da60d"
|
||||||
integrity sha512-5obBtsz9301ULlsgggLg542s/jqtddfOpV5KJc4hajc9JV8GeY2gZHSVpYBn4nWqAUTJ9v+xwtbJ1mIBgIH5Vw==
|
integrity sha512-5obBtsz9301ULlsgggLg542s/jqtddfOpV5KJc4hajc9JV8GeY2gZHSVpYBn4nWqAUTJ9v+xwtbJ1mIBgIH5Vw==
|
||||||
|
|
||||||
js-sha256@^0.11.0:
|
|
||||||
version "0.11.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.11.0.tgz#256a921d9292f7fe98905face82e367abaca9576"
|
|
||||||
integrity sha512-6xNlKayMZvds9h1Y1VWc0fQHQ82BxTXizWPEtEeGvmOUYpBRy4gbWroHLpzowe6xiQhHpelCQiE7HEdznyBL9Q==
|
|
||||||
|
|
||||||
js-sha256@^0.9.0:
|
js-sha256@^0.9.0:
|
||||||
version "0.9.0"
|
version "0.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.9.0.tgz#0b89ac166583e91ef9123644bd3c5334ce9d0966"
|
resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.9.0.tgz#0b89ac166583e91ef9123644bd3c5334ce9d0966"
|
||||||
@@ -20108,14 +20103,6 @@ statsig-js@4.45.1:
|
|||||||
js-sha256 "^0.10.1"
|
js-sha256 "^0.10.1"
|
||||||
uuid "^8.3.2"
|
uuid "^8.3.2"
|
||||||
|
|
||||||
statsig-js@4.49.0:
|
|
||||||
version "4.49.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/statsig-js/-/statsig-js-4.49.0.tgz#8470a9ac218a93d36f4b7b306ff9377e48064740"
|
|
||||||
integrity sha512-N4drx6fzI168Q4NndFY3IJbSDqpWSBWvS290H/RnT/g3Et58SvtXzG5qqgzmqy4CwcmwH+IL8K15pL7hPnfvUQ==
|
|
||||||
dependencies:
|
|
||||||
js-sha256 "^0.11.0"
|
|
||||||
uuid "^8.3.2"
|
|
||||||
|
|
||||||
statsig-react-native-expo@^4.6.1:
|
statsig-react-native-expo@^4.6.1:
|
||||||
version "4.6.1"
|
version "4.6.1"
|
||||||
resolved "https://registry.yarnpkg.com/statsig-react-native-expo/-/statsig-react-native-expo-4.6.1.tgz#0bdf49fee7112f7f28bff2405f4ba0c1727bb3d6"
|
resolved "https://registry.yarnpkg.com/statsig-react-native-expo/-/statsig-react-native-expo-4.6.1.tgz#0bdf49fee7112f7f28bff2405f4ba0c1727bb3d6"
|
||||||
@@ -20136,13 +20123,6 @@ statsig-react@^1.21.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
statsig-js "4.45.1"
|
statsig-js "4.45.1"
|
||||||
|
|
||||||
statsig-react@^1.36.0:
|
|
||||||
version "1.36.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/statsig-react/-/statsig-react-1.36.0.tgz#c2171268a6c76eee534849ec9556b836baba04b6"
|
|
||||||
integrity sha512-QcTHla3ypfn2RvrnHGNlqWbiC2W/ZjcMM5LT6ExNV4skH7Xhspto3dMS3JVzBhOb74OEDZK4DbxQj9Wdz6XW0w==
|
|
||||||
dependencies:
|
|
||||||
statsig-js "4.49.0"
|
|
||||||
|
|
||||||
statuses@2.0.1:
|
statuses@2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
|
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
|
||||||
|
|||||||