Merge branch 'main' into starter-packs
This commit is contained in:
@@ -9,6 +9,7 @@ import React, {
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Keyboard,
|
||||
LayoutChangeEvent,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
@@ -116,7 +117,7 @@ export const ComposePost = observer(function ComposePost({
|
||||
const {closeComposer} = useComposerControls()
|
||||
const {track} = useAnalytics()
|
||||
const pal = usePalette('default')
|
||||
const {isTabletOrDesktop, isMobile} = useWebMediaQueries()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const {_} = useLingui()
|
||||
const requireAltTextEnabled = useRequireAltTextEnabled()
|
||||
const langPrefs = useLanguagePrefs()
|
||||
@@ -170,24 +171,8 @@ export const ComposePost = observer(function ComposePost({
|
||||
[insets, isKeyboardVisible],
|
||||
)
|
||||
|
||||
const hasScrolled = useSharedValue(0)
|
||||
const scrollHandler = useAnimatedScrollHandler({
|
||||
onScroll: event => {
|
||||
hasScrolled.value = withTiming(event.contentOffset.y > 0 ? 1 : 0)
|
||||
},
|
||||
})
|
||||
const topBarAnimatedStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
borderColor: interpolateColor(
|
||||
hasScrolled.value,
|
||||
[0, 1],
|
||||
['transparent', t.atoms.border_contrast_medium.borderColor],
|
||||
),
|
||||
}
|
||||
})
|
||||
|
||||
const onPressCancel = useCallback(() => {
|
||||
if (graphemeLength > 0 || !gallery.isEmpty) {
|
||||
if (graphemeLength > 0 || !gallery.isEmpty || extGif) {
|
||||
closeAllDialogs()
|
||||
if (Keyboard) {
|
||||
Keyboard.dismiss()
|
||||
@@ -197,6 +182,7 @@ export const ComposePost = observer(function ComposePost({
|
||||
onClose()
|
||||
}
|
||||
}, [
|
||||
extGif,
|
||||
graphemeLength,
|
||||
gallery.isEmpty,
|
||||
closeAllDialogs,
|
||||
@@ -395,23 +381,26 @@ export const ComposePost = observer(function ComposePost({
|
||||
[setExtLink],
|
||||
)
|
||||
|
||||
const {
|
||||
scrollHandler,
|
||||
onScrollViewContentSizeChange,
|
||||
onScrollViewLayout,
|
||||
topBarAnimatedStyle,
|
||||
bottomBarAnimatedStyle,
|
||||
} = useAnimatedBorders()
|
||||
|
||||
return (
|
||||
<>
|
||||
<KeyboardAvoidingView
|
||||
testID="composePostView"
|
||||
behavior="padding"
|
||||
style={a.flex_1}
|
||||
keyboardVerticalOffset={replyTo ? 120 : isAndroid ? 180 : 150}>
|
||||
keyboardVerticalOffset={replyTo ? 115 : isAndroid ? 180 : 162}>
|
||||
<View
|
||||
style={[a.flex_1, viewStyles]}
|
||||
aria-modal
|
||||
accessibilityViewIsModal>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.topbar,
|
||||
topBarAnimatedStyle,
|
||||
isWeb && isTabletOrDesktop && styles.topbarDesktop,
|
||||
]}>
|
||||
<Animated.View style={topBarAnimatedStyle}>
|
||||
<View style={styles.topbarInner}>
|
||||
<TouchableOpacity
|
||||
testID="composerDiscardButton"
|
||||
@@ -509,7 +498,9 @@ export const ComposePost = observer(function ComposePost({
|
||||
<Animated.ScrollView
|
||||
onScroll={scrollHandler}
|
||||
style={styles.scrollView}
|
||||
keyboardShouldPersistTaps="always">
|
||||
keyboardShouldPersistTaps="always"
|
||||
onContentSizeChange={onScrollViewContentSizeChange}
|
||||
onLayout={onScrollViewLayout}>
|
||||
{replyTo ? <ComposerReplyTo replyTo={replyTo} /> : undefined}
|
||||
|
||||
<View
|
||||
@@ -575,7 +566,11 @@ export const ComposePost = observer(function ComposePost({
|
||||
<KeyboardStickyView
|
||||
offset={{closed: isIOS ? -insets.bottom : 0, opened: 0}}>
|
||||
{replyTo ? null : (
|
||||
<ThreadgateBtn threadgate={threadgate} onChange={setThreadgate} />
|
||||
<ThreadgateBtn
|
||||
threadgate={threadgate}
|
||||
onChange={setThreadgate}
|
||||
style={bottomBarAnimatedStyle}
|
||||
/>
|
||||
)}
|
||||
<View
|
||||
style={[
|
||||
@@ -625,15 +620,108 @@ export function useComposerCancelRef() {
|
||||
return useRef<CancelRef>(null)
|
||||
}
|
||||
|
||||
function useAnimatedBorders() {
|
||||
const t = useTheme()
|
||||
const hasScrolledTop = useSharedValue(0)
|
||||
const hasScrolledBottom = useSharedValue(0)
|
||||
const contentOffset = useSharedValue(0)
|
||||
const scrollViewHeight = useSharedValue(Infinity)
|
||||
const contentHeight = useSharedValue(0)
|
||||
|
||||
/**
|
||||
* Make sure to run this on the UI thread!
|
||||
*/
|
||||
const showHideBottomBorder = useCallback(
|
||||
({
|
||||
newContentHeight,
|
||||
newContentOffset,
|
||||
newScrollViewHeight,
|
||||
}: {
|
||||
newContentHeight?: number
|
||||
newContentOffset?: number
|
||||
newScrollViewHeight?: number
|
||||
}) => {
|
||||
'worklet'
|
||||
|
||||
if (typeof newContentHeight === 'number')
|
||||
contentHeight.value = Math.floor(newContentHeight)
|
||||
if (typeof newContentOffset === 'number')
|
||||
contentOffset.value = Math.floor(newContentOffset)
|
||||
if (typeof newScrollViewHeight === 'number')
|
||||
scrollViewHeight.value = Math.floor(newScrollViewHeight)
|
||||
|
||||
hasScrolledBottom.value = withTiming(
|
||||
contentHeight.value - contentOffset.value - 5 > scrollViewHeight.value
|
||||
? 1
|
||||
: 0,
|
||||
)
|
||||
},
|
||||
[contentHeight, contentOffset, scrollViewHeight, hasScrolledBottom],
|
||||
)
|
||||
|
||||
const scrollHandler = useAnimatedScrollHandler({
|
||||
onScroll: event => {
|
||||
'worklet'
|
||||
hasScrolledTop.value = withTiming(event.contentOffset.y > 0 ? 1 : 0)
|
||||
showHideBottomBorder({
|
||||
newContentOffset: event.contentOffset.y,
|
||||
newContentHeight: event.contentSize.height,
|
||||
newScrollViewHeight: event.layoutMeasurement.height,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const onScrollViewContentSizeChange = useCallback(
|
||||
(_width: number, height: number) => {
|
||||
'worklet'
|
||||
showHideBottomBorder({
|
||||
newContentHeight: height,
|
||||
})
|
||||
},
|
||||
[showHideBottomBorder],
|
||||
)
|
||||
|
||||
const onScrollViewLayout = useCallback(
|
||||
(evt: LayoutChangeEvent) => {
|
||||
'worklet'
|
||||
showHideBottomBorder({
|
||||
newScrollViewHeight: evt.nativeEvent.layout.height,
|
||||
})
|
||||
},
|
||||
[showHideBottomBorder],
|
||||
)
|
||||
|
||||
const topBarAnimatedStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
borderBottomWidth: hairlineWidth,
|
||||
borderColor: interpolateColor(
|
||||
hasScrolledTop.value,
|
||||
[0, 1],
|
||||
['transparent', t.atoms.border_contrast_medium.borderColor],
|
||||
),
|
||||
}
|
||||
})
|
||||
const bottomBarAnimatedStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
borderTopWidth: hairlineWidth,
|
||||
borderColor: interpolateColor(
|
||||
hasScrolledBottom.value,
|
||||
[0, 1],
|
||||
['transparent', t.atoms.border_contrast_medium.borderColor],
|
||||
),
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
scrollHandler,
|
||||
onScrollViewContentSizeChange,
|
||||
onScrollViewLayout,
|
||||
topBarAnimatedStyle,
|
||||
bottomBarAnimatedStyle,
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
topbar: {
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
topbarDesktop: {
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
height: 50,
|
||||
},
|
||||
topbarInner: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@@ -698,7 +786,8 @@ const styles = StyleSheet.create({
|
||||
bottomBar: {
|
||||
flexDirection: 'row',
|
||||
paddingVertical: 4,
|
||||
paddingLeft: 8,
|
||||
// should be 8 but due to visual alignment we have to fudge it
|
||||
paddingLeft: 7,
|
||||
paddingRight: 16,
|
||||
alignItems: 'center',
|
||||
borderTopWidth: hairlineWidth,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {useCallback} from 'react'
|
||||
import React, {useCallback, useRef} from 'react'
|
||||
import {Keyboard} from 'react-native'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
@@ -7,7 +7,6 @@ import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {Gif} from '#/state/queries/tenor'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
import {GifSelectDialog} from '#/components/dialogs/GifSelect'
|
||||
import {GifSquare_Stroke2_Corner0_Rounded as GifIcon} from '#/components/icons/Gif'
|
||||
|
||||
@@ -19,14 +18,14 @@ type Props = {
|
||||
|
||||
export function SelectGifBtn({onClose, onSelectGif, disabled}: Props) {
|
||||
const {_} = useLingui()
|
||||
const control = useDialogControl()
|
||||
const ref = useRef<{open: () => void}>(null)
|
||||
const t = useTheme()
|
||||
|
||||
const onPressSelectGif = useCallback(async () => {
|
||||
logEvent('composer:gif:open', {})
|
||||
Keyboard.dismiss()
|
||||
control.open()
|
||||
}, [control])
|
||||
ref.current?.open()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -44,7 +43,7 @@ export function SelectGifBtn({onClose, onSelectGif, disabled}: Props) {
|
||||
</Button>
|
||||
|
||||
<GifSelectDialog
|
||||
control={control}
|
||||
controlRef={ref}
|
||||
onClose={onClose}
|
||||
onSelectGif={onSelectGif}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react'
|
||||
import {Keyboard, View} from 'react-native'
|
||||
import {Keyboard, StyleProp, ViewStyle} from 'react-native'
|
||||
import Animated, {AnimatedStyle} from 'react-native-reanimated'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
@@ -16,9 +17,11 @@ import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
|
||||
export function ThreadgateBtn({
|
||||
threadgate,
|
||||
onChange,
|
||||
style,
|
||||
}: {
|
||||
threadgate: ThreadgateSetting[]
|
||||
onChange: (v: ThreadgateSetting[]) => void
|
||||
style?: StyleProp<AnimatedStyle<ViewStyle>>
|
||||
}) {
|
||||
const {track} = useAnalytics()
|
||||
const {_} = useLingui()
|
||||
@@ -46,7 +49,7 @@ export function ThreadgateBtn({
|
||||
: _(msg`Some people can reply`)
|
||||
|
||||
return (
|
||||
<View style={[a.flex_row, a.py_xs, a.px_sm, t.atoms.bg]}>
|
||||
<Animated.View style={[a.flex_row, a.p_sm, t.atoms.bg, style]}>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
@@ -59,6 +62,6 @@ export function ThreadgateBtn({
|
||||
/>
|
||||
<ButtonText>{label}</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
</Animated.View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,23 +7,22 @@ import {
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {useTheme} from '#/lib/ThemeContext'
|
||||
import {logger} from '#/logger'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {isNative, isWeb} from '#/platform/detection'
|
||||
import {hydrateFeedGenerator} from '#/state/queries/feed'
|
||||
import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||
import {RQKEY, useProfileFeedgensQuery} from '#/state/queries/profile-feedgens'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||
import {EmptyState} from 'view/com/util/EmptyState'
|
||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||
import {List, ListRef} from '../util/List'
|
||||
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {FeedSourceCardLoaded} from './FeedSourceCard'
|
||||
|
||||
const LOADING = {_reactKey: '__loading__'}
|
||||
@@ -52,7 +51,6 @@ export const ProfileFeedgens = React.forwardRef<
|
||||
{did, scrollElRef, headerOffset, enabled, style, testID, setScrollViewTag},
|
||||
ref,
|
||||
) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const theme = useTheme()
|
||||
const [isPTRing, setIsPTRing] = React.useState(false)
|
||||
@@ -138,13 +136,11 @@ export const ProfileFeedgens = React.forwardRef<
|
||||
({item, index}: ListRenderItemInfo<any>) => {
|
||||
if (item === EMPTY) {
|
||||
return (
|
||||
<View
|
||||
<EmptyState
|
||||
icon="hashtag"
|
||||
message={_(msg`You have no feeds.`)}
|
||||
testID="listsEmpty"
|
||||
style={[{padding: 18, borderTopWidth: 1}, pal.border]}>
|
||||
<Text style={pal.textLight}>
|
||||
<Trans>You have no feeds.</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
/>
|
||||
)
|
||||
} else if (item === ERROR_ITEM) {
|
||||
return (
|
||||
@@ -170,13 +166,13 @@ export const ProfileFeedgens = React.forwardRef<
|
||||
preferences={preferences}
|
||||
style={styles.item}
|
||||
showLikes
|
||||
hideTopBorder={index === 0}
|
||||
hideTopBorder={index === 0 && !isWeb}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return null
|
||||
},
|
||||
[error, refetch, onPressRetryLoadMore, pal, preferences, _],
|
||||
[error, refetch, onPressRetryLoadMore, preferences, _],
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
import {AppBskyGraphDefs as GraphDefs} from '@atproto/api'
|
||||
import {Trans} from '@lingui/macro'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {logger} from '#/logger'
|
||||
@@ -17,11 +18,10 @@ import {MyListsFilter, useMyListsQuery} from '#/state/queries/my-lists'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {s} from 'lib/styles'
|
||||
import {EmptyState} from 'view/com/util/EmptyState'
|
||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||
import {List} from '../util/List'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {ListCard} from './ListCard'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
|
||||
const LOADING = {_reactKey: '__loading__'}
|
||||
const EMPTY = {_reactKey: '__empty__'}
|
||||
@@ -42,6 +42,7 @@ export function MyLists({
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const {track} = useAnalytics()
|
||||
const {_} = useLingui()
|
||||
const [isPTRing, setIsPTRing] = React.useState(false)
|
||||
const {data, isFetching, isFetched, isError, error, refetch} =
|
||||
useMyListsQuery(filter)
|
||||
@@ -83,14 +84,12 @@ export function MyLists({
|
||||
({item, index}: {item: any; index: number}) => {
|
||||
if (item === EMPTY) {
|
||||
return (
|
||||
<View
|
||||
<EmptyState
|
||||
key={item._reactKey}
|
||||
icon="list-ul"
|
||||
message={_(msg`You have no lists.`)}
|
||||
testID="listsEmpty"
|
||||
style={[{padding: 18, borderTopWidth: hairlineWidth}, pal.border]}>
|
||||
<Text style={pal.textLight}>
|
||||
<Trans>You have no lists.</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
/>
|
||||
)
|
||||
} else if (item === ERROR_ITEM) {
|
||||
return (
|
||||
@@ -118,7 +117,7 @@ export function MyLists({
|
||||
/>
|
||||
)
|
||||
},
|
||||
[error, onRefresh, renderItem, pal],
|
||||
[error, onRefresh, renderItem, _],
|
||||
)
|
||||
|
||||
if (inline) {
|
||||
|
||||
@@ -7,22 +7,21 @@ import {
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {useTheme} from '#/lib/ThemeContext'
|
||||
import {logger} from '#/logger'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {isNative, isWeb} from '#/platform/detection'
|
||||
import {RQKEY, useProfileListsQuery} from '#/state/queries/profile-lists'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||
import {EmptyState} from 'view/com/util/EmptyState'
|
||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||
import {List, ListRef} from '../util/List'
|
||||
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {ListCard} from './ListCard'
|
||||
|
||||
const LOADING = {_reactKey: '__loading__'}
|
||||
@@ -49,7 +48,6 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
|
||||
{did, scrollElRef, headerOffset, enabled, style, testID, setScrollViewTag},
|
||||
ref,
|
||||
) {
|
||||
const pal = usePalette('default')
|
||||
const theme = useTheme()
|
||||
const {track} = useAnalytics()
|
||||
const {_} = useLingui()
|
||||
@@ -142,11 +140,11 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
|
||||
({item, index}: ListRenderItemInfo<any>) => {
|
||||
if (item === EMPTY) {
|
||||
return (
|
||||
<View testID="listsEmpty" style={[{padding: 18}, pal.border]}>
|
||||
<Text style={pal.textLight}>
|
||||
<Trans>You have no lists.</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
<EmptyState
|
||||
icon="list-ul"
|
||||
message={_(msg`You have no lists.`)}
|
||||
testID="listsEmpty"
|
||||
/>
|
||||
)
|
||||
} else if (item === ERROR_ITEM) {
|
||||
return (
|
||||
@@ -172,11 +170,11 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
|
||||
list={item}
|
||||
testID={`list-${item.name}`}
|
||||
style={styles.item}
|
||||
noBorder={index === 0}
|
||||
noBorder={index === 0 && !isWeb}
|
||||
/>
|
||||
)
|
||||
},
|
||||
[error, refetch, onPressRetryLoadMore, pal, _],
|
||||
[error, refetch, onPressRetryLoadMore, _],
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
|
||||
@@ -18,7 +18,13 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {cleanError} from 'lib/strings/errors'
|
||||
import {colors, gradients, s} from 'lib/styles'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {isAndroid} from 'platform/detection'
|
||||
import {isAndroid, isWeb} from 'platform/detection'
|
||||
import {DeactivateAccountDialog} from '#/screens/Settings/components/DeactivateAccountDialog'
|
||||
import {atoms as a, useTheme as useNewTheme} from '#/alf'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
||||
import {InlineLinkText} from '#/components/Link'
|
||||
import {Text as NewText} from '#/components/Typography'
|
||||
import {resetToTab} from '../../../Navigation'
|
||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||
import {Text} from '../util/text/Text'
|
||||
@@ -30,6 +36,7 @@ export const snapPoints = isAndroid ? ['90%'] : ['55%']
|
||||
export function Component({}: {}) {
|
||||
const pal = usePalette('default')
|
||||
const theme = useTheme()
|
||||
const t = useNewTheme()
|
||||
const {currentAccount} = useSession()
|
||||
const agent = useAgent()
|
||||
const {removeAccount} = useSessionApi()
|
||||
@@ -41,6 +48,7 @@ export function Component({}: {}) {
|
||||
const [password, setPassword] = React.useState<string>('')
|
||||
const [isProcessing, setIsProcessing] = React.useState<boolean>(false)
|
||||
const [error, setError] = React.useState<string>('')
|
||||
const deactivateAccountControl = useDialogControl()
|
||||
const onPressSendEmail = async () => {
|
||||
setError('')
|
||||
setIsProcessing(true)
|
||||
@@ -168,6 +176,50 @@ export function Component({}: {}) {
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
)}
|
||||
|
||||
<View style={[!isWeb && a.px_xl]}>
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.flex_row,
|
||||
a.gap_sm,
|
||||
a.mt_lg,
|
||||
a.p_lg,
|
||||
a.rounded_sm,
|
||||
t.atoms.bg_contrast_25,
|
||||
]}>
|
||||
<CircleInfo
|
||||
size="md"
|
||||
style={[
|
||||
a.relative,
|
||||
{
|
||||
top: -1,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<NewText style={[a.leading_snug, a.flex_1]}>
|
||||
<Trans>
|
||||
You can also temporarily deactivate your account instead,
|
||||
and reactivate it at any time.
|
||||
</Trans>{' '}
|
||||
<InlineLinkText
|
||||
label={_(
|
||||
msg`Click here for more information on deactivating your account`,
|
||||
)}
|
||||
to="#"
|
||||
onPress={e => {
|
||||
e.preventDefault()
|
||||
deactivateAccountControl.open()
|
||||
return false
|
||||
}}>
|
||||
<Trans>Click here for more information.</Trans>
|
||||
</InlineLinkText>
|
||||
</NewText>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<DeactivateAccountDialog control={deactivateAccountControl} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -6,28 +6,30 @@ import {
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {AppBskyGraphDefs as GraphDefs} from '@atproto/api'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {UserAvatar} from '../util/UserAvatar'
|
||||
import {MyLists} from '../lists/MyLists'
|
||||
import {Button} from '../util/forms/Button'
|
||||
import * as Toast from '../util/Toast'
|
||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||
import {sanitizeHandle} from 'lib/strings/handles'
|
||||
import {s} from 'lib/styles'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {isWeb, isAndroid, isMobileWeb} from 'platform/detection'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {
|
||||
useDangerousListMembershipsQuery,
|
||||
getMembership,
|
||||
ListMembersip,
|
||||
useDangerousListMembershipsQuery,
|
||||
useListMembershipAddMutation,
|
||||
useListMembershipRemoveMutation,
|
||||
} from '#/state/queries/list-memberships'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {useSession} from '#/state/session'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||
import {sanitizeHandle} from 'lib/strings/handles'
|
||||
import {s} from 'lib/styles'
|
||||
import {isAndroid, isMobileWeb, isWeb} from 'platform/detection'
|
||||
import {MyLists} from '../lists/MyLists'
|
||||
import {Button} from '../util/forms/Button'
|
||||
import {Text} from '../util/text/Text'
|
||||
import * as Toast from '../util/Toast'
|
||||
import {UserAvatar} from '../util/UserAvatar'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
|
||||
export const snapPoints = ['fullscreen']
|
||||
|
||||
@@ -61,12 +63,23 @@ export function Component({
|
||||
return [pal.border, {height: screenHeight / 1.5}]
|
||||
}
|
||||
|
||||
return [pal.border, {flex: 1}]
|
||||
return [pal.border, {flex: 1, borderTopWidth: hairlineWidth}]
|
||||
}, [pal.border, screenHeight])
|
||||
|
||||
return (
|
||||
<View testID="userAddRemoveListsModal" style={s.hContentRegion}>
|
||||
<Text style={[styles.title, pal.text]}>
|
||||
<Text
|
||||
style={[
|
||||
{
|
||||
textAlign: 'center',
|
||||
fontWeight: 'bold',
|
||||
fontSize: 20,
|
||||
marginBottom: 12,
|
||||
paddingHorizontal: 12,
|
||||
},
|
||||
pal.text,
|
||||
]}
|
||||
numberOfLines={1}>
|
||||
<Trans>Update {displayName} in Lists</Trans>
|
||||
</Text>
|
||||
<MyLists
|
||||
@@ -175,9 +188,7 @@ function ListItem({
|
||||
style={[
|
||||
styles.listItem,
|
||||
pal.border,
|
||||
{
|
||||
borderTopWidth: index === 0 ? 0 : 1,
|
||||
},
|
||||
index !== 0 && {borderTopWidth: hairlineWidth},
|
||||
]}>
|
||||
<View style={styles.listItemAvi}>
|
||||
<UserAvatar size={40} avatar={list.avatar} type="list" />
|
||||
@@ -229,16 +240,6 @@ const styles = StyleSheet.create({
|
||||
container: {
|
||||
paddingHorizontal: isWeb ? 0 : 16,
|
||||
},
|
||||
title: {
|
||||
textAlign: 'center',
|
||||
fontWeight: 'bold',
|
||||
fontSize: 24,
|
||||
marginBottom: 10,
|
||||
},
|
||||
list: {
|
||||
flex: 1,
|
||||
borderTopWidth: 1,
|
||||
},
|
||||
btns: {
|
||||
position: 'relative',
|
||||
flexDirection: 'row',
|
||||
@@ -247,7 +248,7 @@ const styles = StyleSheet.create({
|
||||
gap: 10,
|
||||
paddingTop: 10,
|
||||
paddingBottom: isAndroid ? 10 : 0,
|
||||
borderTopWidth: 1,
|
||||
borderTopWidth: hairlineWidth,
|
||||
},
|
||||
footerBtn: {
|
||||
paddingHorizontal: 24,
|
||||
|
||||
@@ -40,7 +40,6 @@ import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe'
|
||||
import {PostAlerts} from '../../../components/moderation/PostAlerts'
|
||||
import {PostHider} from '../../../components/moderation/PostHider'
|
||||
import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers'
|
||||
import {AviFollowButton} from '../posts/AviFollowButton'
|
||||
import {WhoCanReply} from '../threadgate/WhoCanReply'
|
||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||
import {Link, TextLink} from '../util/Link'
|
||||
@@ -472,16 +471,12 @@ let PostThreadItemLoaded = ({
|
||||
{/* If we are in threaded mode, the avatar is rendered in PostMeta */}
|
||||
{!isThreadedChild && (
|
||||
<View style={styles.layoutAvi}>
|
||||
<AviFollowButton author={post.author} moderation={moderation}>
|
||||
<PreviewableUserAvatar
|
||||
size={38}
|
||||
profile={post.author}
|
||||
moderation={moderation.ui('avatar')}
|
||||
type={
|
||||
post.author.associated?.labeler ? 'labeler' : 'user'
|
||||
}
|
||||
/>
|
||||
</AviFollowButton>
|
||||
<PreviewableUserAvatar
|
||||
size={38}
|
||||
profile={post.author}
|
||||
moderation={moderation.ui('avatar')}
|
||||
type={post.author.associated?.labeler ? 'labeler' : 'user'}
|
||||
/>
|
||||
|
||||
{showChildReplyLine && (
|
||||
<View
|
||||
|
||||
@@ -97,8 +97,8 @@ export function AviFollowButton({
|
||||
}),
|
||||
a.absolute,
|
||||
{
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
bottom: -1,
|
||||
right: -1,
|
||||
borderWidth: 1,
|
||||
borderColor: t.atoms.bg.backgroundColor,
|
||||
},
|
||||
|
||||
@@ -1 +1,5 @@
|
||||
export {Fragment as AviFollowButton} from 'react'
|
||||
import React from 'react'
|
||||
|
||||
export function AviFollowButton({children}: {children: React.ReactNode}) {
|
||||
return children
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ import {
|
||||
import {useSession} from '#/state/session'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {List, ListRef} from '../util/List'
|
||||
import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
|
||||
@@ -102,7 +101,6 @@ let Feed = ({
|
||||
const checkForNewRef = React.useRef<(() => void) | null>(null)
|
||||
const lastFetchRef = React.useRef<number>(Date.now())
|
||||
const [feedType, feedUri] = feed.split('|')
|
||||
const {isTabletOrMobile} = useWebMediaQueries()
|
||||
|
||||
const opts = React.useMemo(
|
||||
() => ({enabled, ignoreFilterFor}),
|
||||
@@ -314,15 +312,9 @@ let Feed = ({
|
||||
// -prf
|
||||
return <DiscoverFallbackHeader />
|
||||
}
|
||||
return (
|
||||
<FeedSlice
|
||||
slice={item}
|
||||
hideTopBorder={index === 0 && isTabletOrMobile}
|
||||
/>
|
||||
)
|
||||
return <FeedSlice slice={item} hideTopBorder={index === 0 && !isWeb} />
|
||||
},
|
||||
[
|
||||
isTabletOrMobile,
|
||||
renderEmptyState,
|
||||
feed,
|
||||
error,
|
||||
|
||||
@@ -43,6 +43,7 @@ import {Text} from '../util/text/Text'
|
||||
import {PreviewableUserAvatar} from '../util/UserAvatar'
|
||||
import {AviFollowButton} from './AviFollowButton'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost'
|
||||
|
||||
interface FeedItemProps {
|
||||
record: AppBskyFeedPost.Record
|
||||
@@ -251,13 +252,10 @@ let FeedItemInner = ({
|
||||
)}`,
|
||||
)}
|
||||
onBeforePress={onOpenReposter}>
|
||||
<FontAwesomeIcon
|
||||
icon="retweet"
|
||||
style={{
|
||||
marginRight: 4,
|
||||
color: pal.colors.textLight,
|
||||
minWidth: 16,
|
||||
}}
|
||||
<Repost
|
||||
style={{color: pal.colors.textLight, marginRight: 3}}
|
||||
width={14}
|
||||
height={14}
|
||||
/>
|
||||
<Text
|
||||
type="sm-bold"
|
||||
@@ -463,9 +461,10 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
includeReason: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginTop: 2,
|
||||
marginBottom: 2,
|
||||
marginLeft: -20,
|
||||
marginLeft: -18,
|
||||
},
|
||||
layout: {
|
||||
flexDirection: 'row',
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import {Text} from './text/Text'
|
||||
import {UserGroupIcon} from 'lib/icons'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {isWeb} from 'platform/detection'
|
||||
|
||||
export function EmptyState({
|
||||
testID,
|
||||
@@ -22,7 +23,9 @@ export function EmptyState({
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
return (
|
||||
<View testID={testID} style={[styles.container, pal.border, style]}>
|
||||
<View
|
||||
testID={testID}
|
||||
style={[styles.container, isWeb && pal.border, style]}>
|
||||
<View style={styles.iconContainer}>
|
||||
{icon === 'user-group' ? (
|
||||
<UserGroupIcon size="64" style={styles.icon} />
|
||||
@@ -48,9 +51,9 @@ export function EmptyState({
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
paddingVertical: 20,
|
||||
paddingVertical: 24,
|
||||
paddingHorizontal: 36,
|
||||
borderTopWidth: 1,
|
||||
borderTopWidth: isWeb ? 1 : undefined,
|
||||
},
|
||||
iconContainer: {
|
||||
flexDirection: 'row',
|
||||
|
||||
@@ -32,14 +32,17 @@ interface AddedProps {
|
||||
desktopFixedHeight?: boolean | number
|
||||
}
|
||||
|
||||
export const CenteredView = React.forwardRef(function CenteredView({
|
||||
style,
|
||||
sideBorders,
|
||||
topBorder,
|
||||
...props
|
||||
}: React.PropsWithChildren<
|
||||
ViewProps & {sideBorders?: boolean; topBorder?: boolean}
|
||||
>) {
|
||||
export const CenteredView = React.forwardRef(function CenteredView(
|
||||
{
|
||||
style,
|
||||
sideBorders,
|
||||
topBorder,
|
||||
...props
|
||||
}: React.PropsWithChildren<
|
||||
ViewProps & {sideBorders?: boolean; topBorder?: boolean}
|
||||
>,
|
||||
ref: React.Ref<View>,
|
||||
) {
|
||||
const pal = usePalette('default')
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
if (!isMobile) {
|
||||
@@ -58,7 +61,7 @@ export const CenteredView = React.forwardRef(function CenteredView({
|
||||
})
|
||||
style = addStyle(style, pal.border)
|
||||
}
|
||||
return <View style={style} {...props} />
|
||||
return <View ref={ref} style={style} {...props} />
|
||||
})
|
||||
|
||||
export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl<ItemT>(
|
||||
|
||||
@@ -2,7 +2,6 @@ import React from 'react'
|
||||
import {
|
||||
StyleProp,
|
||||
StyleSheet,
|
||||
TextStyle,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
ViewStyle,
|
||||
@@ -32,7 +31,7 @@ import {InfoCircleIcon} from 'lib/icons'
|
||||
import {makeProfileLink} from 'lib/routes/links'
|
||||
import {precacheProfile} from 'state/queries/profile'
|
||||
import {ComposerOptsQuote} from 'state/shell/composer'
|
||||
import {atoms as a, flatten} from '#/alf'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {RichText} from '#/components/RichText'
|
||||
import {ContentHider} from '../../../../components/moderation/ContentHider'
|
||||
import {PostAlerts} from '../../../../components/moderation/PostAlerts'
|
||||
@@ -46,12 +45,12 @@ export function MaybeQuoteEmbed({
|
||||
embed,
|
||||
onOpen,
|
||||
style,
|
||||
textStyle,
|
||||
allowNestedQuotes,
|
||||
}: {
|
||||
embed: AppBskyEmbedRecord.View
|
||||
onOpen?: () => void
|
||||
style?: StyleProp<ViewStyle>
|
||||
textStyle?: StyleProp<TextStyle>
|
||||
allowNestedQuotes?: boolean
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
if (
|
||||
@@ -65,7 +64,7 @@ export function MaybeQuoteEmbed({
|
||||
postRecord={embed.record.value}
|
||||
onOpen={onOpen}
|
||||
style={style}
|
||||
textStyle={textStyle}
|
||||
allowNestedQuotes={allowNestedQuotes}
|
||||
/>
|
||||
)
|
||||
} else if (AppBskyEmbedRecord.isViewBlocked(embed.record)) {
|
||||
@@ -95,13 +94,13 @@ function QuoteEmbedModerated({
|
||||
postRecord,
|
||||
onOpen,
|
||||
style,
|
||||
textStyle,
|
||||
allowNestedQuotes,
|
||||
}: {
|
||||
viewRecord: AppBskyEmbedRecord.ViewRecord
|
||||
postRecord: AppBskyFeedPost.Record
|
||||
onOpen?: () => void
|
||||
style?: StyleProp<ViewStyle>
|
||||
textStyle?: StyleProp<TextStyle>
|
||||
allowNestedQuotes?: boolean
|
||||
}) {
|
||||
const moderationOpts = useModerationOpts()
|
||||
const moderation = React.useMemo(() => {
|
||||
@@ -126,7 +125,7 @@ function QuoteEmbedModerated({
|
||||
moderation={moderation}
|
||||
onOpen={onOpen}
|
||||
style={style}
|
||||
textStyle={textStyle}
|
||||
allowNestedQuotes={allowNestedQuotes}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -136,13 +135,13 @@ export function QuoteEmbed({
|
||||
moderation,
|
||||
onOpen,
|
||||
style,
|
||||
textStyle,
|
||||
allowNestedQuotes,
|
||||
}: {
|
||||
quote: ComposerOptsQuote
|
||||
moderation?: ModerationDecision
|
||||
onOpen?: () => void
|
||||
style?: StyleProp<ViewStyle>
|
||||
textStyle?: StyleProp<TextStyle>
|
||||
allowNestedQuotes?: boolean
|
||||
}) {
|
||||
const queryClient = useQueryClient()
|
||||
const pal = usePalette('default')
|
||||
@@ -161,16 +160,20 @@ export function QuoteEmbed({
|
||||
const embed = React.useMemo(() => {
|
||||
const e = quote.embeds?.[0]
|
||||
|
||||
if (AppBskyEmbedImages.isView(e) || AppBskyEmbedExternal.isView(e)) {
|
||||
if (allowNestedQuotes) {
|
||||
return e
|
||||
} else if (
|
||||
AppBskyEmbedRecordWithMedia.isView(e) &&
|
||||
(AppBskyEmbedImages.isView(e.media) ||
|
||||
AppBskyEmbedExternal.isView(e.media))
|
||||
) {
|
||||
return e.media
|
||||
} else {
|
||||
if (AppBskyEmbedImages.isView(e) || AppBskyEmbedExternal.isView(e)) {
|
||||
return e
|
||||
} else if (
|
||||
AppBskyEmbedRecordWithMedia.isView(e) &&
|
||||
(AppBskyEmbedImages.isView(e.media) ||
|
||||
AppBskyEmbedExternal.isView(e.media))
|
||||
) {
|
||||
return e.media
|
||||
}
|
||||
}
|
||||
}, [quote.embeds])
|
||||
}, [quote.embeds, allowNestedQuotes])
|
||||
|
||||
const onBeforePress = React.useCallback(() => {
|
||||
precacheProfile(queryClient, quote.author)
|
||||
@@ -201,7 +204,7 @@ export function QuoteEmbed({
|
||||
{richText ? (
|
||||
<RichText
|
||||
value={richText}
|
||||
style={[a.text_md, flatten(textStyle)]}
|
||||
style={a.text_md}
|
||||
numberOfLines={20}
|
||||
disableLinks
|
||||
/>
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
StyleProp,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextStyle,
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
@@ -42,13 +41,13 @@ export function PostEmbeds({
|
||||
moderation,
|
||||
onOpen,
|
||||
style,
|
||||
quoteTextStyle,
|
||||
allowNestedQuotes,
|
||||
}: {
|
||||
embed?: Embed
|
||||
moderation?: ModerationDecision
|
||||
onOpen?: () => void
|
||||
style?: StyleProp<ViewStyle>
|
||||
quoteTextStyle?: StyleProp<TextStyle>
|
||||
allowNestedQuotes?: boolean
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const {openLightbox} = useLightboxControls()
|
||||
@@ -63,11 +62,7 @@ export function PostEmbeds({
|
||||
moderation={moderation}
|
||||
onOpen={onOpen}
|
||||
/>
|
||||
<MaybeQuoteEmbed
|
||||
embed={embed.record}
|
||||
onOpen={onOpen}
|
||||
textStyle={quoteTextStyle}
|
||||
/>
|
||||
<MaybeQuoteEmbed embed={embed.record} onOpen={onOpen} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -98,8 +93,8 @@ export function PostEmbeds({
|
||||
<MaybeQuoteEmbed
|
||||
embed={embed}
|
||||
style={style}
|
||||
textStyle={quoteTextStyle}
|
||||
onOpen={onOpen}
|
||||
allowNestedQuotes={allowNestedQuotes}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user