Merge branch 'main' into starter-packs

This commit is contained in:
Hailey
2024-06-04 11:19:37 -07:00
51 changed files with 1817 additions and 577 deletions
+125 -36
View File
@@ -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>
)
}
+9 -13
View File
@@ -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 -10
View File
@@ -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) {
+10 -12
View File
@@ -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(() => {
+53 -1
View File
@@ -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} />
</>
) : (
<>
+30 -29
View File
@@ -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,
+6 -11
View File
@@ -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
+2 -2
View File
@@ -97,8 +97,8 @@ export function AviFollowButton({
}),
a.absolute,
{
bottom: 0,
right: 0,
bottom: -1,
right: -1,
borderWidth: 1,
borderColor: t.atoms.bg.backgroundColor,
},
+5 -1
View File
@@ -1 +1,5 @@
export {Fragment as AviFollowButton} from 'react'
import React from 'react'
export function AviFollowButton({children}: {children: React.ReactNode}) {
return children
}
+1 -9
View File
@@ -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,
+7 -8
View File
@@ -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',
+6 -3
View File
@@ -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',
+12 -9
View File
@@ -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>(
+22 -19
View File
@@ -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 -9
View File
@@ -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}
/>
)
}
+6 -6
View File
@@ -52,12 +52,12 @@ export function ListsScreen({}: Props) {
<View style={s.hContentRegion} testID="listsScreen">
<SimpleViewHeader
showBackButton={isMobile}
style={
!isMobile && [
pal.border,
{borderLeftWidth: hairlineWidth, borderRightWidth: hairlineWidth},
]
}>
style={[
pal.border,
isMobile
? {borderBottomWidth: hairlineWidth}
: {borderLeftWidth: hairlineWidth, borderRightWidth: hairlineWidth},
]}>
<View style={{flex: 1}}>
<Text type="title-lg" style={[pal.text, {fontWeight: 'bold'}]}>
<Trans>User Lists</Trans>
+22 -33
View File
@@ -1,28 +1,26 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import Animated from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {useFocusEffect} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {makeRecordUri} from 'lib/strings/url-helpers'
import {PostThread as PostThreadComponent} from '../com/post-thread/PostThread'
import {ComposePrompt} from 'view/com/composer/Prompt'
import {s} from 'lib/styles'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {clamp} from 'lodash'
import {isWeb} from '#/platform/detection'
import {
RQKEY as POST_THREAD_RQKEY,
ThreadNode,
} from '#/state/queries/post-thread'
import {clamp} from 'lodash'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
import {useSetMinimalShellMode} from '#/state/shell'
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
import {ErrorMessage} from '../com/util/error/ErrorMessage'
import {CenteredView} from '../com/util/Views'
import {useComposerControls} from '#/state/shell/composer'
import {useSession} from '#/state/session'
import {isWeb} from '#/platform/detection'
import {useSetMinimalShellMode} from '#/state/shell'
import {useComposerControls} from '#/state/shell/composer'
import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
import {makeRecordUri} from 'lib/strings/url-helpers'
import {s} from 'lib/styles'
import {ComposePrompt} from 'view/com/composer/Prompt'
import {PostThread as PostThreadComponent} from '../com/post-thread/PostThread'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostThread'>
export function PostThreadScreen({route}: Props) {
@@ -35,7 +33,6 @@ export function PostThreadScreen({route}: Props) {
const {name, rkey} = route.params
const {isMobile} = useWebMediaQueries()
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
const {data: resolvedUri, error: uriError} = useResolveUriQuery(uri)
const [canReply, setCanReply] = React.useState(false)
useFocusEffect(
@@ -45,12 +42,10 @@ export function PostThreadScreen({route}: Props) {
)
const onPressReply = React.useCallback(() => {
if (!resolvedUri) {
if (!uri) {
return
}
const thread = queryClient.getQueryData<ThreadNode>(
POST_THREAD_RQKEY(resolvedUri.uri),
)
const thread = queryClient.getQueryData<ThreadNode>(POST_THREAD_RQKEY(uri))
if (thread?.type !== 'post') {
return
}
@@ -64,25 +59,19 @@ export function PostThreadScreen({route}: Props) {
},
onPost: () =>
queryClient.invalidateQueries({
queryKey: POST_THREAD_RQKEY(resolvedUri.uri || ''),
queryKey: POST_THREAD_RQKEY(uri),
}),
})
}, [openComposer, queryClient, resolvedUri])
}, [openComposer, queryClient, uri])
return (
<View style={s.hContentRegion}>
<View style={s.flex1}>
{uriError ? (
<CenteredView>
<ErrorMessage message={String(uriError)} />
</CenteredView>
) : (
<PostThreadComponent
uri={resolvedUri?.uri}
onPressReply={onPressReply}
onCanReply={setCanReply}
/>
)}
<PostThreadComponent
uri={uri}
onPressReply={onPressReply}
onCanReply={setCanReply}
/>
</View>
{isMobile && canReply && hasSession && (
<Animated.View
+199 -7
View File
@@ -1,12 +1,16 @@
import React from 'react'
import {
ActivityIndicator,
Image,
ImageStyle,
Platform,
Pressable,
StyleProp,
StyleSheet,
TextInput,
View,
} from 'react-native'
import {ScrollView as RNGHScrollView} from 'react-native-gesture-handler'
import {AppBskyActorDefs, AppBskyFeedDefs, moderateProfile} from '@atproto/api'
import {
FontAwesomeIcon,
@@ -18,9 +22,11 @@ import AsyncStorage from '@react-native-async-storage/async-storage'
import {useFocusEffect, useNavigation} from '@react-navigation/native'
import {useAnalytics} from '#/lib/analytics/analytics'
import {createHitslop} from '#/lib/constants'
import {HITSLOP_10} from '#/lib/constants'
import {usePalette} from '#/lib/hooks/usePalette'
import {MagnifyingGlassIcon} from '#/lib/icons'
import {makeProfileLink} from '#/lib/routes/links'
import {NavigationProp} from '#/lib/routes/types'
import {augmentSearchQuery} from '#/lib/strings/helpers'
import {s} from '#/lib/styles'
@@ -46,6 +52,7 @@ import {Pager} from '#/view/com/pager/Pager'
import {TabBar} from '#/view/com/pager/TabBar'
import {Post} from '#/view/com/post/Post'
import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
import {Link} from '#/view/com/util/Link'
import {List} from '#/view/com/util/List'
import {Text} from '#/view/com/util/text/Text'
import {CenteredView, ScrollView} from '#/view/com/util/Views'
@@ -488,6 +495,9 @@ export function SearchScreen(
const [showAutocomplete, setShowAutocomplete] = React.useState(false)
const [searchHistory, setSearchHistory] = React.useState<string[]>([])
const [selectedProfiles, setSelectedProfiles] = React.useState<
AppBskyActorDefs.ProfileViewBasic[]
>([])
useFocusEffect(
useNonReactiveCallback(() => {
@@ -504,6 +514,10 @@ export function SearchScreen(
if (history !== null) {
setSearchHistory(JSON.parse(history))
}
const profiles = await AsyncStorage.getItem('selectedProfiles')
if (profiles !== null) {
setSelectedProfiles(JSON.parse(profiles))
}
} catch (e: any) {
logger.error('Failed to load search history', {message: e})
}
@@ -562,6 +576,30 @@ export function SearchScreen(
[searchHistory, setSearchHistory],
)
const updateSelectedProfiles = React.useCallback(
async (profile: AppBskyActorDefs.ProfileViewBasic) => {
let newProfiles = [
profile,
...selectedProfiles.filter(p => p.did !== profile.did),
]
if (newProfiles.length > 5) {
newProfiles = newProfiles.slice(0, 5)
}
setSelectedProfiles(newProfiles)
try {
await AsyncStorage.setItem(
'selectedProfiles',
JSON.stringify(newProfiles),
)
} catch (e: any) {
logger.error('Failed to save selected profiles', {message: e})
}
},
[selectedProfiles, setSelectedProfiles],
)
const navigateToItem = React.useCallback(
(item: string) => {
scrollToTopWeb()
@@ -598,6 +636,16 @@ export function SearchScreen(
[navigateToItem],
)
const handleProfileClick = React.useCallback(
(profile: AppBskyActorDefs.ProfileViewBasic) => {
// Slight delay to avoid updating during push nav animation.
setTimeout(() => {
updateSelectedProfiles(profile)
}, 400)
},
[updateSelectedProfiles],
)
const onSoftReset = React.useCallback(() => {
if (isWeb) {
// Empty params resets the URL to be /search rather than /search?q=
@@ -629,6 +677,22 @@ export function SearchScreen(
[searchHistory],
)
const handleRemoveProfile = React.useCallback(
(profileToRemove: AppBskyActorDefs.ProfileViewBasic) => {
const updatedProfiles = selectedProfiles.filter(
profile => profile.did !== profileToRemove.did,
)
setSelectedProfiles(updatedProfiles)
AsyncStorage.setItem(
'selectedProfiles',
JSON.stringify(updatedProfiles),
).catch(e => {
logger.error('Failed to update selected profiles', {message: e})
})
},
[selectedProfiles],
)
return (
<View style={isWeb ? null : {flex: 1}}>
<CenteredView
@@ -689,12 +753,16 @@ export function SearchScreen(
searchText={searchText}
onSubmit={onSubmit}
onResultPress={onAutocompleteResultPress}
onProfileClick={handleProfileClick}
/>
) : (
<SearchHistory
searchHistory={searchHistory}
selectedProfiles={selectedProfiles}
onItemClick={handleHistoryItemClick}
onProfileClick={handleProfileClick}
onRemoveItemClick={handleRemoveHistoryItem}
onRemoveProfileClick={handleRemoveProfile}
/>
)}
</View>
@@ -814,12 +882,14 @@ let AutocompleteResults = ({
searchText,
onSubmit,
onResultPress,
onProfileClick,
}: {
isAutocompleteFetching: boolean
autocompleteData: AppBskyActorDefs.ProfileViewBasic[] | undefined
searchText: string
onSubmit: () => void
onResultPress: () => void
onProfileClick: (profile: AppBskyActorDefs.ProfileViewBasic) => void
}): React.ReactNode => {
const moderationOpts = useModerationOpts()
const {_} = useLingui()
@@ -850,7 +920,10 @@ let AutocompleteResults = ({
key={item.did}
profile={item}
moderation={moderateProfile(item, moderationOpts)}
onPress={onResultPress}
onPress={() => {
onProfileClick(item)
onResultPress()
}}
/>
))}
<View style={{height: 200}} />
@@ -861,17 +934,32 @@ let AutocompleteResults = ({
}
AutocompleteResults = React.memo(AutocompleteResults)
function truncateText(text: string, maxLength: number) {
if (text.length > maxLength) {
return text.substring(0, maxLength) + '...'
}
return text
}
function SearchHistory({
searchHistory,
selectedProfiles,
onItemClick,
onProfileClick,
onRemoveItemClick,
onRemoveProfileClick,
}: {
searchHistory: string[]
selectedProfiles: AppBskyActorDefs.ProfileViewBasic[]
onItemClick: (item: string) => void
onProfileClick: (profile: AppBskyActorDefs.ProfileViewBasic) => void
onRemoveItemClick: (item: string) => void
onRemoveProfileClick: (profile: AppBskyActorDefs.ProfileViewBasic) => void
}) {
const {isTabletOrDesktop} = useWebMediaQueries()
const {isTabletOrDesktop, isMobile} = useWebMediaQueries()
const pal = usePalette('default')
const {_} = useLingui()
return (
<CenteredView
sideBorders={isTabletOrDesktop}
@@ -880,12 +968,70 @@ function SearchHistory({
height: isWeb ? '100vh' : undefined,
}}>
<View style={styles.searchHistoryContainer}>
{(searchHistory.length > 0 || selectedProfiles.length > 0) && (
<Text style={[pal.text, styles.searchHistoryTitle]}>
<Trans>Recent Searches</Trans>
</Text>
)}
{selectedProfiles.length > 0 && (
<View
style={[
styles.selectedProfilesContainer,
isMobile && styles.selectedProfilesContainerMobile,
]}>
<RNGHScrollView
keyboardShouldPersistTaps="handled"
horizontal={true}
style={styles.profilesRow}
contentContainerStyle={{
borderWidth: 0,
}}>
{selectedProfiles.slice(0, 5).map((profile, index) => (
<View
key={index}
style={[
styles.profileItem,
isMobile && styles.profileItemMobile,
]}>
<Link
href={makeProfileLink(profile)}
title={profile.handle}
asAnchor
anchorNoUnderline
onBeforePress={() => onProfileClick(profile)}
style={styles.profilePressable}>
<Image
source={{uri: profile.avatar}}
style={styles.profileAvatar as StyleProp<ImageStyle>}
accessibilityIgnoresInvertColors
/>
<Text style={[pal.text, styles.profileName]}>
{truncateText(profile.displayName || '', 12)}
</Text>
</Link>
<Pressable
accessibilityRole="button"
accessibilityLabel={_(msg`Remove profile`)}
accessibilityHint={_(
msg`Remove profile from search history`,
)}
onPress={() => onRemoveProfileClick(profile)}
hitSlop={createHitslop(6)}
style={styles.profileRemoveBtn}>
<FontAwesomeIcon
icon="xmark"
size={14}
style={pal.textLight as FontAwesomeIconStyle}
/>
</Pressable>
</View>
))}
</RNGHScrollView>
</View>
)}
{searchHistory.length > 0 && (
<View style={styles.searchHistoryContent}>
<Text style={[pal.text, styles.searchHistoryTitle]}>
<Trans>Recent Searches</Trans>
</Text>
{searchHistory.map((historyItem, index) => (
{searchHistory.slice(0, 5).map((historyItem, index) => (
<View
key={index}
style={[
@@ -982,11 +1128,57 @@ const styles = StyleSheet.create({
width: '100%',
paddingHorizontal: 12,
},
selectedProfilesContainer: {
marginTop: 10,
paddingHorizontal: 12,
height: 80,
},
selectedProfilesContainerMobile: {
height: 100,
},
profilesRow: {
flexDirection: 'row',
flexWrap: 'nowrap',
},
profileItem: {
alignItems: 'center',
marginRight: 15,
width: 78,
},
profileItemMobile: {
width: 70,
},
profilePressable: {
alignItems: 'center',
},
profileAvatar: {
width: 60,
height: 60,
borderRadius: 45,
},
profileName: {
fontSize: 12,
textAlign: 'center',
marginTop: 5,
},
profileRemoveBtn: {
position: 'absolute',
top: 0,
right: 5,
backgroundColor: 'white',
borderRadius: 10,
width: 18,
height: 18,
alignItems: 'center',
justifyContent: 'center',
},
searchHistoryContent: {
padding: 10,
paddingHorizontal: 10,
borderRadius: 8,
},
searchHistoryTitle: {
fontWeight: 'bold',
paddingVertical: 12,
paddingHorizontal: 10,
},
})
+32 -3
View File
@@ -60,6 +60,7 @@ import {Text} from 'view/com/util/text/Text'
import * as Toast from 'view/com/util/Toast'
import {UserAvatar} from 'view/com/util/UserAvatar'
import {ScrollView} from 'view/com/util/Views'
import {DeactivateAccountDialog} from '#/screens/Settings/components/DeactivateAccountDialog'
import {useTheme} from '#/alf'
import {useDialogControl} from '#/components/Dialog'
import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings'
@@ -103,10 +104,10 @@ function SettingsAccountCard({
/>
</View>
<View style={[s.flex1]}>
<Text type="md-bold" style={pal.text}>
<Text type="md-bold" style={pal.text} numberOfLines={1}>
{profile?.displayName || account.handle}
</Text>
<Text type="sm" style={pal.textLight}>
<Text type="sm" style={pal.textLight} numberOfLines={1}>
{account.handle}
</Text>
</View>
@@ -307,6 +308,11 @@ export function SettingsScreen({}: Props) {
Toast.show(_(msg`Legacy storage cleared, you need to restart the app now.`))
}, [_])
const deactivateAccountControl = useDialogControl()
const onPressDeactivateAccount = React.useCallback(() => {
deactivateAccountControl.open()
}, [deactivateAccountControl])
const {mutate: onPressDeleteChatDeclaration} = useDeleteActorDeclaration()
return (
@@ -381,7 +387,7 @@ export function SettingsScreen({}: Props) {
{!currentAccount.emailConfirmed && <EmailConfirmationNotice />}
<View style={[s.flexRow, styles.heading]}>
<Text type="xl-bold" style={pal.text}>
<Text type="xl-bold" style={pal.text} numberOfLines={1}>
<Trans>Signed in as</Trans>
</Text>
<View style={s.flex1} />
@@ -791,6 +797,29 @@ export function SettingsScreen({}: Props) {
<Trans>Export My Data</Trans>
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[pal.view, styles.linkCard]}
onPress={onPressDeactivateAccount}
accessible={true}
accessibilityRole="button"
accessibilityLabel={_(msg`Deactivate account`)}
accessibilityHint={_(
msg`Opens modal for account deactivation confirmation`,
)}>
<View style={[styles.iconContainer, dangerBg]}>
<FontAwesomeIcon
icon={'users-slash'}
style={dangerText as FontAwesomeIconStyle}
size={18}
/>
</View>
<Text type="lg" style={dangerText}>
<Trans>Deactivate my account</Trans>
</Text>
</TouchableOpacity>
<DeactivateAccountDialog control={deactivateAccountControl} />
<TouchableOpacity
style={[pal.view, styles.linkCard]}
onPress={onPressDeleteAccount}
+11 -20
View File
@@ -1,22 +1,20 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import Animated, {FadeIn, FadeInDown, FadeOut} from 'react-native-reanimated'
import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {useComposerState} from 'state/shell/composer'
import {
EmojiPicker,
EmojiPickerState,
} from 'view/com/composer/text-input/web/EmojiPicker.web'
import {useBreakpoints, useTheme} from '#/alf'
import {ComposePost} from '../com/composer/Composer'
const BOTTOM_BAR_HEIGHT = 61
export function Composer({}: {winHeight: number}) {
const pal = usePalette('default')
const {isMobile} = useWebMediaQueries()
const t = useTheme()
const {gtMobile} = useBreakpoints()
const state = useComposerState()
const isActive = !!state
useWebBodyScrollLock(isActive)
@@ -49,20 +47,13 @@ export function Composer({}: {winHeight: number}) {
}
return (
<Animated.View
style={styles.mask}
aria-modal
accessibilityViewIsModal
entering={FadeIn.duration(100)}
exiting={FadeOut}>
<Animated.View
entering={FadeInDown.duration(150)}
exiting={FadeOut}
<View style={styles.mask} aria-modal accessibilityViewIsModal>
<View
style={[
styles.container,
isMobile && styles.containerMobile,
pal.view,
pal.border,
!gtMobile && styles.containerMobile,
t.atoms.bg,
t.atoms.border_contrast_medium,
]}>
<ComposePost
replyTo={state.replyTo}
@@ -72,9 +63,9 @@ export function Composer({}: {winHeight: number}) {
openPicker={onOpenPicker}
text={state.text}
/>
</Animated.View>
</View>
<EmojiPicker state={pickerState} close={onClosePicker} />
</Animated.View>
</View>
)
}
@@ -94,12 +85,12 @@ const styles = StyleSheet.create({
maxWidth: 600,
width: '100%',
paddingVertical: 0,
paddingHorizontal: 2,
borderRadius: 8,
marginBottom: 0,
borderWidth: 1,
// @ts-ignore web only
maxHeight: 'calc(100% - (40px * 2))',
overflow: 'hidden',
},
containerMobile: {
borderRadius: 0,
@@ -32,6 +32,7 @@ import {
import {isWeb} from 'platform/detection'
import {Deactivated} from '#/screens/Deactivated'
import {Onboarding} from '#/screens/Onboarding'
import {SignupQueued} from '#/screens/SignupQueued'
import {LoggedOut} from '../com/auth/LoggedOut'
import {BottomBarWeb} from './bottom-bar/BottomBarWeb'
import {DesktopLeftNav} from './desktop/LeftNav'
@@ -102,12 +103,15 @@ function NativeStackNavigator({
if ((!PWI_ENABLED || activeRouteRequiresAuth) && !hasSession) {
return <LoggedOut />
}
if (hasSession && currentAccount?.deactivated) {
return <Deactivated />
if (hasSession && currentAccount?.signupQueued) {
return <SignupQueued />
}
if (showLoggedOut) {
return <LoggedOut onDismiss={() => setShowLoggedOut(false)} />
}
if (currentAccount?.status === 'deactivated') {
return <Deactivated />
}
if (onboardingState.isActive) {
return <Onboarding />
}