This commit is contained in:
Eric Bailey
2023-12-19 17:09:02 -06:00
parent e84d035e86
commit e333a3bc38
16 changed files with 331 additions and 366 deletions
+24 -19
View File
@@ -13,6 +13,7 @@ import {
import 'view/icons'
import {ThemeProvider as Alf} from '#/alf'
import {init as initPersistedState} from '#/state/persisted'
import {listenSessionDropped} from './state/events'
import {useColorMode} from 'state/shell'
@@ -39,6 +40,7 @@ import {
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
import * as persisted from '#/state/persisted'
import {Splash} from '#/Splash'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
SplashScreen.preventAutoHideAsync()
@@ -46,6 +48,7 @@ function InnerApp() {
const colorMode = useColorMode()
const {isInitialLoad, currentAccount} = useSession()
const {resumeSession} = useSessionApi()
const theme = useColorModeTheme(colorMode)
// init
useEffect(() => {
@@ -60,25 +63,27 @@ function InnerApp() {
return (
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
<Splash isReady={!isInitialLoad}>
<React.Fragment
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<LoggedOutViewProvider>
<UnreadNotifsProvider>
<ThemeProvider theme={colorMode}>
{/* All components should be within this provider */}
<RootSiblingParent>
<GestureHandlerRootView style={s.h100pct}>
<TestCtrls />
<Shell />
</GestureHandlerRootView>
</RootSiblingParent>
</ThemeProvider>
</UnreadNotifsProvider>
</LoggedOutViewProvider>
</React.Fragment>
</Splash>
<Alf theme={theme}>
<Splash isReady={!isInitialLoad}>
<React.Fragment
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<LoggedOutViewProvider>
<UnreadNotifsProvider>
<ThemeProvider theme={colorMode}>
{/* All components should be within this provider */}
<RootSiblingParent>
<GestureHandlerRootView style={s.h100pct}>
<TestCtrls />
<Shell />
</GestureHandlerRootView>
</RootSiblingParent>
</ThemeProvider>
</UnreadNotifsProvider>
</LoggedOutViewProvider>
</React.Fragment>
</Splash>
</Alf>
</SafeAreaProvider>
)
}
+3 -3
View File
@@ -96,9 +96,9 @@ const sizeVariants: {
} = {
small: {
pressable: {
py: 'm',
px: 'l',
radius: 'm',
py: 's',
px: 'm',
radius: 'round',
},
text: {
fontSize: 'm',
+3
View File
@@ -0,0 +1,3 @@
import {Pressable as RNPressable} from 'react-native'
import {styled} from '#/alf/system'
export const Pressable = styled(RNPressable, {})
+2
View File
@@ -1,4 +1,6 @@
export * from '#/alf/system'
export * from '#/alf/util/platform'
export * from '#/alf/components/Box'
export * from '#/alf/components/Typography'
export * from '#/alf/components/Button'
export * from '#/alf/components/Pressable'
+2
View File
@@ -147,6 +147,8 @@ export const light = createTheme({
jcc: (_: boolean) => ({justifyContent: 'center'}),
/** Shorthand for `justifyContent: 'space-between'` */
jcb: (_: boolean) => ({justifyContent: 'space-between'}),
/** Shorthand for `justifyContent: 'flex-end'` */
jce: (_: boolean) => ({justifyContent: 'flex-end'}),
/** Shorthand for `position: 'absolute'` */
abs: (_: boolean) => ({position: 'absolute'}),
/** Shorthand for `StyleSheet.absoluteFillObject` */
+7
View File
@@ -18,6 +18,13 @@ export function android(value: any) {
})
}
export function notAndroid(value: any) {
return Platform.select({
ios: value,
web: value,
})
}
export function native(value: any) {
return Platform.select({
native: value,
+6 -5
View File
@@ -30,6 +30,7 @@ import {isWeb} from '#/platform/detection'
import {listenPostCreated} from '#/state/events'
import {useSession} from '#/state/session'
import {STALE} from '#/state/queries'
import {Box} from '#/alf'
const LOADING_ITEM = {_reactKey: '__loading__'}
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
@@ -273,14 +274,14 @@ let Feed = ({
const offset = Math.max(headerOffset, 32) * (isWeb ? 1 : 2)
return isFetchingNextPage ? (
<View style={[styles.feedFooter]}>
<Box pt='xl'>
<ActivityIndicator />
<View style={{height: offset}} />
</View>
<Box h={offset} />
</Box>
) : shouldRenderEndOfFeed ? (
<View style={{minHeight: offset}}>{renderEndOfFeed()}</View>
<Box minHeight={offset}>{renderEndOfFeed()}</Box>
) : (
<View style={{height: offset}} />
<Box h={offset} />
)
}, [isFetchingNextPage, shouldRenderEndOfFeed, renderEndOfFeed, headerOffset])
+39 -38
View File
@@ -13,7 +13,6 @@ import {
} from '@fortawesome/react-native-fontawesome'
import {ReasonFeedSource, isReasonFeedSource} from 'lib/api/feed/types'
import {Link, TextLinkOnWebOnly, TextLink} from '../util/Link'
import {Text} from '../util/text/Text'
import {UserInfoText} from '../util/UserInfoText'
import {PostMeta} from '../util/PostMeta'
import {PostCtrls} from '../util/post-ctrls/PostCtrls'
@@ -34,6 +33,7 @@ import {countLines} from 'lib/strings/helpers'
import {useComposerControls} from '#/state/shell/composer'
import {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow'
import {FeedNameText} from '../util/FeedInfoText'
import {Box, Text, useStyle} from '#/alf'
export function FeedItem({
post,
@@ -130,18 +130,19 @@ let FeedItemInner = ({
})
}, [post, record, openComposer])
const outerStyles = [
styles.outer,
pal.view,
{
borderColor: pal.colors.border,
paddingBottom:
isThreadLastChild || (!isThreadChild && !isThreadParent)
? 6
: undefined,
},
isThreadChild ? styles.outerSmallTop : undefined,
]
const outerStyles = useStyle(React.useMemo(() => ({
bg: 'l0',
borderTopWidth: isThreadChild ? 0 : 1,
paddingLeft: 'm',
pr: 'l',
cursor: 'pointer',
overflow: 'hidden',
borderColor: 'l2',
paddingBottom:
isThreadLastChild || (!isThreadChild && !isThreadParent)
? 's'
: undefined,
}), []))
return (
<Link
@@ -152,8 +153,8 @@ let FeedItemInner = ({
accessible={false}>
<PostSandboxWarning />
<View style={{flexDirection: 'row', gap: 10, paddingLeft: 8}}>
<View style={{width: 52}}>
<Box row gap='s' pl='s'>
<Box width={52}>
{isThreadChild && (
<View
style={[
@@ -166,15 +167,15 @@ let FeedItemInner = ({
]}
/>
)}
</View>
</Box>
<View style={{paddingTop: 12, flexShrink: 1}}>
<Box pt='m' flexShrink={1}>
{isReasonFeedSource(reason) ? (
<Link href={reason.href}>
<Text
type="sm-bold"
style={pal.textLight}
lineHeight={1.2}
fontSize='s'
fontWeight='bold'
c='l5'
numberOfLines={1}>
From{' '}
<FeedNameText
@@ -203,9 +204,9 @@ let FeedItemInner = ({
}}
/>
<Text
type="sm-bold"
style={pal.textLight}
lineHeight={1.2}
fontSize='s'
fontWeight='bold'
c='l5'
numberOfLines={1}>
Reposted by{' '}
<TextLinkOnWebOnly
@@ -221,11 +222,11 @@ let FeedItemInner = ({
</Text>
</Link>
) : null}
</View>
</View>
</Box>
</Box>
<View style={styles.layout}>
<View style={styles.layoutAvi}>
<Box row gap='m' mt={1}>
<Box pl='s'>
<PreviewableUserAvatar
size={52}
did={post.author.did}
@@ -245,8 +246,8 @@ let FeedItemInner = ({
]}
/>
)}
</View>
<View style={styles.layoutContent}>
</Box>
<Box flex={1}>
<PostMeta
author={post.author}
authorHasWarning={!!post.author.labels?.length}
@@ -254,7 +255,7 @@ let FeedItemInner = ({
postHref={href}
/>
{!isThreadChild && replyAuthorDid !== '' && (
<View style={[s.flexRow, s.mb2, s.alignCenter]}>
<Box row aic mb={2}>
<FontAwesomeIcon
icon="reply"
size={9}
@@ -264,9 +265,9 @@ let FeedItemInner = ({
]}
/>
<Text
type="md"
style={[pal.textLight, s.mr2]}
lineHeight={1.2}
fontSize='m'
c='l4'
mr={2}
numberOfLines={1}>
Reply to{' '}
<UserInfoText
@@ -276,7 +277,7 @@ let FeedItemInner = ({
style={[pal.textLight, s.ml2]}
/>
</Text>
</View>
</Box>
)}
<PostContent
moderation={moderation}
@@ -285,8 +286,8 @@ let FeedItemInner = ({
postAuthor={post.author}
/>
<PostCtrls post={post} record={record} onPressReply={onPressReply} />
</View>
</View>
</Box>
</Box>
</Link>
)
}
@@ -320,7 +321,7 @@ let PostContent = ({
childContainerStyle={styles.contentHiderChild}>
<PostAlerts moderation={moderation.content} style={styles.alert} />
{richText.text ? (
<View style={styles.postTextContainer}>
<Box row aic flexWrap='wrap' pb='xs'>
<RichText
testID="postText"
type="post-text"
@@ -329,7 +330,7 @@ let PostContent = ({
numberOfLines={limitLines ? MAX_POST_LINES : undefined}
style={s.flex1}
/>
</View>
</Box>
) : undefined}
{limitLines ? (
<TextLink
+37 -58
View File
@@ -1,10 +1,7 @@
import React from 'react'
import {Pressable, StyleSheet, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {useNavigation} from '@react-navigation/native'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {Text} from '../util/text/Text'
import {TextLink} from '../util/Link'
import {UserAvatar, UserAvatarType} from '../util/UserAvatar'
import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
@@ -13,13 +10,14 @@ import {sanitizeHandle} from 'lib/strings/handles'
import {makeProfileLink} from 'lib/routes/links'
import {NavigationProp} from 'lib/routes/types'
import {BACK_HITSLOP} from 'lib/constants'
import {isNative} from 'platform/detection'
import {useLightboxControls, ImagesLightbox} from '#/state/lightbox'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
import {useSetDrawerOpen} from '#/state/shell'
import {emitSoftReset} from '#/state/events'
import { Box, useTokens, web, Pressable, Text } from '#/alf'
export function ProfileSubpageHeader({
isLoading,
href,
@@ -48,7 +46,7 @@ export function ProfileSubpageHeader({
const {_} = useLingui()
const {isMobile} = useWebMediaQueries()
const {openLightbox} = useLightboxControls()
const pal = usePalette('default')
const tokens = useTokens()
const canGoBack = navigation.canGoBack()
const onPressBack = React.useCallback(() => {
@@ -72,25 +70,25 @@ export function ProfileSubpageHeader({
}, [openLightbox, avatar])
return (
<CenteredView style={pal.view}>
<CenteredView style={{ backgroundColor: tokens.color.l0 }}>
{isMobile && (
<View
style={[
{
flexDirection: 'row',
alignItems: 'center',
borderBottomWidth: 1,
paddingTop: isNative ? 0 : 8,
paddingBottom: 8,
paddingHorizontal: isMobile ? 12 : 14,
},
pal.border,
]}>
<Box
row aic
pt={web('s')}
pb='s'
px='m'
borderColor='l3'
borderBottomWidth={1}
gtMobile={{
px: 14
}}>
<Pressable
testID="headerDrawerBtn"
onPress={canGoBack ? onPressBack : onPressMenu}
hitSlop={BACK_HITSLOP}
style={canGoBack ? styles.backBtn : styles.backBtnWide}
w={canGoBack ? 20 : 40}
h={30}
px={canGoBack ? 0 : 6}
accessibilityRole="button"
accessibilityLabel={canGoBack ? 'Back' : 'Menu'}
accessibilityHint="">
@@ -98,28 +96,28 @@ export function ProfileSubpageHeader({
<FontAwesomeIcon
size={18}
icon="angle-left"
style={[styles.backIcon, pal.text]}
style={{ marginTop: tokens.space.s, color: tokens.color.l5 }}
/>
) : (
<FontAwesomeIcon
size={18}
icon="bars"
style={[styles.backIcon, pal.textLight]}
style={{ marginTop: tokens.space.s, color: tokens.color.l5 }}
/>
)}
</Pressable>
<View style={{flex: 1}} />
<Box style={{flex: 1}} />
{children}
</View>
</Box>
)}
<View
style={{
flexDirection: 'row',
alignItems: 'flex-start',
gap: 10,
paddingTop: 14,
paddingBottom: 6,
paddingHorizontal: isMobile ? 12 : 14,
<Box
row
gap='m'
pt='m'
pb='s'
px='m'
gtMobile={{
px: 14
}}>
<Pressable
testID="headerAviButton"
@@ -130,7 +128,7 @@ export function ProfileSubpageHeader({
style={{width: 58}}>
<UserAvatar type={avatarType} size={58} avatar={avatar} />
</Pressable>
<View style={{flex: 1}}>
<Box flex={1}>
{isLoading ? (
<LoadingPlaceholder
width={200}
@@ -142,7 +140,7 @@ export function ProfileSubpageHeader({
testID="headerTitle"
type="title-xl"
href={href}
style={[pal.text, {fontWeight: 'bold'}]}
style={{fontWeight: 'bold', color: tokens.color.l7}}
text={title || ''}
onPress={emitSoftReset}
numberOfLines={4}
@@ -152,7 +150,7 @@ export function ProfileSubpageHeader({
{isLoading ? (
<LoadingPlaceholder width={50} height={8} />
) : (
<Text type="xl" style={[pal.textLight]} numberOfLines={1}>
<Text fontSize='l' c='l5' numberOfLines={1}>
by{' '}
{!creator ? (
'—'
@@ -162,37 +160,18 @@ export function ProfileSubpageHeader({
<TextLink
text={sanitizeHandle(creator.handle, '@')}
href={makeProfileLink(creator)}
style={pal.textLight}
style={{ color: tokens.color.l5 }}
/>
)}
</Text>
)}
</View>
</Box>
{!isMobile && (
<View
style={{
flexDirection: 'row',
alignItems: 'center',
}}>
<Box row aic>
{children}
</View>
</Box>
)}
</View>
</Box>
</CenteredView>
)
}
const styles = StyleSheet.create({
backBtn: {
width: 20,
height: 30,
},
backBtnWide: {
width: 20,
height: 30,
paddingHorizontal: 6,
},
backIcon: {
marginTop: 6,
},
})
+32 -32
View File
@@ -11,6 +11,7 @@ import {HeartIcon, HeartIconSolid} from 'lib/icons'
import {s} from 'lib/styles'
import {useTheme} from 'lib/ThemeContext'
import {usePalette} from 'lib/hooks/usePalette'
import {Box, useTokens} from '#/alf'
export function LoadingPlaceholder({
width,
@@ -42,58 +43,57 @@ export function PostLoadingPlaceholder({
}: {
style?: StyleProp<ViewStyle>
}) {
const theme = useTheme()
const pal = usePalette('default')
const tokens = useTokens()
return (
<View style={[styles.post, pal.view, style]}>
<LoadingPlaceholder
<Box bg='l0' row px='m' pt='xl' pb='xs'>
<Box
bg='l1'
width={52}
height={52}
style={[
styles.avatar,
{
position: 'relative',
top: -6,
},
]}
radius='round'
mr='m'
ml='s'
top={-6}
/>
<View style={[s.flex1]}>
<LoadingPlaceholder width={100} height={6} style={{marginBottom: 10}} />
<LoadingPlaceholder width="95%" height={6} style={{marginBottom: 8}} />
<LoadingPlaceholder width="95%" height={6} style={{marginBottom: 8}} />
<LoadingPlaceholder width="80%" height={6} style={{marginBottom: 15}} />
<View style={s.flexRow}>
<View style={s.flex1}>
<Box flex={1}>
<Box gap='s'>
<Box bg='l1' width={100} height={6} radius='s' />
<Box bg='l1' width="95%" height={6} radius='s' />
<Box bg='l1' width="95%" height={6} radius='s' />
<Box bg='l1' width="80%" height={6} radius='s' mb='s' />
</Box>
<Box row>
<Box column>
<FontAwesomeIcon
style={{color: theme.palette.default.icon}}
style={{color: tokens.color.l2}}
icon={['far', 'comment']}
size={14}
/>
</View>
<View style={s.flex1}>
</Box>
<Box column>
<FontAwesomeIcon
style={{color: theme.palette.default.icon}}
style={{color: tokens.color.l2}}
icon="retweet"
size={18}
/>
</View>
<View style={s.flex1}>
</Box>
<Box column>
<HeartIcon
style={{color: theme.palette.default.icon} as ViewStyle}
style={{color: tokens.color.l2}}
size={17}
strokeWidth={1.7}
/>
</View>
<View style={s.flex1} />
</View>
</View>
</View>
</Box>
<Box column />
</Box>
</Box>
</Box>
)
}
export function PostFeedLoadingPlaceholder() {
return (
<View>
<Box>
<PostLoadingPlaceholder />
<PostLoadingPlaceholder />
<PostLoadingPlaceholder />
@@ -102,7 +102,7 @@ export function PostFeedLoadingPlaceholder() {
<PostLoadingPlaceholder />
<PostLoadingPlaceholder />
<PostLoadingPlaceholder />
</View>
</Box>
)
}
+13 -32
View File
@@ -1,6 +1,5 @@
import React, {memo} from 'react'
import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'
import {Text} from './text/Text'
import {StyleProp, TextStyle, ViewStyle} from 'react-native'
import {TextLinkOnWebOnly} from './Link'
import {niceDate} from 'lib/strings/time'
import {usePalette} from 'lib/hooks/usePalette'
@@ -11,6 +10,7 @@ import {sanitizeHandle} from 'lib/strings/handles'
import {isAndroid} from 'platform/detection'
import {TimeElapsed} from './TimeElapsed'
import {makeProfileLink} from 'lib/routes/links'
import {Box, Text, android, notAndroid} from '#/alf'
interface PostMetaOpts {
author: {
@@ -35,17 +35,17 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
const handle = opts.author.handle
return (
<View style={[styles.container, opts.style]}>
<Box row aic pb={2} gap='xs' zIndex={1} style={[opts.style]}>
{opts.showAvatar && (
<View style={styles.avatar}>
<Box alignSelf='center'>
<UserAvatar
avatar={opts.author.avatar}
size={opts.avatarSize || 16}
// TODO moderation
/>
</View>
</Box>
)}
<View style={styles.maxWidth}>
<Box flex={android(1)} maxWidth={notAndroid('80%')}>
<TextLinkOnWebOnly
type={opts.displayNameType || 'lg-bold'}
style={[pal.text, opts.displayNameStyle]}
@@ -55,22 +55,21 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
<>
{sanitizeDisplayName(displayName)}&nbsp;
<Text
type="md"
numberOfLines={1}
lineHeight={1.2}
style={pal.textLight}>
c='l4'
fontSize='m'
fontWeight='normal'
numberOfLines={1}>
{sanitizeHandle(handle, '@')}
</Text>
</>
}
href={makeProfileLink(opts.author)}
/>
</View>
</Box>
{!isAndroid && (
<Text
type="md"
fontSize='m'
style={pal.textLight}
lineHeight={1.2}
accessible={false}>
&middot;
</Text>
@@ -89,26 +88,8 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
/>
)}
</TimeElapsed>
</View>
</Box>
)
}
PostMeta = memo(PostMeta)
export {PostMeta}
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
paddingBottom: 2,
gap: 4,
zIndex: 1,
flex: 1,
},
avatar: {
alignSelf: 'center',
},
maxWidth: {
flex: isAndroid ? 1 : undefined,
maxWidth: !isAndroid ? '80%' : undefined,
},
})
+23 -22
View File
@@ -1,5 +1,5 @@
import React, {ComponentProps} from 'react'
import {StyleSheet, TouchableWithoutFeedback} from 'react-native'
import {TouchableWithoutFeedback} from 'react-native'
import LinearGradient from 'react-native-linear-gradient'
import {gradients} from 'lib/styles'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
@@ -8,6 +8,8 @@ import {clamp} from 'lib/numbers'
import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
import Animated from 'react-native-reanimated'
import {useStyles} from '#/alf'
export interface FABProps
extends ComponentProps<typeof TouchableWithoutFeedback> {
testID?: string
@@ -18,6 +20,26 @@ export function FABInner({testID, icon, ...props}: FABProps) {
const insets = useSafeAreaInsets()
const {isMobile, isTablet} = useWebMediaQueries()
const {fabMinimalShellTransform} = useMinimalShellMode()
const styles = useStyles(React.useMemo(() => ({
sizeRegular: {
width: 60,
height: 60,
radius: 'round',
},
sizeLarge: {
width: 70,
height: 70,
radius: 'round',
},
outer: {
position: 'absolute',
zIndex: 1,
},
inner: {
justifyContent: 'center',
alignItems: 'center',
},
}), []))
const size = React.useMemo(() => {
return isTablet ? styles.sizeLarge : styles.sizeRegular
@@ -51,24 +73,3 @@ export function FABInner({testID, icon, ...props}: FABProps) {
</TouchableWithoutFeedback>
)
}
const styles = StyleSheet.create({
sizeRegular: {
width: 60,
height: 60,
borderRadius: 30,
},
sizeLarge: {
width: 70,
height: 70,
borderRadius: 35,
},
outer: {
position: 'absolute',
zIndex: 1,
},
inner: {
justifyContent: 'center',
alignItems: 'center',
},
})
+43 -41
View File
@@ -1,7 +1,6 @@
import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {TouchableOpacity, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {colors} from 'lib/styles'
import {HITSLOP_20} from 'lib/constants'
@@ -11,6 +10,8 @@ const AnimatedTouchableOpacity =
Animated.createAnimatedComponent(TouchableOpacity)
import {isWeb} from 'platform/detection'
import { useStyles, useTokens } from '#/alf'
export function LoadLatestBtn({
onPress,
label,
@@ -20,18 +21,53 @@ export function LoadLatestBtn({
label: string
showIndicator: boolean
}) {
const pal = usePalette('default')
const {isDesktop, isTablet, isMobile} = useWebMediaQueries()
const {fabMinimalShellTransform} = useMinimalShellMode()
const tokens = useTokens()
const styles = useStyles({
loadLatest: {
// @ts-ignore 'fixed' is web only -prf
position: isWeb ? 'fixed' : 'absolute',
left: 18,
bottom: 44,
borderWidth: 1,
width: 52,
height: 52,
borderRadius: 26,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
borderColor: 'l3',
bg: 'l0',
},
loadLatestTablet: {
// @ts-ignore web only
left: 'calc(50vw - 282px)',
},
loadLatestDesktop: {
// @ts-ignore web only
left: 'calc(50vw - 382px)',
},
indicator: {
position: 'absolute',
top: 3,
right: 3,
backgroundColor: colors.blue3,
width: 12,
height: 12,
borderRadius: 6,
borderWidth: 1,
borderColor: 'l3',
},
})
return (
<AnimatedTouchableOpacity
style={[
styles.loadLatest,
isDesktop && styles.loadLatestDesktop,
isTablet && styles.loadLatestTablet,
pal.borderDark,
pal.view,
isMobile && fabMinimalShellTransform,
]}
onPress={onPress}
@@ -39,42 +75,8 @@ export function LoadLatestBtn({
accessibilityRole="button"
accessibilityLabel={label}
accessibilityHint="">
<FontAwesomeIcon icon="angle-up" color={pal.colors.text} size={19} />
{showIndicator && <View style={[styles.indicator, pal.borderDark]} />}
<FontAwesomeIcon icon="angle-up" color={tokens.color.l7} size={19} />
{showIndicator && <View style={[styles.indicator]} />}
</AnimatedTouchableOpacity>
)
}
const styles = StyleSheet.create({
loadLatest: {
// @ts-ignore 'fixed' is web only -prf
position: isWeb ? 'fixed' : 'absolute',
left: 18,
bottom: 44,
borderWidth: 1,
width: 52,
height: 52,
borderRadius: 26,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
loadLatestTablet: {
// @ts-ignore web only
left: 'calc(50vw - 282px)',
},
loadLatestDesktop: {
// @ts-ignore web only
left: 'calc(50vw - 382px)',
},
indicator: {
position: 'absolute',
top: 3,
right: 3,
backgroundColor: colors.blue3,
width: 12,
height: 12,
borderRadius: 6,
borderWidth: 1,
},
})
+17 -17
View File
@@ -7,7 +7,6 @@ import {
ViewStyle,
} from 'react-native'
import {AppBskyFeedDefs, AppBskyFeedPost} from '@atproto/api'
import {Text} from '../text/Text'
import {PostDropdownBtn} from '../forms/PostDropdownBtn'
import {HeartIcon, HeartIconSolid, CommentBottomArrow} from 'lib/icons'
import {s} from 'lib/styles'
@@ -26,6 +25,7 @@ import {
import {useComposerControls} from '#/state/shell/composer'
import {Shadow} from '#/state/cache/types'
import {useRequireAuth} from '#/state/session'
import {Box, Text, Pressable} from '#/alf'
let PostCtrls = ({
big,
@@ -129,12 +129,12 @@ let PostCtrls = ({
])
return (
<View style={[styles.ctrls, style]}>
<TouchableOpacity
<Box row jcb style={[style]}>
<Pressable
testID="replyBtn"
row aic
pa={!big ? 'xs' : 0}
style={[
styles.ctrl,
!big && styles.ctrlPad,
{paddingLeft: 0},
post.viewer?.replyDisabled ? {opacity: 0.5} : undefined,
]}
@@ -155,11 +155,11 @@ let PostCtrls = ({
size={big ? 20 : 15}
/>
{typeof post.replyCount !== 'undefined' ? (
<Text style={[defaultCtrlColor, s.ml5, s.f15]}>
<Text fontSize='s' ml='xs' c='l4'>
{post.replyCount}
</Text>
) : undefined}
</TouchableOpacity>
</Pressable>
<RepostButton
big={big}
isReposted={!!post.viewer?.repost}
@@ -167,9 +167,10 @@ let PostCtrls = ({
onRepost={onRepost}
onQuote={onQuote}
/>
<TouchableOpacity
<Pressable
testID="likeBtn"
style={[styles.ctrl, !big && styles.ctrlPad]}
row aic
pa={!big ? 'xs' : 0}
onPress={() => {
requireAuth(() => onPressToggleLike())
}}
@@ -190,16 +191,15 @@ let PostCtrls = ({
)}
{typeof post.likeCount !== 'undefined' ? (
<Text
testID="likeCount"
style={
post.viewer?.like
? [s.bold, s.likeColor, s.f15, s.ml5]
: [defaultCtrlColor, s.f15, s.ml5]
}>
fontSize='s'
ml='xs'
c={post.viewer?.like ? 'red' : 'l4'}
fontWeight={post.viewer?.like ? 'semi' : 'normal'}
testID="likeCount">
{post.likeCount}
</Text>
) : undefined}
</TouchableOpacity>
</Pressable>
{big ? undefined : (
<PostDropdownBtn
testID="postDropdownBtn"
@@ -212,7 +212,7 @@ let PostCtrls = ({
)}
{/* used for adding pad to the right side */}
<View />
</View>
</Box>
)
}
PostCtrls = memo(PostCtrls)
@@ -3,11 +3,11 @@ import {StyleProp, StyleSheet, TouchableOpacity, ViewStyle} from 'react-native'
import {RepostIcon} from 'lib/icons'
import {s, colors} from 'lib/styles'
import {useTheme} from 'lib/ThemeContext'
import {Text} from '../text/Text'
import {pluralize} from 'lib/strings/helpers'
import {HITSLOP_10, HITSLOP_20} from 'lib/constants'
import {useModalControls} from '#/state/modals'
import {useRequireAuth} from '#/state/session'
import {Pressable, Text} from '#/alf'
interface Props {
isReposted: boolean
@@ -45,12 +45,13 @@ let RepostButton = ({
}, [onRepost, onQuote, isReposted, openModal])
return (
<TouchableOpacity
<Pressable
row aic
pa={!big ? 'xs' : 0}
testID="repostBtn"
onPress={() => {
requireAuth(() => onPressToggleRepostWrapper())
}}
style={[styles.control, !big && styles.controlPad]}
accessibilityRole="button"
accessibilityLabel={`${
isReposted ? 'Undo repost' : 'Repost'
@@ -77,7 +78,7 @@ let RepostButton = ({
{repostCount}
</Text>
) : undefined}
</TouchableOpacity>
</Pressable>
)
}
RepostButton = memo(RepostButton)
+75 -95
View File
@@ -1,9 +1,8 @@
import React, {useMemo, useCallback} from 'react'
import {Dimensions, StyleSheet, View, ActivityIndicator} from 'react-native'
import {Dimensions, View, ActivityIndicator} from 'react-native'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {useIsFocused, useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
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'
@@ -15,8 +14,6 @@ import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader'
import {Feed} from 'view/com/posts/Feed'
import {TextLink} from 'view/com/util/Link'
import {ListRef} from 'view/com/util/List'
import {Button} from 'view/com/util/forms/Button'
import {Text} from 'view/com/util/text/Text'
import {RichText} from 'view/com/util/text/RichText'
import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
import {FAB} from 'view/com/util/fab/FAB'
@@ -58,6 +55,8 @@ import {useComposerControls} from '#/state/shell/composer'
import {truncateAndInvalidate} from '#/state/queries/util'
import {isNative} from '#/platform/detection'
import {Box, Text, Button, Pressable, web, native, useTokens} from '#/alf'
const SECTION_TITLES = ['Posts', 'About']
interface SectionRef {
@@ -68,7 +67,6 @@ type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFeed'>
export function ProfileFeedScreen(props: Props) {
const {rkey, name: handleOrDid} = props.route.params
const pal = usePalette('default')
const {_} = useLingui()
const navigation = useNavigation<NavigationProp>()
@@ -89,27 +87,26 @@ export function ProfileFeedScreen(props: Props) {
if (error) {
return (
<CenteredView>
<View style={[pal.view, pal.border, styles.notFoundContainer]}>
<Text type="title-lg" style={[pal.text, s.mb10]}>
<Box bg='l0' borderColor='l4' ma='m' px='l' py='m' radius='s'>
<Text fontSize='l' mb='m'>
<Trans>Could not load feed</Trans>
</Text>
<Text type="md" style={[pal.text, s.mb20]}>
<Text fontSize='m' mb='xl'>
{error.toString()}
</Text>
<View style={{flexDirection: 'row'}}>
<Box row>
<Button
type="default"
accessibilityLabel={_(msg`Go Back`)}
accessibilityHint="Return to previous page"
onPress={onPressBack}
style={{flexShrink: 1}}>
<Text type="button" style={pal.text}>
<Text c='l0' fontWeight='bold'>
<Trans>Go Back</Trans>
</Text>
</Button>
</View>
</View>
</Box>
</Box>
</CenteredView>
)
}
@@ -118,9 +115,9 @@ export function ProfileFeedScreen(props: Props) {
<ProfileFeedScreenIntermediate feedUri={resolvedUri.uri} />
) : (
<CenteredView>
<View style={s.p20}>
<Box pa='xl'>
<ActivityIndicator size="large" />
</View>
</Box>
</CenteredView>
)
}
@@ -132,9 +129,9 @@ function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) {
if (!preferences || !info) {
return (
<CenteredView>
<View style={s.p20}>
<Box pa='xl'>
<ActivityIndicator size="large" />
</View>
</Box>
</CenteredView>
)
}
@@ -155,7 +152,7 @@ export function ProfileFeedScreenInner({
feedInfo: FeedSourceFeedInfo
}) {
const {_} = useLingui()
const pal = usePalette('default')
const tokens = useTokens()
const {hasSession, currentAccount} = useSession()
const {openModal} = useModalControls()
const {openComposer} = useComposerControls()
@@ -335,44 +332,47 @@ export function ProfileFeedScreenInner({
: undefined
}
avatarType="algo">
{feedInfo && hasSession && (
<>
<Button
disabled={isSavePending || isRemovePending}
type="default"
label={isSaved ? 'Unsave' : 'Save'}
onPress={onToggleSaved}
style={styles.btn}
/>
<Button
testID={isPinned ? 'unpinBtn' : 'pinBtn'}
disabled={isPinPending || isUnpinPending}
type={isPinned ? 'default' : 'inverted'}
label={isPinned ? 'Unpin' : 'Pin to home'}
onPress={onTogglePinned}
style={styles.btn}
/>
</>
)}
<NativeDropdown
testID="headerDropdownBtn"
items={dropdownItems}
accessibilityLabel={_(msg`More options`)}
accessibilityHint="">
<View style={[pal.viewLight, styles.btn]}>
<FontAwesomeIcon
icon="ellipsis"
size={20}
color={pal.colors.text}
/>
</View>
</NativeDropdown>
<Box row gap='s' jce>
{feedInfo && hasSession && (
<>
<Button
size='small'
disabled={isSavePending || isRemovePending}
type="primary"
onPress={onToggleSaved}
>{isSaved ? 'Unsave' : 'Save'}
</Button>
<Button
size='small'
testID={isPinned ? 'unpinBtn' : 'pinBtn'}
disabled={isPinPending || isUnpinPending}
type={isPinned ? 'secondary' : 'primary'}
onPress={onTogglePinned}
>
{isPinned ? 'Unpin' : 'Pin to home'}
</Button>
</>
)}
<NativeDropdown
testID="headerDropdownBtn"
items={dropdownItems}
accessibilityLabel={_(msg`More options`)}
accessibilityHint="">
<Box bg='l1' py='s' px='m' radius='round'>
<FontAwesomeIcon
icon="ellipsis"
size={20}
color={tokens.color.l4}
/>
</Box>
</NativeDropdown>
</Box>
</ProfileSubpageHeader>
)
}, [
_,
hasSession,
pal,
feedInfo,
isPinned,
onTogglePinned,
@@ -387,7 +387,7 @@ export function ProfileFeedScreenInner({
])
return (
<View style={s.hContentRegion}>
<Box h={web('100%')} flex={native(1)}>
<PagerWithHeader
items={SECTION_TITLES}
isHeaderReady={true}
@@ -431,7 +431,7 @@ export function ProfileFeedScreenInner({
accessibilityHint=""
/>
)}
</View>
</Box>
)
}
@@ -503,7 +503,7 @@ function AboutSection({
scrollElRef: React.MutableRefObject<ScrollView | null>
isOwner: boolean
}) {
const pal = usePalette('default')
const tokens = useTokens()
const {_} = useLingui()
const scrollHandlers = useScrollHandlers()
const onScroll = useAnimatedScrollHandler(scrollHandlers)
@@ -548,52 +548,50 @@ function AboutSection({
paddingTop: headerHeight,
minHeight: Dimensions.get('window').height * 1.5,
}}>
<View
style={[
{
borderTopWidth: 1,
paddingVertical: 20,
paddingHorizontal: 20,
gap: 12,
},
pal.border,
]}>
<Box
borderColor='l2'
borderTopWidth={1}
pa='xl'
gap='m'
>
{feedInfo.description ? (
<RichText
testID="listDescription"
type="lg"
style={pal.text}
style={{ color: tokens.color.l7 }}
richText={feedInfo.description}
/>
) : (
<Text type="lg" style={[{fontStyle: 'italic'}, pal.textLight]}>
<Text fontSize='m' c='l5' fontStyle='italic'>
<Trans>No description</Trans>
</Text>
)}
<View style={{flexDirection: 'row', alignItems: 'center', gap: 10}}>
<Button
type="default"
<Box row aic gap='m'>
<Pressable
testID="toggleLikeBtn"
accessibilityLabel={_(msg`Like this feed`)}
accessibilityHint=""
disabled={!hasSession || isLikePending || isUnlikePending}
onPress={onToggleLiked}
style={{paddingHorizontal: 10}}>
bg='l1'
pa={10}
radius='round'
>
{isLiked ? (
<HeartIconSolid size={19} style={s.likeColor} />
) : (
<HeartIcon strokeWidth={3} size={19} style={pal.textLight} />
<HeartIcon strokeWidth={3} size={19} style={{ color: tokens.color.l4 }} />
)}
</Button>
</Pressable>
{typeof likeCount === 'number' && (
<TextLink
href={makeCustomFeedLink(feedOwnerDid, feedRkey, 'liked-by')}
text={`Liked by ${likeCount} ${pluralize(likeCount, 'user')}`}
style={[pal.textLight, s.semiBold]}
style={[{ color: tokens.color.l4, fontWeight: tokens.fontWeight.semi }]}
/>
)}
</View>
<Text type="md" style={[pal.textLight]} numberOfLines={1}>
</Box>
<Text fontSize='m' c='l5' numberOfLines={1}>
Created by{' '}
{isOwner ? (
'you'
@@ -604,29 +602,11 @@ function AboutSection({
did: feedInfo.creatorDid,
handle: feedInfo.creatorHandle,
})}
style={pal.textLight}
style={{ color: tokens.color.l4 }}
/>
)}
</Text>
</View>
</Box>
</ScrollView>
)
}
const styles = StyleSheet.create({
btn: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
paddingVertical: 7,
paddingHorizontal: 14,
borderRadius: 50,
marginLeft: 6,
},
notFoundContainer: {
margin: 10,
paddingHorizontal: 18,
paddingVertical: 14,
borderRadius: 6,
},
})