Move to expo and react-navigation (#288)

* WIP - adding expo

* WIP - adding expo 2

* Fix tsc

* Finish adding expo

* Disable the 'require cycle' warning

* Tweak plist

* Modify some dependency versions to make expo happy

* Fix icon fill

* Get Web compiling for expo

* 1.7

* Switch to react-navigation in expo2 (#287)

* WIP Switch to react-navigation

* WIP Switch to react-navigation 2

* WIP Switch to react-navigation 3

* Convert all screens to react navigation

* Update BottomBar for react navigation

* Update mobile menu to be react-native drawer

* Fixes to drawer and bottombar

* Factor out some helpers

* Replace the navigation model with react-navigation

* Restructure the shell folder and fix the header positioning

* Restore the error boundary

* Fix tsc

* Implement not-found page

* Remove react-native-gesture-handler (no longer used)

* Handle notifee card presses

* Handle all navigations from the state layer

* Fix drawer behaviors

* Fix two linking issues

* Switch to our react-native-progress fork to fix an svg rendering issue

* Get Web working with react-navigation

* Refactor routes and navigation for a bit more clarity

* Remove dead code

* Rework Web shell to left/right nav to make this easier

* Fix ViewHeader for desktop web

* Hide profileheader back btn on desktop web

* Move the compose button to the left nav

* Implement reply prompt in threads for desktop web

* Composer refactors

* Factor out all platform-specific text input behaviors from the composer

* Small fix

* Update the web build to use tiptap for the composer

* Tune up the mention autocomplete dropdown

* Simplify the default avatar and banner

* Fixes to link cards in web composer

* Fix dropdowns on web

* Tweak load latest on desktop

* Add web beta message and feedback link

* Fix up links in desktop web
This commit is contained in:
Paul Frazee 2023-03-13 16:01:43 -05:00 committed by GitHub
parent 503e03d91e
commit 56cf890deb
222 changed files with 8705 additions and 6338 deletions

View file

@ -1,5 +1,6 @@
import React, {Component, ErrorInfo, ReactNode} from 'react'
import {ErrorScreen} from './error/ErrorScreen'
import {CenteredView} from './Views'
interface Props {
children?: ReactNode
@ -27,11 +28,13 @@ export class ErrorBoundary extends Component<Props, State> {
public render() {
if (this.state.hasError) {
return (
<ErrorScreen
title="Oh no!"
message="There was an unexpected issue in the application. Please let us know if this happened to you!"
details={this.state.error.toString()}
/>
<CenteredView>
<ErrorScreen
title="Oh no!"
message="There was an unexpected issue in the application. Please let us know if this happened to you!"
details={this.state.error.toString()}
/>
</CenteredView>
)
}

View file

@ -2,6 +2,8 @@ import React from 'react'
import {observer} from 'mobx-react-lite'
import {
Linking,
GestureResponderEvent,
Platform,
StyleProp,
TouchableWithoutFeedback,
TouchableOpacity,
@ -9,10 +11,22 @@ import {
View,
ViewStyle,
} from 'react-native'
import {
useLinkProps,
useNavigation,
StackActions,
} from '@react-navigation/native'
import {Text} from './text/Text'
import {TypographyVariant} from 'lib/ThemeContext'
import {NavigationProp} from 'lib/routes/types'
import {router} from '../../../routes'
import {useStores, RootStoreModel} from 'state/index'
import {convertBskyAppUrlIfNeeded} from 'lib/strings/url-helpers'
import {isDesktopWeb} from 'platform/detection'
type Event =
| React.MouseEvent<HTMLAnchorElement, MouseEvent>
| GestureResponderEvent
export const Link = observer(function Link({
style,
@ -20,30 +34,33 @@ export const Link = observer(function Link({
title,
children,
noFeedback,
asAnchor,
}: {
style?: StyleProp<ViewStyle>
href?: string
title?: string
children?: React.ReactNode
noFeedback?: boolean
asAnchor?: boolean
}) {
const store = useStores()
const onPress = () => {
if (href) {
handleLink(store, href, false)
}
}
const onLongPress = () => {
if (href) {
handleLink(store, href, true)
}
}
const navigation = useNavigation<NavigationProp>()
const onPress = React.useCallback(
(e?: Event) => {
if (typeof href === 'string') {
return onPressInner(store, navigation, href, e)
}
},
[store, navigation, href],
)
if (noFeedback) {
return (
<TouchableWithoutFeedback
onPress={onPress}
onLongPress={onLongPress}
delayPressIn={50}>
// @ts-ignore web only -prf
href={asAnchor ? href : undefined}>
<View style={style}>
{children ? children : <Text>{title || 'link'}</Text>}
</View>
@ -52,10 +69,10 @@ export const Link = observer(function Link({
}
return (
<TouchableOpacity
style={style}
onPress={onPress}
onLongPress={onLongPress}
delayPressIn={50}
style={style}>
// @ts-ignore web only -prf
href={asAnchor ? href : undefined}>
{children ? children : <Text>{title || 'link'}</Text>}
</TouchableOpacity>
)
@ -66,35 +83,123 @@ export const TextLink = observer(function TextLink({
style,
href,
text,
numberOfLines,
lineHeight,
}: {
type?: TypographyVariant
style?: StyleProp<TextStyle>
href: string
text: string
text: string | JSX.Element
numberOfLines?: number
lineHeight?: number
}) {
const {...props} = useLinkProps({to: href})
const store = useStores()
const onPress = () => {
handleLink(store, href, false)
}
const onLongPress = () => {
handleLink(store, href, true)
}
const navigation = useNavigation<NavigationProp>()
props.onPress = React.useCallback(
(e?: Event) => {
return onPressInner(store, navigation, href, e)
},
[store, navigation, href],
)
return (
<Text type={type} style={style} onPress={onPress} onLongPress={onLongPress}>
<Text
type={type}
style={style}
numberOfLines={numberOfLines}
lineHeight={lineHeight}
{...props}>
{text}
</Text>
)
})
function handleLink(store: RootStoreModel, href: string, longPress: boolean) {
href = convertBskyAppUrlIfNeeded(href)
if (href.startsWith('http')) {
Linking.openURL(href)
} else if (longPress) {
store.shell.closeModal() // close any active modals
store.nav.newTab(href)
} else {
store.shell.closeModal() // close any active modals
store.nav.navigate(href)
/**
* Only acts as a link on desktop web
*/
export const DesktopWebTextLink = observer(function DesktopWebTextLink({
type = 'md',
style,
href,
text,
numberOfLines,
lineHeight,
}: {
type?: TypographyVariant
style?: StyleProp<TextStyle>
href: string
text: string | JSX.Element
numberOfLines?: number
lineHeight?: number
}) {
if (isDesktopWeb) {
return (
<TextLink
type={type}
style={style}
href={href}
text={text}
numberOfLines={numberOfLines}
lineHeight={lineHeight}
/>
)
}
return (
<Text
type={type}
style={style}
numberOfLines={numberOfLines}
lineHeight={lineHeight}>
{text}
</Text>
)
})
// NOTE
// we can't use the onPress given by useLinkProps because it will
// match most paths to the HomeTab routes while we actually want to
// preserve the tab the app is currently in
//
// we also have some additional behaviors - closing the current modal,
// converting bsky urls, and opening http/s links in the system browser
//
// this method copies from the onPress implementation but adds our
// needed customizations
// -prf
function onPressInner(
store: RootStoreModel,
navigation: NavigationProp,
href: string,
e?: Event,
) {
let shouldHandle = false
if (Platform.OS !== 'web' || !e) {
shouldHandle = e ? !e.defaultPrevented : true
} else if (
!e.defaultPrevented && // onPress prevented default
// @ts-ignore Web only -prf
!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) && // ignore clicks with modifier keys
// @ts-ignore Web only -prf
(e.button == null || e.button === 0) && // ignore everything but left clicks
// @ts-ignore Web only -prf
[undefined, null, '', 'self'].includes(e.currentTarget?.target) // let browser handle "target=_blank" etc.
) {
e.preventDefault()
shouldHandle = true
}
if (shouldHandle) {
href = convertBskyAppUrlIfNeeded(href)
if (href.startsWith('http')) {
Linking.openURL(href)
} else {
store.shell.closeModal() // close any active modals
// @ts-ignore we're not able to type check on this one -prf
navigation.dispatch(StackActions.push(...router.matchPath(href)))
}
}
}

View file

@ -2,6 +2,7 @@ import React from 'react'
import {StyleSheet, TouchableOpacity} from 'react-native'
import {Text} from './text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {UpIcon} from 'lib/icons'
const HITSLOP = {left: 20, top: 20, right: 20, bottom: 20}
@ -9,10 +10,11 @@ export const LoadLatestBtn = ({onPress}: {onPress: () => void}) => {
const pal = usePalette('default')
return (
<TouchableOpacity
style={[pal.view, styles.loadLatest]}
style={[pal.view, pal.borderDark, styles.loadLatest]}
onPress={onPress}
hitSlop={HITSLOP}>
<Text type="md-bold" style={pal.text}>
<UpIcon size={16} strokeWidth={1} style={[pal.text, styles.icon]} />
Load new posts
</Text>
</TouchableOpacity>
@ -29,8 +31,15 @@ const styles = StyleSheet.create({
shadowOpacity: 0.2,
shadowOffset: {width: 0, height: 2},
shadowRadius: 4,
paddingHorizontal: 24,
paddingLeft: 20,
paddingRight: 24,
paddingVertical: 10,
borderRadius: 30,
borderWidth: 1,
},
icon: {
position: 'relative',
top: 2,
marginRight: 5,
},
})

View file

@ -25,6 +25,7 @@ const QuoteEmbed = ({quote}: {quote: ComposerOptsQuote}) => {
authorAvatar={quote.author.avatar}
authorHandle={quote.author.handle}
authorDisplayName={quote.author.displayName}
postHref={itemHref}
timestamp={quote.indexedAt}
/>
<Text type="post-text" style={pal.text} numberOfLines={6}>

View file

@ -1,6 +1,7 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import {Text} from './text/Text'
import {DesktopWebTextLink} from './Link'
import {ago} from 'lib/strings/time'
import {usePalette} from 'lib/hooks/usePalette'
import {useStores} from 'state/index'
@ -12,6 +13,7 @@ interface PostMetaOpts {
authorAvatar?: string
authorHandle: string
authorDisplayName: string | undefined
postHref: string
timestamp: string
did?: string
declarationCid?: string
@ -20,8 +22,8 @@ interface PostMetaOpts {
export const PostMeta = observer(function (opts: PostMetaOpts) {
const pal = usePalette('default')
let displayName = opts.authorDisplayName || opts.authorHandle
let handle = opts.authorHandle
const displayName = opts.authorDisplayName || opts.authorHandle
const handle = opts.authorHandle
const store = useStores()
const isMe = opts.did === store.me.did
const isFollowing =
@ -41,31 +43,35 @@ export const PostMeta = observer(function (opts: PostMetaOpts) {
) {
// two-liner with follow button
return (
<View style={[styles.metaTwoLine]}>
<View style={styles.metaTwoLine}>
<View>
<Text
type="lg-bold"
style={[pal.text]}
numberOfLines={1}
lineHeight={1.2}>
{displayName}{' '}
<Text
<View style={styles.metaTwoLineTop}>
<DesktopWebTextLink
type="lg-bold"
style={pal.text}
numberOfLines={1}
lineHeight={1.2}
text={displayName}
href={`/profile/${opts.authorHandle}`}
/>
<Text type="md" style={pal.textLight} lineHeight={1.2}>
&nbsp;&middot;&nbsp;
</Text>
<DesktopWebTextLink
type="md"
style={[styles.metaItem, pal.textLight]}
lineHeight={1.2}>
&middot; {ago(opts.timestamp)}
</Text>
</Text>
<Text
lineHeight={1.2}
text={ago(opts.timestamp)}
href={opts.postHref}
/>
</View>
<DesktopWebTextLink
type="md"
style={[styles.metaItem, pal.textLight]}
lineHeight={1.2}>
{handle ? (
<Text type="md" style={[pal.textLight]}>
@{handle}
</Text>
) : undefined}
</Text>
lineHeight={1.2}
text={`@${handle}`}
href={`/profile/${opts.authorHandle}`}
/>
</View>
<View>
@ -84,31 +90,36 @@ export const PostMeta = observer(function (opts: PostMetaOpts) {
<View style={styles.meta}>
{typeof opts.authorAvatar !== 'undefined' && (
<View style={[styles.metaItem, styles.avatar]}>
<UserAvatar
avatar={opts.authorAvatar}
handle={opts.authorHandle}
displayName={opts.authorDisplayName}
size={16}
/>
<UserAvatar avatar={opts.authorAvatar} size={16} />
</View>
)}
<View style={[styles.metaItem, styles.maxWidth]}>
<Text
<DesktopWebTextLink
type="lg-bold"
style={[pal.text]}
style={pal.text}
numberOfLines={1}
lineHeight={1.2}>
{displayName}
{handle ? (
<Text type="md" style={[pal.textLight]}>
&nbsp;{handle}
</Text>
) : undefined}
</Text>
lineHeight={1.2}
text={
<>
{displayName}
<Text type="md" style={[pal.textLight]}>
&nbsp;{handle}
</Text>
</>
}
href={`/profile/${opts.authorHandle}`}
/>
</View>
<Text type="md" style={[styles.metaItem, pal.textLight]} lineHeight={1.2}>
&middot; {ago(opts.timestamp)}
<Text type="md" style={pal.textLight} lineHeight={1.2}>
&middot;&nbsp;
</Text>
<DesktopWebTextLink
type="md"
style={[styles.metaItem, pal.textLight]}
lineHeight={1.2}
text={ago(opts.timestamp)}
href={opts.postHref}
/>
</View>
)
})
@ -125,6 +136,10 @@ const styles = StyleSheet.create({
justifyContent: 'space-between',
paddingBottom: 2,
},
metaTwoLineTop: {
flexDirection: 'row',
alignItems: 'baseline',
},
metaItem: {
paddingRight: 5,
},

View file

@ -7,7 +7,7 @@ import {Text} from './text/Text'
export function PostMutedWrapper({
isMuted,
children,
}: React.PropsWithChildren<{isMuted: boolean}>) {
}: React.PropsWithChildren<{isMuted?: boolean}>) {
const pal = usePalette('default')
const [override, setOverride] = React.useState(false)
if (!isMuted || override) {

View file

@ -1,6 +1,6 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import Svg, {Circle, Text, Defs, LinearGradient, Stop} from 'react-native-svg'
import Svg, {Circle, Path} from 'react-native-svg'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {IconProp} from '@fortawesome/fontawesome-svg-core'
import {HighPriorityImage} from 'view/com/util/images/Image'
@ -11,52 +11,48 @@ import {
PickedMedia,
} from '../../../lib/media/picker'
import {
requestPhotoAccessIfNeeded,
requestCameraAccessIfNeeded,
} from 'lib/permissions'
usePhotoLibraryPermission,
useCameraPermission,
} from 'lib/hooks/usePermissions'
import {useStores} from 'state/index'
import {colors, gradients} from 'lib/styles'
import {colors} from 'lib/styles'
import {DropdownButton} from './forms/DropdownButton'
import {usePalette} from 'lib/hooks/usePalette'
import {isWeb} from 'platform/detection'
function DefaultAvatar({size}: {size: number}) {
return (
<Svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="none">
<Circle cx="12" cy="12" r="12" fill="#0070ff" />
<Circle cx="12" cy="9.5" r="3.5" fill="#fff" />
<Path
strokeLinecap="round"
strokeLinejoin="round"
fill="#fff"
d="M 12.058 22.784 C 9.422 22.784 7.007 21.836 5.137 20.262 C 5.667 17.988 8.534 16.25 11.99 16.25 C 15.494 16.25 18.391 18.036 18.864 20.357 C 17.01 21.874 14.64 22.784 12.058 22.784 Z"
/>
</Svg>
)
}
export function UserAvatar({
size,
handle,
avatar,
displayName,
onSelectNewAvatar,
}: {
size: number
handle: string
displayName: string | undefined
avatar?: string | null
onSelectNewAvatar?: (img: PickedMedia | null) => void
}) {
const store = useStores()
const pal = usePalette('default')
const initials = getInitials(displayName || handle)
const renderSvg = (svgSize: number, svgInitials: string) => (
<Svg width={svgSize} height={svgSize} viewBox="0 0 100 100">
<Defs>
<LinearGradient id="grad" x1="0" y1="0" x2="1" y2="1">
<Stop offset="0" stopColor={gradients.blue.start} stopOpacity="1" />
<Stop offset="1" stopColor={gradients.blue.end} stopOpacity="1" />
</LinearGradient>
</Defs>
<Circle cx="50" cy="50" r="50" fill="url(#grad)" />
<Text
fill="white"
fontSize="50"
fontWeight="bold"
x="50"
y="67"
textAnchor="middle">
{svgInitials}
</Text>
</Svg>
)
const {requestCameraAccessIfNeeded} = useCameraPermission()
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
const dropdownItems = [
!isWeb && {
@ -124,7 +120,7 @@ export function UserAvatar({
source={{uri: avatar}}
/>
) : (
renderSvg(size, initials)
<DefaultAvatar size={size} />
)}
<View style={[styles.editButtonContainer, pal.btn]}>
<FontAwesomeIcon
@ -141,26 +137,10 @@ export function UserAvatar({
source={{uri: avatar}}
/>
) : (
renderSvg(size, initials)
<DefaultAvatar size={size} />
)
}
function getInitials(str: string): string {
const tokens = str
.toLowerCase()
.replace(/[^a-z]/g, '')
.split(' ')
.filter(Boolean)
.map(v => v.trim())
if (tokens.length >= 2 && tokens[0][0] && tokens[0][1]) {
return tokens[0][0].toUpperCase() + tokens[1][0].toUpperCase()
}
if (tokens.length === 1 && tokens[0][0]) {
return tokens[0][0].toUpperCase()
}
return 'X'
}
const styles = StyleSheet.create({
editButtonContainer: {
position: 'absolute',

View file

@ -1,10 +1,10 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import Svg, {Rect, Defs, LinearGradient, Stop} from 'react-native-svg'
import Svg, {Rect} from 'react-native-svg'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {IconProp} from '@fortawesome/fontawesome-svg-core'
import Image from 'view/com/util/images/Image'
import {colors, gradients} from 'lib/styles'
import {colors} from 'lib/styles'
import {
openCamera,
openCropper,
@ -13,9 +13,9 @@ import {
} from '../../../lib/media/picker'
import {useStores} from 'state/index'
import {
requestPhotoAccessIfNeeded,
requestCameraAccessIfNeeded,
} from 'lib/permissions'
usePhotoLibraryPermission,
useCameraPermission,
} from 'lib/hooks/usePermissions'
import {DropdownButton} from './forms/DropdownButton'
import {usePalette} from 'lib/hooks/usePalette'
import {isWeb} from 'platform/detection'
@ -29,6 +29,9 @@ export function UserBanner({
}) {
const store = useStores()
const pal = usePalette('default')
const {requestCameraAccessIfNeeded} = useCameraPermission()
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
const dropdownItems = [
!isWeb && {
label: 'Camera',
@ -80,19 +83,8 @@ export function UserBanner({
]
const renderSvg = () => (
<Svg width="100%" height="150" viewBox="50 0 200 100">
<Defs>
<LinearGradient id="grad" x1="0" y1="0" x2="1" y2="1">
<Stop
offset="0"
stopColor={gradients.blueDark.start}
stopOpacity="1"
/>
<Stop offset="1" stopColor={gradients.blueDark.end} stopOpacity="1" />
</LinearGradient>
</Defs>
<Rect x="0" y="0" width="400" height="100" fill="url(#grad)" />
<Rect x="0" y="0" width="400" height="100" fill="url(#grad2)" />
<Svg width="100%" height="150" viewBox="0 0 400 100">
<Rect x="0" y="0" width="400" height="100" fill="#0070ff" />
</Svg>
)

View file

@ -1,7 +1,7 @@
import React, {useState, useEffect} from 'react'
import {AppBskyActorGetProfile as GetProfile} from '@atproto/api'
import {StyleProp, StyleSheet, TextStyle} from 'react-native'
import {Link} from './Link'
import {DesktopWebTextLink} from './Link'
import {Text} from './text/Text'
import {LoadingPlaceholder} from './LoadingPlaceholder'
import {useStores} from 'state/index'
@ -14,7 +14,6 @@ export function UserInfoText({
failed,
prefix,
style,
asLink,
}: {
type?: TypographyVariant
did: string
@ -23,7 +22,6 @@ export function UserInfoText({
failed?: string
prefix?: string
style?: StyleProp<TextStyle>
asLink?: boolean
}) {
attr = attr || 'handle'
failed = failed || 'user'
@ -64,9 +62,14 @@ export function UserInfoText({
)
} else if (profile) {
inner = (
<Text type={type} style={style} lineHeight={1.2} numberOfLines={1}>{`${
prefix || ''
}${profile[attr] || profile.handle}`}</Text>
<DesktopWebTextLink
type={type}
style={style}
lineHeight={1.2}
numberOfLines={1}
href={`/profile/${profile.handle}`}
text={`${prefix || ''}${profile[attr] || profile.handle}`}
/>
)
} else {
inner = (
@ -78,17 +81,6 @@ export function UserInfoText({
)
}
if (asLink) {
const title = profile?.displayName || profile?.handle || 'User'
return (
<Link
href={`/profile/${profile?.handle ? profile.handle : did}`}
title={title}>
{inner}
</Link>
)
}
return inner
}

View file

@ -2,17 +2,19 @@ import React from 'react'
import {observer} from 'mobx-react-lite'
import {Animated, StyleSheet, TouchableOpacity, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {useNavigation} from '@react-navigation/native'
import {UserAvatar} from './UserAvatar'
import {Text} from './text/Text'
import {useStores} from 'state/index'
import {usePalette} from 'lib/hooks/usePalette'
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
import {useAnalytics} from 'lib/analytics'
import {isDesktopWeb} from '../../../platform/detection'
import {NavigationProp} from 'lib/routes/types'
import {isDesktopWeb} from 'platform/detection'
const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20}
export const ViewHeader = observer(function ViewHeader({
export const ViewHeader = observer(function ({
title,
canGoBack,
hideOnScroll,
@ -23,50 +25,55 @@ export const ViewHeader = observer(function ViewHeader({
}) {
const pal = usePalette('default')
const store = useStores()
const navigation = useNavigation<NavigationProp>()
const {track} = useAnalytics()
const onPressBack = () => {
store.nav.tab.goBack()
}
const onPressMenu = () => {
const onPressBack = React.useCallback(() => {
if (navigation.canGoBack()) {
navigation.goBack()
} else {
navigation.navigate('Home')
}
}, [navigation])
const onPressMenu = React.useCallback(() => {
track('ViewHeader:MenuButtonClicked')
store.shell.setMainMenuOpen(true)
}
if (typeof canGoBack === 'undefined') {
canGoBack = store.nav.tab.canGoBack
}
store.shell.openDrawer()
}, [track, store])
if (isDesktopWeb) {
return <></>
} else {
if (typeof canGoBack === 'undefined') {
canGoBack = navigation.canGoBack()
}
return (
<Container hideOnScroll={hideOnScroll || false}>
<TouchableOpacity
testID="viewHeaderBackOrMenuBtn"
onPress={canGoBack ? onPressBack : onPressMenu}
hitSlop={BACK_HITSLOP}
style={canGoBack ? styles.backBtn : styles.backBtnWide}>
{canGoBack ? (
<FontAwesomeIcon
size={18}
icon="angle-left"
style={[styles.backIcon, pal.text]}
/>
) : (
<UserAvatar size={30} avatar={store.me.avatar} />
)}
</TouchableOpacity>
<View style={styles.titleContainer} pointerEvents="none">
<Text type="title" style={[pal.text, styles.title]}>
{title}
</Text>
</View>
<View style={canGoBack ? styles.backBtn : styles.backBtnWide} />
</Container>
)
}
return (
<Container hideOnScroll={hideOnScroll || false}>
<TouchableOpacity
testID="viewHeaderBackOrMenuBtn"
onPress={canGoBack ? onPressBack : onPressMenu}
hitSlop={BACK_HITSLOP}
style={canGoBack ? styles.backBtn : styles.backBtnWide}>
{canGoBack ? (
<FontAwesomeIcon
size={18}
icon="angle-left"
style={[styles.backIcon, pal.text]}
/>
) : (
<UserAvatar
size={30}
handle={store.me.handle}
displayName={store.me.displayName}
avatar={store.me.avatar}
/>
)}
</TouchableOpacity>
<View style={styles.titleContainer} pointerEvents="none">
<Text type="title" style={[pal.text, styles.title]}>
{title}
</Text>
</View>
<View style={canGoBack ? styles.backBtn : styles.backBtnWide} />
</Container>
)
})
const Container = observer(
@ -119,8 +126,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 12,
paddingTop: 6,
paddingBottom: 6,
paddingVertical: 6,
},
headerFloating: {
position: 'absolute',

View file

@ -23,7 +23,6 @@ import {
ViewProps,
} from 'react-native'
import {addStyle, colors} from 'lib/styles'
import {DESKTOP_HEADER_HEIGHT} from 'lib/constants'
export function CenteredView({
style,
@ -73,14 +72,14 @@ export const ScrollView = React.forwardRef(function (
const styles = StyleSheet.create({
container: {
width: '100%',
maxWidth: 550,
maxWidth: 600,
marginLeft: 'auto',
marginRight: 'auto',
},
containerScroll: {
width: '100%',
height: `calc(100vh - ${DESKTOP_HEADER_HEIGHT}px)`,
maxWidth: 550,
minHeight: '100vh',
maxWidth: 600,
marginLeft: 'auto',
marginRight: 'auto',
},

View file

@ -17,7 +17,6 @@ import {Button, ButtonType} from './Button'
import {colors} from 'lib/styles'
import {toShareUrl} from 'lib/strings/url-helpers'
import {useStores} from 'state/index'
import {TABS_ENABLED} from 'lib/build-flags'
import {usePalette} from 'lib/hooks/usePalette'
import {useTheme} from 'lib/ThemeContext'
@ -138,15 +137,6 @@ export function PostDropdownBtn({
const store = useStores()
const dropdownItems: DropdownItem[] = [
TABS_ENABLED
? {
icon: ['far', 'clone'],
label: 'Open in new tab',
onPress() {
store.nav.newTab(itemHref)
},
}
: undefined,
{
icon: 'language',
label: 'Translate...',

View file

@ -41,6 +41,9 @@ export function RadioButton({
'secondary-light': {
borderColor: theme.palette.secondary.border,
},
default: {
borderColor: theme.palette.default.border,
},
'default-light': {
borderColor: theme.palette.default.border,
},
@ -69,6 +72,9 @@ export function RadioButton({
'secondary-light': {
backgroundColor: theme.palette.secondary.background,
},
default: {
backgroundColor: theme.palette.primary.background,
},
'default-light': {
backgroundColor: theme.palette.primary.background,
},
@ -103,6 +109,10 @@ export function RadioButton({
color: theme.palette.secondary.textInverted,
fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined,
},
default: {
color: theme.palette.default.text,
fontWeight: theme.palette.default.isLowContrast ? '500' : undefined,
},
'default-light': {
color: theme.palette.default.text,
fontWeight: theme.palette.default.isLowContrast ? '500' : undefined,

View file

@ -42,6 +42,9 @@ export function ToggleButton({
'secondary-light': {
borderColor: theme.palette.secondary.border,
},
default: {
borderColor: theme.palette.default.border,
},
'default-light': {
borderColor: theme.palette.default.border,
},
@ -77,6 +80,11 @@ export function ToggleButton({
backgroundColor: theme.palette.secondary.background,
opacity: isSelected ? 1 : 0.5,
},
default: {
backgroundColor: isSelected
? theme.palette.primary.background
: colors.gray3,
},
'default-light': {
backgroundColor: isSelected
? theme.palette.primary.background
@ -113,6 +121,10 @@ export function ToggleButton({
color: theme.palette.secondary.textInverted,
fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined,
},
default: {
color: theme.palette.default.text,
fontWeight: theme.palette.default.isLowContrast ? '500' : undefined,
},
'default-light': {
color: theme.palette.default.text,
fontWeight: theme.palette.default.isLowContrast ? '500' : undefined,