Merge remote-tracking branch 'origin/main' into subs/base
* origin/main: Revert "[Video] Revert safari hackfix (#5367)" (#7001) Fix squashed content on mod screens (#7004) [ELI5] Change feed page copy (#6932) Immediately parse pre-filled links in composer state (#6974) Add useFormatCurrency hook (#6978) Update view shot (#6995) Remove icon (#6994) clean rn 0.76 upgrade (#6887) Trim back prefs exposure in NUXs, make naming more friendly (#6980) [Layout] Base (#6907) Add some icons (#6976)
This commit is contained in:
@@ -124,7 +124,7 @@ type GalleryItemProps = {
|
||||
image: ComposerImage
|
||||
altTextControlStyle?: ViewStyle
|
||||
imageControlsStyle?: ViewStyle
|
||||
imageStyle?: ViewStyle
|
||||
imageStyle?: ImageStyle
|
||||
onChange: (next: ComposerImage) => void
|
||||
onRemove: () => void
|
||||
}
|
||||
@@ -160,7 +160,7 @@ const GalleryItem = ({
|
||||
|
||||
return (
|
||||
<View
|
||||
style={imageStyle}
|
||||
style={imageStyle as ViewStyle}
|
||||
// Fixes ALT and icons appearing with half opacity when the post is inactive
|
||||
renderToHardwareTextureAndroid>
|
||||
<TouchableOpacity
|
||||
@@ -221,7 +221,7 @@ const GalleryItem = ({
|
||||
|
||||
<Image
|
||||
testID="selectedPhotoImage"
|
||||
style={[styles.image, imageStyle] as ImageStyle}
|
||||
style={[styles.image, imageStyle]}
|
||||
source={{
|
||||
uri: (image.transformed ?? image.source).path,
|
||||
}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {ImagePickerAsset} from 'expo-image-picker'
|
||||
import {AppBskyFeedPostgate, RichText} from '@atproto/api'
|
||||
import {AppBskyFeedPostgate, AppBskyRichtextFacet, RichText} from '@atproto/api'
|
||||
import {nanoid} from 'nanoid/non-secure'
|
||||
|
||||
import {SelfLabel} from '#/lib/moderation'
|
||||
@@ -16,6 +16,10 @@ import {Gif} from '#/state/queries/tenor'
|
||||
import {threadgateViewToAllowUISetting} from '#/state/queries/threadgate'
|
||||
import {ThreadgateAllowUISetting} from '#/state/queries/threadgate'
|
||||
import {ComposerOpts} from '#/state/shell/composer'
|
||||
import {
|
||||
LinkFacetMatch,
|
||||
suggestLinkCardUri,
|
||||
} from '#/view/com/composer/text-input/text-input-util'
|
||||
import {createVideoState, VideoAction, videoReducer, VideoState} from './video'
|
||||
|
||||
type ImagesMedia = {
|
||||
@@ -508,6 +512,68 @@ export function createComposerState({
|
||||
)
|
||||
: '',
|
||||
})
|
||||
|
||||
let link: Link | undefined
|
||||
|
||||
/**
|
||||
* `initText` atm is only used for compose intents, meaning share links from
|
||||
* external sources. If `initText` is defined, we want to extract links/posts
|
||||
* from `initText` and suggest them as embeds.
|
||||
*
|
||||
* This checks for posts separately from other types of links so that posts
|
||||
* can become quotes. The util `suggestLinkCardUri` is then applied to ensure
|
||||
* we suggest at most 1 of each.
|
||||
*/
|
||||
if (initText) {
|
||||
initRichText.detectFacetsWithoutResolution()
|
||||
const detectedExtUris = new Map<string, LinkFacetMatch>()
|
||||
const detectedPostUris = new Map<string, LinkFacetMatch>()
|
||||
if (initRichText.facets) {
|
||||
for (const facet of initRichText.facets) {
|
||||
for (const feature of facet.features) {
|
||||
if (AppBskyRichtextFacet.isLink(feature)) {
|
||||
if (isBskyPostUrl(feature.uri)) {
|
||||
detectedPostUris.set(feature.uri, {facet, rt: initRichText})
|
||||
} else {
|
||||
detectedExtUris.set(feature.uri, {facet, rt: initRichText})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const pastSuggestedUris = new Set<string>()
|
||||
const suggestedExtUri = suggestLinkCardUri(
|
||||
true,
|
||||
detectedExtUris,
|
||||
new Map(),
|
||||
pastSuggestedUris,
|
||||
)
|
||||
if (suggestedExtUri) {
|
||||
link = {
|
||||
type: 'link',
|
||||
uri: suggestedExtUri,
|
||||
}
|
||||
}
|
||||
const suggestedPostUri = suggestLinkCardUri(
|
||||
true,
|
||||
detectedPostUris,
|
||||
new Map(),
|
||||
pastSuggestedUris,
|
||||
)
|
||||
if (suggestedPostUri) {
|
||||
/*
|
||||
* `initQuote` is only populated via in-app user action, but we're being
|
||||
* future-defensive here.
|
||||
*/
|
||||
if (!quote) {
|
||||
quote = {
|
||||
type: 'link',
|
||||
uri: suggestedPostUri,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
activePostIndex: 0,
|
||||
mutableNeedsFocusActive: false,
|
||||
@@ -521,7 +587,7 @@ export function createComposerState({
|
||||
embed: {
|
||||
quote,
|
||||
media,
|
||||
link: undefined,
|
||||
link,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -257,6 +257,10 @@ export const TextInput = forwardRef(function TextInputImpl(
|
||||
minHeight: 60,
|
||||
includeFontPadding: false,
|
||||
},
|
||||
{
|
||||
borderWidth: 1,
|
||||
borderColor: 'transparent',
|
||||
},
|
||||
]}
|
||||
{...props}>
|
||||
{textDecorated}
|
||||
|
||||
@@ -6,7 +6,7 @@ export type LinkFacetMatch = {
|
||||
}
|
||||
|
||||
export function suggestLinkCardUri(
|
||||
mayBePaste: boolean,
|
||||
suggestLinkImmediately: boolean,
|
||||
nextDetectedUris: Map<string, LinkFacetMatch>,
|
||||
prevDetectedUris: Map<string, LinkFacetMatch>,
|
||||
pastSuggestedUris: Set<string>,
|
||||
@@ -20,8 +20,8 @@ export function suggestLinkCardUri(
|
||||
// Don't suggest already added or already dismissed link cards.
|
||||
continue
|
||||
}
|
||||
if (mayBePaste) {
|
||||
// Immediately add the pasted link without waiting to type more.
|
||||
if (suggestLinkImmediately) {
|
||||
// Immediately add the pasted or intent-prefilled link without waiting to type more.
|
||||
suggestedUris.add(uri)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ export function FeedPage({
|
||||
}, [scrollToTop, feed, queryClient, setHasNew])
|
||||
|
||||
return (
|
||||
<View testID={testID} style={s.h100pct}>
|
||||
<View testID={testID}>
|
||||
<MainScrollProvider>
|
||||
<FeedFeedbackProvider value={feedFeedback}>
|
||||
<Feed
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
import React from 'react'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import {View} from 'react-native'
|
||||
import Animated from 'react-native-reanimated'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useMinimalShellHeaderTransform} from '#/lib/hooks/useMinimalShellTransform'
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {useKawaiiMode} from '#/state/preferences/kawaii'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useShellLayout} from '#/state/shell/shell-layout'
|
||||
import {HomeHeaderLayoutMobile} from '#/view/com/home/HomeHeaderLayoutMobile'
|
||||
import {Logo} from '#/view/icons/Logo'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {atoms as a, useBreakpoints, useGutterStyles, useTheme} from '#/alf'
|
||||
import {ButtonIcon} from '#/components/Button'
|
||||
import {Hashtag_Stroke2_Corner0_Rounded as FeedsIcon} from '#/components/icons/Hashtag'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {Link} from '#/components/Link'
|
||||
import {HomeHeaderLayoutMobile} from './HomeHeaderLayoutMobile'
|
||||
|
||||
export function HomeHeaderLayout(props: {
|
||||
children: React.ReactNode
|
||||
tabBarAnchor: JSX.Element | null | undefined
|
||||
}) {
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
if (isMobile) {
|
||||
const {gtMobile} = useBreakpoints()
|
||||
if (!gtMobile) {
|
||||
return <HomeHeaderLayoutMobile {...props} />
|
||||
} else {
|
||||
return <HomeHeaderLayoutDesktopAndTablet {...props} />
|
||||
@@ -40,98 +41,43 @@ function HomeHeaderLayoutDesktopAndTablet({
|
||||
const {hasSession} = useSession()
|
||||
const {_} = useLingui()
|
||||
const kawaii = useKawaiiMode()
|
||||
const gutter = useGutterStyles()
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasSession && (
|
||||
<View
|
||||
style={[
|
||||
a.relative,
|
||||
a.flex_row,
|
||||
a.justify_end,
|
||||
a.align_center,
|
||||
a.pt_lg,
|
||||
a.px_md,
|
||||
a.pb_2xs,
|
||||
t.atoms.bg,
|
||||
t.atoms.border_contrast_low,
|
||||
styles.bar,
|
||||
kawaii && {paddingTop: 22, paddingBottom: 16},
|
||||
]}>
|
||||
<Layout.Center>
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
a.pt_lg,
|
||||
a.m_auto,
|
||||
kawaii && {paddingTop: 4, paddingBottom: 0},
|
||||
{
|
||||
width: kawaii ? 84 : 28,
|
||||
},
|
||||
]}>
|
||||
<Logo width={kawaii ? 60 : 28} />
|
||||
style={[a.flex_row, a.align_center, a.pt_md, gutter, t.atoms.bg]}>
|
||||
<View style={{width: 34}} />
|
||||
<View style={[a.flex_1, a.align_center, a.justify_center]}>
|
||||
<Logo width={kawaii ? 60 : 28} />
|
||||
</View>
|
||||
<Link
|
||||
to="/feeds"
|
||||
hitSlop={10}
|
||||
label={_(msg`View your feeds and explore more`)}
|
||||
size="small"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
shape="square"
|
||||
style={[a.justify_center]}>
|
||||
<ButtonIcon icon={FeedsIcon} size="lg" />
|
||||
</Link>
|
||||
</View>
|
||||
|
||||
<Link
|
||||
to="/feeds"
|
||||
hitSlop={10}
|
||||
label={_(msg`View your feeds and explore more`)}
|
||||
size="small"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
shape="square"
|
||||
style={[
|
||||
a.justify_center,
|
||||
{
|
||||
marginTop: -4,
|
||||
},
|
||||
]}>
|
||||
<FeedsIcon size="md" fill={t.atoms.text_contrast_medium.color} />
|
||||
</Link>
|
||||
</View>
|
||||
</Layout.Center>
|
||||
)}
|
||||
{tabBarAnchor}
|
||||
<Animated.View
|
||||
onLayout={e => {
|
||||
headerHeight.set(e.nativeEvent.layout.height)
|
||||
}}
|
||||
style={[
|
||||
t.atoms.bg,
|
||||
t.atoms.border_contrast_low,
|
||||
styles.bar,
|
||||
styles.tabBar,
|
||||
headerMinimalShellTransform,
|
||||
]}>
|
||||
{children}
|
||||
</Animated.View>
|
||||
<Layout.Center
|
||||
style={[a.sticky, a.z_10, a.align_center, t.atoms.bg, {top: 0}]}>
|
||||
<Animated.View
|
||||
onLayout={e => {
|
||||
headerHeight.set(e.nativeEvent.layout.height)
|
||||
}}
|
||||
style={[headerMinimalShellTransform]}>
|
||||
{children}
|
||||
</Animated.View>
|
||||
</Layout.Center>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
bar: {
|
||||
// @ts-ignore Web only
|
||||
left: 'calc(50% - 300px)',
|
||||
width: 600,
|
||||
borderLeftWidth: 1,
|
||||
borderRightWidth: 1,
|
||||
},
|
||||
topBar: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 18,
|
||||
paddingTop: 16,
|
||||
paddingBottom: 8,
|
||||
},
|
||||
tabBar: {
|
||||
// @ts-ignore Web only
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
borderLeftWidth: 1,
|
||||
borderRightWidth: 1,
|
||||
zIndex: 1,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,26 +1,22 @@
|
||||
import React from 'react'
|
||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
import {View} from 'react-native'
|
||||
import Animated from 'react-native-reanimated'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {HITSLOP_10} from '#/lib/constants'
|
||||
import {PressableScale} from '#/lib/custom-animations/PressableScale'
|
||||
import {useHaptics} from '#/lib/haptics'
|
||||
import {useMinimalShellHeaderTransform} from '#/lib/hooks/useMinimalShellTransform'
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {emitSoftReset} from '#/state/events'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useSetDrawerOpen} from '#/state/shell/drawer-open'
|
||||
import {useShellLayout} from '#/state/shell/shell-layout'
|
||||
import {Logo} from '#/view/icons/Logo'
|
||||
import {atoms} from '#/alf'
|
||||
import {useTheme} from '#/alf'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {ColorPalette_Stroke2_Corner0_Rounded as ColorPalette} from '#/components/icons/ColorPalette'
|
||||
import {Gift1_Stroke2_Corner0_Rounded as Gift} from '#/components/icons/Gift1'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {ButtonIcon} from '#/components/Button'
|
||||
import {Hashtag_Stroke2_Corner0_Rounded as FeedsIcon} from '#/components/icons/Hashtag'
|
||||
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {Link} from '#/components/Link'
|
||||
import {IS_DEV} from '#/env'
|
||||
|
||||
export function HomeHeaderLayoutMobile({
|
||||
children,
|
||||
@@ -29,61 +25,50 @@ export function HomeHeaderLayoutMobile({
|
||||
tabBarAnchor: JSX.Element | null | undefined
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const setDrawerOpen = useSetDrawerOpen()
|
||||
const {headerHeight} = useShellLayout()
|
||||
const headerMinimalShellTransform = useMinimalShellHeaderTransform()
|
||||
const {hasSession} = useSession()
|
||||
|
||||
const onPressAvi = React.useCallback(() => {
|
||||
setDrawerOpen(true)
|
||||
}, [setDrawerOpen])
|
||||
const playHaptic = useHaptics()
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[pal.border, styles.tabBar, headerMinimalShellTransform]}
|
||||
style={[
|
||||
a.fixed,
|
||||
a.z_10,
|
||||
t.atoms.bg,
|
||||
{
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
},
|
||||
headerMinimalShellTransform,
|
||||
]}
|
||||
onLayout={e => {
|
||||
headerHeight.set(e.nativeEvent.layout.height)
|
||||
}}>
|
||||
<View style={[pal.view, styles.topBar]}>
|
||||
<View style={[{width: 100}]}>
|
||||
<TouchableOpacity
|
||||
testID="viewHeaderDrawerBtn"
|
||||
onPress={onPressAvi}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Open navigation`)}
|
||||
accessibilityHint={_(
|
||||
msg`Access profile and other navigation links`,
|
||||
)}
|
||||
hitSlop={HITSLOP_10}>
|
||||
<Menu size="lg" fill={t.atoms.text_contrast_medium.color} />
|
||||
</TouchableOpacity>
|
||||
<Layout.Header.Outer noBottomBorder>
|
||||
<Layout.Header.Slot>
|
||||
<Layout.Header.MenuButton />
|
||||
</Layout.Header.Slot>
|
||||
|
||||
<View style={[a.flex_1, a.align_center]}>
|
||||
<PressableScale
|
||||
targetScale={0.9}
|
||||
onPress={() => {
|
||||
emitSoftReset()
|
||||
}}
|
||||
onPressIn={() => {
|
||||
playHaptic('Heavy')
|
||||
}}
|
||||
onPressOut={() => {
|
||||
playHaptic('Light')
|
||||
}}>
|
||||
<Logo width={30} />
|
||||
</PressableScale>
|
||||
</View>
|
||||
<View>
|
||||
<Logo width={30} />
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
atoms.flex_row,
|
||||
atoms.justify_end,
|
||||
atoms.align_center,
|
||||
atoms.gap_md,
|
||||
{width: 100},
|
||||
]}>
|
||||
{IS_DEV && (
|
||||
<>
|
||||
<Link label="Jump to subscriptions" to="/subscriptions">
|
||||
<Gift size="md" />
|
||||
</Link>
|
||||
<Link
|
||||
label="View storybook"
|
||||
to="/sys/debug"
|
||||
testID="storybookBtn">
|
||||
<ColorPalette size="md" />
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Layout.Header.Slot>
|
||||
{hasSession && (
|
||||
<Link
|
||||
testID="viewHeaderHomeFeedPrefsBtn"
|
||||
@@ -97,40 +82,15 @@ export function HomeHeaderLayoutMobile({
|
||||
style={[
|
||||
a.justify_center,
|
||||
{
|
||||
marginTop: 2,
|
||||
marginRight: -6,
|
||||
marginRight: -Layout.BUTTON_VISUAL_ALIGNMENT_OFFSET,
|
||||
},
|
||||
]}>
|
||||
<FeedsIcon size="lg" fill={t.atoms.text_contrast_medium.color} />
|
||||
<ButtonIcon icon={FeedsIcon} size="lg" />
|
||||
</Link>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</Layout.Header.Slot>
|
||||
</Layout.Header.Outer>
|
||||
{children}
|
||||
</Animated.View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
tabBar: {
|
||||
// @ts-ignore web-only
|
||||
position: isWeb ? 'fixed' : 'absolute',
|
||||
zIndex: 1,
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
flexDirection: 'column',
|
||||
},
|
||||
topBar: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 5,
|
||||
width: '100%',
|
||||
minHeight: 46,
|
||||
},
|
||||
title: {
|
||||
fontSize: 21,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -15,8 +15,8 @@ import {
|
||||
} from '@fortawesome/react-native-fontawesome'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {RemoveScrollBar} from 'react-remove-scroll-bar'
|
||||
|
||||
import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {colors, s} from '#/lib/styles'
|
||||
import {useLightbox, useLightboxControls} from '#/state/lightbox'
|
||||
@@ -28,7 +28,6 @@ export function Lightbox() {
|
||||
const {activeLightbox} = useLightbox()
|
||||
const {closeLightbox} = useLightboxControls()
|
||||
const isActive = !!activeLightbox
|
||||
useWebBodyScrollLock(isActive)
|
||||
|
||||
if (!isActive) {
|
||||
return null
|
||||
@@ -37,11 +36,14 @@ export function Lightbox() {
|
||||
const initialIndex = activeLightbox.index
|
||||
const imgs = activeLightbox.images
|
||||
return (
|
||||
<LightboxInner
|
||||
imgs={imgs}
|
||||
initialIndex={initialIndex}
|
||||
onClose={closeLightbox}
|
||||
/>
|
||||
<>
|
||||
<RemoveScrollBar />
|
||||
<LightboxInner
|
||||
imgs={imgs}
|
||||
initialIndex={initialIndex}
|
||||
onClose={closeLightbox}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {s} from '#/lib/styles'
|
||||
import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {MyListsFilter, useMyListsQuery} from '#/state/queries/my-lists'
|
||||
import {EmptyState} from '#/view/com/util/EmptyState'
|
||||
@@ -110,7 +109,7 @@ export function MyLists({
|
||||
) : (
|
||||
<View
|
||||
style={[
|
||||
(index !== 0 || isWeb) && a.border_t,
|
||||
index !== 0 && a.border_t,
|
||||
t.atoms.border_contrast_low,
|
||||
a.px_lg,
|
||||
a.py_lg,
|
||||
@@ -141,8 +140,6 @@ export function MyLists({
|
||||
}
|
||||
contentContainerStyle={[s.contentContainer]}
|
||||
removeClippedSubviews={true}
|
||||
// @ts-ignore our .web version only -prf
|
||||
desktopFixedHeight
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
@@ -160,8 +157,8 @@ export function MyLists({
|
||||
onRefresh={onRefresh}
|
||||
contentContainerStyle={[s.contentContainer]}
|
||||
removeClippedSubviews={true}
|
||||
// @ts-ignore our .web version only -prf
|
||||
desktopFixedHeight
|
||||
sideBorders={false}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native'
|
||||
import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'
|
||||
import {RemoveScrollBar} from 'react-remove-scroll-bar'
|
||||
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import type {Modal as ModalIface} from '#/state/modals'
|
||||
import {useModalControls, useModals} from '#/state/modals'
|
||||
@@ -22,7 +22,6 @@ import * as VerifyEmailModal from './VerifyEmail'
|
||||
|
||||
export function ModalsContainer() {
|
||||
const {isModalActive, activeModals} = useModals()
|
||||
useWebBodyScrollLock(isModalActive)
|
||||
|
||||
if (!isModalActive) {
|
||||
return null
|
||||
@@ -30,6 +29,7 @@ export function ModalsContainer() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<RemoveScrollBar />
|
||||
{activeModals.map((modal, i) => (
|
||||
<Modal key={`modal-${i}`} modal={modal} />
|
||||
))}
|
||||
|
||||
@@ -10,7 +10,6 @@ import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {s} from '#/lib/styles'
|
||||
import {logger} from '#/logger'
|
||||
@@ -22,7 +21,6 @@ import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
|
||||
import {List, ListRef} from '#/view/com/util/List'
|
||||
import {NotificationFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||
import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn'
|
||||
import {CenteredView} from '#/view/com/util/Views'
|
||||
import {FeedItem} from './FeedItem'
|
||||
|
||||
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
|
||||
@@ -46,7 +44,6 @@ export function Feed({
|
||||
|
||||
const [isPTRing, setIsPTRing] = React.useState(false)
|
||||
const pal = usePalette('default')
|
||||
const {isTabletOrMobile} = useWebMediaQueries()
|
||||
|
||||
const {_} = useLingui()
|
||||
const moderationOpts = useModerationOpts()
|
||||
@@ -133,11 +130,7 @@ export function Feed({
|
||||
)
|
||||
} else if (item === LOADING_ITEM) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
pal.border,
|
||||
!isTabletOrMobile && {borderTopWidth: StyleSheet.hairlineWidth},
|
||||
]}>
|
||||
<View style={[pal.border]}>
|
||||
<NotificationFeedLoadingPlaceholder />
|
||||
</View>
|
||||
)
|
||||
@@ -146,11 +139,11 @@ export function Feed({
|
||||
<FeedItem
|
||||
item={item}
|
||||
moderationOpts={moderationOpts!}
|
||||
hideTopBorder={index === 0 && isTabletOrMobile}
|
||||
hideTopBorder={index === 0}
|
||||
/>
|
||||
)
|
||||
},
|
||||
[moderationOpts, isTabletOrMobile, _, onPressRetryLoadMore, pal.border],
|
||||
[moderationOpts, _, onPressRetryLoadMore, pal.border],
|
||||
)
|
||||
|
||||
const FeedFooter = React.useCallback(
|
||||
@@ -168,12 +161,10 @@ export function Feed({
|
||||
return (
|
||||
<View style={s.hContentRegion}>
|
||||
{error && (
|
||||
<CenteredView>
|
||||
<ErrorMessage
|
||||
message={cleanError(error)}
|
||||
onPressTryAgain={onPressTryAgain}
|
||||
/>
|
||||
</CenteredView>
|
||||
<ErrorMessage
|
||||
message={cleanError(error)}
|
||||
onPressTryAgain={onPressTryAgain}
|
||||
/>
|
||||
)}
|
||||
<List
|
||||
testID="notifsFeed"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import * as React from 'react'
|
||||
import {ScrollView, StyleSheet, View} from 'react-native'
|
||||
import {ScrollView, View} from 'react-native'
|
||||
import {useAnimatedRef} from 'react-native-reanimated'
|
||||
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {Pager, PagerRef, RenderTabBarFnProps} from '#/view/com/pager/Pager'
|
||||
import {atoms as a, web} from '#/alf'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {ListMethods} from '../util/List'
|
||||
import {TabBar} from './TabBar'
|
||||
|
||||
@@ -121,30 +121,19 @@ let PagerTabBar = ({
|
||||
onSelect?: (index: number) => void
|
||||
tabBarAnchor?: JSX.Element | null | undefined
|
||||
}): React.ReactNode => {
|
||||
const pal = usePalette('default')
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
return (
|
||||
<>
|
||||
<View
|
||||
style={[
|
||||
!isMobile && styles.headerContainerDesktop,
|
||||
pal.border,
|
||||
!isHeaderReady && styles.loadingHeader,
|
||||
]}>
|
||||
{renderHeader?.()}
|
||||
</View>
|
||||
<Layout.Center>{renderHeader?.()}</Layout.Center>
|
||||
{tabBarAnchor}
|
||||
<View
|
||||
style={[
|
||||
styles.tabBarContainer,
|
||||
isMobile
|
||||
? styles.tabBarContainerMobile
|
||||
: styles.tabBarContainerDesktop,
|
||||
pal.border,
|
||||
<Layout.Center
|
||||
style={web([
|
||||
a.sticky,
|
||||
a.z_10,
|
||||
{
|
||||
top: 0,
|
||||
display: isHeaderReady ? undefined : 'none',
|
||||
},
|
||||
]}>
|
||||
])}>
|
||||
<TabBar
|
||||
testID={testID}
|
||||
items={items}
|
||||
@@ -154,7 +143,7 @@ let PagerTabBar = ({
|
||||
dragProgress={undefined as any /* native-only */}
|
||||
dragState={undefined as any /* native-only */}
|
||||
/>
|
||||
</View>
|
||||
</Layout.Center>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -180,33 +169,6 @@ function PagerItem({
|
||||
})
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
headerContainerDesktop: {
|
||||
marginHorizontal: 'auto',
|
||||
width: 600,
|
||||
borderLeftWidth: 1,
|
||||
borderRightWidth: 1,
|
||||
},
|
||||
tabBarContainer: {
|
||||
// @ts-ignore web-only
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 1,
|
||||
},
|
||||
tabBarContainerDesktop: {
|
||||
marginHorizontal: 'auto',
|
||||
width: 600,
|
||||
borderLeftWidth: 1,
|
||||
borderRightWidth: 1,
|
||||
},
|
||||
tabBarContainerMobile: {
|
||||
paddingHorizontal: 0,
|
||||
},
|
||||
loadingHeader: {
|
||||
borderColor: 'transparent',
|
||||
},
|
||||
})
|
||||
|
||||
function toArray<T>(v: T | T[]): T[] {
|
||||
if (Array.isArray(v)) {
|
||||
return v
|
||||
|
||||
@@ -6,7 +6,6 @@ import {useLingui} from '@lingui/react'
|
||||
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useLikedByQuery} from '#/state/queries/post-liked-by'
|
||||
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
|
||||
import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
|
||||
@@ -18,7 +17,7 @@ function renderItem({item, index}: {item: GetLikes.Like; index: number}) {
|
||||
<ProfileCardWithFollowBtn
|
||||
key={item.actor.did}
|
||||
profile={item.actor}
|
||||
noBorder={index === 0 && !isWeb}
|
||||
noBorder={index === 0}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -88,6 +87,7 @@ export function PostLikedBy({uri}: {uri: string}) {
|
||||
)}
|
||||
errorMessage={cleanError(resolveError || error)}
|
||||
sideBorders={false}
|
||||
topBorder={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -108,7 +108,6 @@ export function PostLikedBy({uri}: {uri: string}) {
|
||||
onRetry={fetchNextPage}
|
||||
/>
|
||||
}
|
||||
// @ts-ignore our .web version only -prf
|
||||
desktopFixedHeight
|
||||
initialNumToRender={initialNumToRender}
|
||||
windowSize={11}
|
||||
|
||||
@@ -11,7 +11,6 @@ import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
||||
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {usePostQuotesQuery} from '#/state/queries/post-quotes'
|
||||
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
|
||||
@@ -30,7 +29,7 @@ function renderItem({
|
||||
}
|
||||
index: number
|
||||
}) {
|
||||
return <Post post={item.post} hideTopBorder={index === 0 && !isWeb} />
|
||||
return <Post post={item.post} hideTopBorder={index === 0} />
|
||||
}
|
||||
|
||||
function keyExtractor(item: {
|
||||
|
||||
@@ -12,8 +12,20 @@ import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
|
||||
import {List} from '#/view/com/util/List'
|
||||
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
|
||||
|
||||
function renderItem({item}: {item: ActorDefs.ProfileViewBasic}) {
|
||||
return <ProfileCardWithFollowBtn key={item.did} profile={item} />
|
||||
function renderItem({
|
||||
item,
|
||||
index,
|
||||
}: {
|
||||
item: ActorDefs.ProfileViewBasic
|
||||
index: number
|
||||
}) {
|
||||
return (
|
||||
<ProfileCardWithFollowBtn
|
||||
key={item.did}
|
||||
profile={item}
|
||||
noBorder={index === 0}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function keyExtractor(item: ActorDefs.ProfileViewBasic) {
|
||||
|
||||
@@ -32,7 +32,6 @@ import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useComposerControls} from '#/state/shell'
|
||||
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
|
||||
import {CenteredView} from '#/view/com/util/Views'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
|
||||
import {Text} from '#/components/Typography'
|
||||
@@ -484,7 +483,7 @@ export function PostThread({uri}: {uri: string | undefined}) {
|
||||
}
|
||||
|
||||
return (
|
||||
<CenteredView style={[a.flex_1]} sideBorders={true}>
|
||||
<>
|
||||
{showHeader && (
|
||||
<ViewHeader
|
||||
title={_(msg({message: `Post`, context: 'description'}))}
|
||||
@@ -531,7 +530,7 @@ export function PostThread({uri}: {uri: string | undefined}) {
|
||||
{isMobile && canReply && hasSession && (
|
||||
<MobileComposePrompt onPressReply={onPressReply} />
|
||||
)}
|
||||
</CenteredView>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import {useLingui} from '@lingui/react'
|
||||
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useProfileFollowersQuery} from '#/state/queries/profile-followers'
|
||||
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
|
||||
import {useSession} from '#/state/session'
|
||||
@@ -25,7 +24,7 @@ function renderItem({
|
||||
<ProfileCardWithFollowBtn
|
||||
key={item.did}
|
||||
profile={item}
|
||||
noBorder={index === 0 && !isWeb}
|
||||
noBorder={index === 0}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import {useLingui} from '@lingui/react'
|
||||
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
|
||||
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
|
||||
import {useSession} from '#/state/session'
|
||||
@@ -25,7 +24,7 @@ function renderItem({
|
||||
<ProfileCardWithFollowBtn
|
||||
key={item.did}
|
||||
profile={item}
|
||||
noBorder={index === 0 && !isWeb}
|
||||
noBorder={index === 0}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,29 +1,24 @@
|
||||
import React from 'react'
|
||||
import {Pressable, StyleSheet, View} from 'react-native'
|
||||
import {Pressable, View} from 'react-native'
|
||||
import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {BACK_HITSLOP} from '#/lib/constants'
|
||||
import {measureHandle, useHandleRef} from '#/lib/hooks/useHandleRef'
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {makeProfileLink} from '#/lib/routes/links'
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {emitSoftReset} from '#/state/events'
|
||||
import {useLightboxControls} from '#/state/lightbox'
|
||||
import {useSetDrawerOpen} from '#/state/shell'
|
||||
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
|
||||
import {TextLink} from '#/view/com/util/Link'
|
||||
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import {UserAvatar, UserAvatarType} from '#/view/com/util/UserAvatar'
|
||||
import {StarterPack} from '#/components/icons/StarterPack'
|
||||
import {TextLink} from '../util/Link'
|
||||
import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {UserAvatar, UserAvatarType} from '../util/UserAvatar'
|
||||
import {CenteredView} from '../util/Views'
|
||||
import * as Layout from '#/components/Layout'
|
||||
|
||||
export function ProfileSubpageHeader({
|
||||
isLoading,
|
||||
@@ -48,7 +43,6 @@ export function ProfileSubpageHeader({
|
||||
| undefined
|
||||
avatarType: UserAvatarType | 'starter-pack'
|
||||
}>) {
|
||||
const setDrawerOpen = useSetDrawerOpen()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {_} = useLingui()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
@@ -57,18 +51,6 @@ export function ProfileSubpageHeader({
|
||||
const canGoBack = navigation.canGoBack()
|
||||
const aviRef = useHandleRef()
|
||||
|
||||
const onPressBack = React.useCallback(() => {
|
||||
if (navigation.canGoBack()) {
|
||||
navigation.goBack()
|
||||
} else {
|
||||
navigation.navigate('Home')
|
||||
}
|
||||
}, [navigation])
|
||||
|
||||
const onPressMenu = React.useCallback(() => {
|
||||
setDrawerOpen(true)
|
||||
}, [setDrawerOpen])
|
||||
|
||||
const _openLightbox = React.useCallback(
|
||||
(uri: string, thumbRect: MeasuredDimensions | null) => {
|
||||
openLightbox({
|
||||
@@ -106,42 +88,17 @@ export function ProfileSubpageHeader({
|
||||
}, [_openLightbox, avatar, aviRef])
|
||||
|
||||
return (
|
||||
<CenteredView style={pal.view}>
|
||||
{isMobile && (
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
paddingTop: isNative ? 0 : 8,
|
||||
paddingBottom: 8,
|
||||
paddingHorizontal: isMobile ? 12 : 14,
|
||||
},
|
||||
pal.border,
|
||||
]}>
|
||||
<Pressable
|
||||
testID="headerDrawerBtn"
|
||||
onPress={canGoBack ? onPressBack : onPressMenu}
|
||||
hitSlop={BACK_HITSLOP}
|
||||
style={canGoBack ? styles.backBtn : styles.backBtnWide}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={canGoBack ? 'Back' : 'Menu'}
|
||||
accessibilityHint="">
|
||||
{canGoBack ? (
|
||||
<FontAwesomeIcon
|
||||
size={18}
|
||||
icon="angle-left"
|
||||
style={[styles.backIcon, pal.text]}
|
||||
/>
|
||||
) : (
|
||||
<Menu size="lg" style={[{marginTop: 4}, pal.textLight]} />
|
||||
)}
|
||||
</Pressable>
|
||||
<View style={{flex: 1}} />
|
||||
{children}
|
||||
</View>
|
||||
)}
|
||||
<>
|
||||
<Layout.Header.Outer>
|
||||
{canGoBack ? (
|
||||
<Layout.Header.BackButton />
|
||||
) : (
|
||||
<Layout.Header.MenuButton />
|
||||
)}
|
||||
<Layout.Header.Content />
|
||||
{children}
|
||||
</Layout.Header.Outer>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
@@ -206,31 +163,7 @@ export function ProfileSubpageHeader({
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
{!isMobile && (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
{children}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</CenteredView>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
backBtn: {
|
||||
width: 20,
|
||||
height: 30,
|
||||
},
|
||||
backBtnWide: {
|
||||
width: 20,
|
||||
height: 30,
|
||||
marginRight: 4,
|
||||
},
|
||||
backIcon: {
|
||||
marginTop: 6,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -4,10 +4,9 @@ import {ReanimatedScrollEvent} from 'react-native-reanimated/lib/typescript/hook
|
||||
|
||||
import {batchedUpdates} from '#/lib/batchedUpdates'
|
||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {useScrollHandlers} from '#/lib/ScrollContext'
|
||||
import {addStyle} from '#/lib/styles'
|
||||
import * as Layout from '#/components/Layout'
|
||||
|
||||
export type ListMethods = any // TODO: Better types.
|
||||
export type ListProps<ItemT> = Omit<
|
||||
@@ -24,6 +23,9 @@ export type ListProps<ItemT> = Omit<
|
||||
desktopFixedHeight?: number | boolean
|
||||
// Web only prop to contain the scroll to the container rather than the window
|
||||
disableFullWindowScroll?: boolean
|
||||
/**
|
||||
* @deprecated Should be using Layout components
|
||||
*/
|
||||
sideBorders?: boolean
|
||||
}
|
||||
export type ListRef = React.MutableRefObject<any | null> // TODO: Better types.
|
||||
@@ -56,20 +58,11 @@ function ListImpl<ItemT>(
|
||||
renderItem,
|
||||
extraData,
|
||||
style,
|
||||
sideBorders = true,
|
||||
...props
|
||||
}: ListProps<ItemT>,
|
||||
ref: React.Ref<ListMethods>,
|
||||
) {
|
||||
const contextScrollHandlers = useScrollHandlers()
|
||||
const pal = usePalette('default')
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
if (!isMobile) {
|
||||
contentContainerStyle = addStyle(
|
||||
contentContainerStyle,
|
||||
styles.containerScroll,
|
||||
)
|
||||
}
|
||||
|
||||
const isEmpty = !data || data.length === 0
|
||||
|
||||
@@ -326,53 +319,53 @@ function ListImpl<ItemT>(
|
||||
styles.parentTreeVisibilityDetector
|
||||
}
|
||||
/>
|
||||
<View
|
||||
ref={containerRef}
|
||||
style={[
|
||||
!isMobile && sideBorders && styles.sideBorders,
|
||||
contentContainerStyle,
|
||||
desktopFixedHeight ? styles.minHeightViewport : null,
|
||||
pal.border,
|
||||
]}>
|
||||
<Visibility
|
||||
root={disableFullWindowScroll ? nativeRef : null}
|
||||
onVisibleChange={handleAboveTheFoldVisibleChange}
|
||||
style={[styles.aboveTheFoldDetector, {height: headerOffset}]}
|
||||
/>
|
||||
{onStartReached && !isEmpty && (
|
||||
<EdgeVisibility
|
||||
<Layout.Center>
|
||||
<View
|
||||
ref={containerRef}
|
||||
style={[
|
||||
contentContainerStyle,
|
||||
desktopFixedHeight ? styles.minHeightViewport : null,
|
||||
]}>
|
||||
<Visibility
|
||||
root={disableFullWindowScroll ? nativeRef : null}
|
||||
onVisibleChange={onHeadVisibilityChange}
|
||||
topMargin={(onStartReachedThreshold ?? 0) * 100 + '%'}
|
||||
containerRef={containerRef}
|
||||
onVisibleChange={handleAboveTheFoldVisibleChange}
|
||||
style={[styles.aboveTheFoldDetector, {height: headerOffset}]}
|
||||
/>
|
||||
)}
|
||||
{headerComponent}
|
||||
{isEmpty
|
||||
? emptyComponent
|
||||
: (data as Array<ItemT>)?.map((item, index) => {
|
||||
const key = keyExtractor!(item, index)
|
||||
return (
|
||||
<Row<ItemT>
|
||||
key={key}
|
||||
item={item}
|
||||
index={index}
|
||||
renderItem={renderItem}
|
||||
extraData={extraData}
|
||||
onItemSeen={onItemSeen}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{onEndReached && !isEmpty && (
|
||||
<EdgeVisibility
|
||||
root={disableFullWindowScroll ? nativeRef : null}
|
||||
onVisibleChange={onTailVisibilityChange}
|
||||
bottomMargin={(onEndReachedThreshold ?? 0) * 100 + '%'}
|
||||
containerRef={containerRef}
|
||||
/>
|
||||
)}
|
||||
{footerComponent}
|
||||
</View>
|
||||
{onStartReached && !isEmpty && (
|
||||
<EdgeVisibility
|
||||
root={disableFullWindowScroll ? nativeRef : null}
|
||||
onVisibleChange={onHeadVisibilityChange}
|
||||
topMargin={(onStartReachedThreshold ?? 0) * 100 + '%'}
|
||||
containerRef={containerRef}
|
||||
/>
|
||||
)}
|
||||
{headerComponent}
|
||||
{isEmpty
|
||||
? emptyComponent
|
||||
: (data as Array<ItemT>)?.map((item, index) => {
|
||||
const key = keyExtractor!(item, index)
|
||||
return (
|
||||
<Row<ItemT>
|
||||
key={key}
|
||||
item={item}
|
||||
index={index}
|
||||
renderItem={renderItem}
|
||||
extraData={extraData}
|
||||
onItemSeen={onItemSeen}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{onEndReached && !isEmpty && (
|
||||
<EdgeVisibility
|
||||
root={disableFullWindowScroll ? nativeRef : null}
|
||||
onVisibleChange={onTailVisibilityChange}
|
||||
bottomMargin={(onEndReachedThreshold ?? 0) * 100 + '%'}
|
||||
containerRef={containerRef}
|
||||
/>
|
||||
)}
|
||||
{footerComponent}
|
||||
</View>
|
||||
</Layout.Center>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -558,16 +551,6 @@ export const List = memo(React.forwardRef(ListImpl)) as <ItemT>(
|
||||
// https://stackoverflow.com/questions/7944460/detect-safari-browser
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
sideBorders: {
|
||||
borderLeftWidth: 1,
|
||||
borderRightWidth: 1,
|
||||
},
|
||||
containerScroll: {
|
||||
width: '100%',
|
||||
maxWidth: 600,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
},
|
||||
minHeightViewport: {
|
||||
// @ts-ignore web only
|
||||
minHeight: '100vh',
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import {ActivityIndicator, View} from 'react-native'
|
||||
|
||||
import {s} from '#/lib/styles'
|
||||
import {CenteredView} from './Views'
|
||||
import * as Layout from '#/components/Layout'
|
||||
|
||||
/**
|
||||
* @deprecated use Layout compoenents directly
|
||||
*/
|
||||
export function LoadingScreen() {
|
||||
return (
|
||||
<CenteredView>
|
||||
<Layout.Content>
|
||||
<View style={s.p20}>
|
||||
<ActivityIndicator size="large" />
|
||||
</View>
|
||||
</CenteredView>
|
||||
</Layout.Content>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
StyleProp,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
ViewStyle,
|
||||
} 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 {NavigationProp} from '#/lib/routes/types'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useSetDrawerOpen} from '#/state/shell'
|
||||
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
|
||||
import {CenteredView} from './Views'
|
||||
|
||||
const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20}
|
||||
|
||||
export function SimpleViewHeader({
|
||||
showBackButton = true,
|
||||
style,
|
||||
children,
|
||||
}: React.PropsWithChildren<{
|
||||
showBackButton?: boolean
|
||||
style?: StyleProp<ViewStyle>
|
||||
}>) {
|
||||
const pal = usePalette('default')
|
||||
const setDrawerOpen = useSetDrawerOpen()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const canGoBack = navigation.canGoBack()
|
||||
|
||||
const onPressBack = React.useCallback(() => {
|
||||
if (navigation.canGoBack()) {
|
||||
navigation.goBack()
|
||||
} else {
|
||||
navigation.navigate('Home')
|
||||
}
|
||||
}, [navigation])
|
||||
|
||||
const onPressMenu = React.useCallback(() => {
|
||||
setDrawerOpen(true)
|
||||
}, [setDrawerOpen])
|
||||
|
||||
const Container = isMobile ? View : CenteredView
|
||||
return (
|
||||
<Container
|
||||
style={[
|
||||
styles.header,
|
||||
isMobile && styles.headerMobile,
|
||||
isWeb && styles.headerWeb,
|
||||
pal.view,
|
||||
style,
|
||||
]}>
|
||||
{showBackButton ? (
|
||||
<TouchableOpacity
|
||||
testID="viewHeaderDrawerBtn"
|
||||
onPress={canGoBack ? onPressBack : onPressMenu}
|
||||
hitSlop={BACK_HITSLOP}
|
||||
style={canGoBack ? styles.backBtn : styles.backBtnWide}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={canGoBack ? 'Back' : 'Menu'}
|
||||
accessibilityHint="">
|
||||
{canGoBack ? (
|
||||
<FontAwesomeIcon
|
||||
size={18}
|
||||
icon="angle-left"
|
||||
style={[styles.backIcon, pal.text]}
|
||||
/>
|
||||
) : (
|
||||
<Menu size="lg" style={[{marginTop: 4}, pal.textLight]} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
{children}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 18,
|
||||
paddingVertical: 12,
|
||||
width: '100%',
|
||||
},
|
||||
headerMobile: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
headerWeb: {
|
||||
// @ts-ignore web-only
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 1,
|
||||
},
|
||||
backBtn: {
|
||||
width: 30,
|
||||
height: 30,
|
||||
},
|
||||
backBtnWide: {
|
||||
width: 30,
|
||||
height: 30,
|
||||
paddingLeft: 4,
|
||||
marginRight: 4,
|
||||
},
|
||||
backIcon: {
|
||||
marginTop: 6,
|
||||
},
|
||||
})
|
||||
@@ -1,271 +1,27 @@
|
||||
import React from 'react'
|
||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
import Animated from 'react-native-reanimated'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {useMinimalShellHeaderTransform} from '#/lib/hooks/useMinimalShellTransform'
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
import {useSetDrawerOpen} from '#/state/shell'
|
||||
import {useTheme} from '#/alf'
|
||||
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
|
||||
import {Text} from './text/Text'
|
||||
import {CenteredView} from './Views'
|
||||
|
||||
const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20}
|
||||
import {Header} from '#/components/Layout'
|
||||
|
||||
/**
|
||||
* Legacy ViewHeader component. Use Layout.Header going forward.
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
export function ViewHeader({
|
||||
title,
|
||||
subtitle,
|
||||
canGoBack,
|
||||
showBackButton = true,
|
||||
hideOnScroll,
|
||||
showOnDesktop,
|
||||
showBorder,
|
||||
renderButton,
|
||||
}: {
|
||||
title: string
|
||||
subtitle?: string
|
||||
canGoBack?: boolean
|
||||
showBackButton?: boolean
|
||||
hideOnScroll?: boolean
|
||||
showOnDesktop?: boolean
|
||||
showBorder?: boolean
|
||||
renderButton?: () => JSX.Element
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const setDrawerOpen = useSetDrawerOpen()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {isDesktop, isTablet} = useWebMediaQueries()
|
||||
const t = useTheme()
|
||||
|
||||
const onPressBack = React.useCallback(() => {
|
||||
if (navigation.canGoBack()) {
|
||||
navigation.goBack()
|
||||
} else {
|
||||
navigation.navigate('Home')
|
||||
}
|
||||
}, [navigation])
|
||||
|
||||
const onPressMenu = React.useCallback(() => {
|
||||
setDrawerOpen(true)
|
||||
}, [setDrawerOpen])
|
||||
|
||||
if (isDesktop) {
|
||||
if (showOnDesktop) {
|
||||
return (
|
||||
<DesktopWebHeader
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
renderButton={renderButton}
|
||||
showBorder={showBorder}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return null
|
||||
} else {
|
||||
if (typeof canGoBack === 'undefined') {
|
||||
canGoBack = navigation.canGoBack()
|
||||
}
|
||||
|
||||
return (
|
||||
<Container hideOnScroll={hideOnScroll || false} showBorder={showBorder}>
|
||||
<View style={{flex: 1}}>
|
||||
<View style={{flexDirection: 'row', alignItems: 'center'}}>
|
||||
{showBackButton ? (
|
||||
<TouchableOpacity
|
||||
testID="viewHeaderDrawerBtn"
|
||||
onPress={canGoBack ? onPressBack : onPressMenu}
|
||||
hitSlop={BACK_HITSLOP}
|
||||
style={canGoBack ? styles.backBtn : styles.backBtnWide}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={canGoBack ? _(msg`Back`) : _(msg`Menu`)}
|
||||
accessibilityHint={
|
||||
canGoBack ? '' : _(msg`Access navigation links and settings`)
|
||||
}>
|
||||
{canGoBack ? (
|
||||
<FontAwesomeIcon
|
||||
size={18}
|
||||
icon="angle-left"
|
||||
style={[styles.backIcon, pal.text]}
|
||||
/>
|
||||
) : !isTablet ? (
|
||||
<Menu size="lg" style={[{marginTop: 3}, pal.textLight]} />
|
||||
) : null}
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
<View style={styles.titleContainer} pointerEvents="none">
|
||||
<Text emoji type="title" style={[pal.text, styles.title]}>
|
||||
{title}
|
||||
</Text>
|
||||
</View>
|
||||
{renderButton ? (
|
||||
renderButton()
|
||||
) : showBackButton ? (
|
||||
<View style={canGoBack ? styles.backBtn : styles.backBtnWide} />
|
||||
) : null}
|
||||
</View>
|
||||
{subtitle ? (
|
||||
<View
|
||||
style={[styles.titleContainer, {marginTop: -3}]}
|
||||
pointerEvents="none">
|
||||
<Text
|
||||
style={[
|
||||
pal.text,
|
||||
styles.subtitle,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
{subtitle}
|
||||
</Text>
|
||||
</View>
|
||||
) : undefined}
|
||||
</View>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function DesktopWebHeader({
|
||||
title,
|
||||
subtitle,
|
||||
renderButton,
|
||||
showBorder = true,
|
||||
}: {
|
||||
title: string
|
||||
subtitle?: string
|
||||
renderButton?: () => JSX.Element
|
||||
showBorder?: boolean
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const t = useTheme()
|
||||
return (
|
||||
<CenteredView
|
||||
style={[
|
||||
styles.header,
|
||||
styles.desktopHeader,
|
||||
pal.border,
|
||||
{
|
||||
borderBottomWidth: showBorder ? StyleSheet.hairlineWidth : 0,
|
||||
},
|
||||
{display: 'flex', flexDirection: 'column'},
|
||||
]}>
|
||||
<View>
|
||||
<View style={styles.titleContainer} pointerEvents="none">
|
||||
<Text type="title-lg" style={[pal.text, styles.title]}>
|
||||
{title}
|
||||
</Text>
|
||||
</View>
|
||||
{renderButton?.()}
|
||||
</View>
|
||||
{subtitle ? (
|
||||
<View>
|
||||
<View style={[styles.titleContainer]} pointerEvents="none">
|
||||
<Text
|
||||
style={[
|
||||
pal.text,
|
||||
styles.subtitleDesktop,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
{subtitle}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
) : null}
|
||||
</CenteredView>
|
||||
<Header.Outer>
|
||||
<Header.BackButton />
|
||||
<Header.Content>
|
||||
<Header.TitleText>{title}</Header.TitleText>
|
||||
</Header.Content>
|
||||
<Header.Slot>{renderButton?.() ?? null}</Header.Slot>
|
||||
</Header.Outer>
|
||||
)
|
||||
}
|
||||
|
||||
function Container({
|
||||
children,
|
||||
hideOnScroll,
|
||||
showBorder,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
hideOnScroll: boolean
|
||||
showBorder?: boolean
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const headerMinimalShellTransform = useMinimalShellHeaderTransform()
|
||||
|
||||
if (!hideOnScroll) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.header,
|
||||
pal.view,
|
||||
pal.border,
|
||||
showBorder && styles.border,
|
||||
]}>
|
||||
{children}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.header,
|
||||
styles.headerFloating,
|
||||
pal.view,
|
||||
pal.border,
|
||||
headerMinimalShellTransform,
|
||||
showBorder && styles.border,
|
||||
]}>
|
||||
{children}
|
||||
</Animated.View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
width: '100%',
|
||||
},
|
||||
headerFloating: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
width: '100%',
|
||||
},
|
||||
desktopHeader: {
|
||||
paddingVertical: 12,
|
||||
maxWidth: 600,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
},
|
||||
border: {
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
titleContainer: {
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
alignItems: 'center',
|
||||
},
|
||||
title: {
|
||||
fontWeight: '600',
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 13,
|
||||
},
|
||||
subtitleDesktop: {
|
||||
fontSize: 15,
|
||||
},
|
||||
backBtn: {
|
||||
width: 30,
|
||||
height: 30,
|
||||
},
|
||||
backBtnWide: {
|
||||
width: 30,
|
||||
height: 30,
|
||||
paddingLeft: 4,
|
||||
marginRight: 4,
|
||||
},
|
||||
backIcon: {
|
||||
marginTop: 6,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -15,9 +15,16 @@ export type FlatList_INTERNAL<ItemT = any> = Omit<
|
||||
FlatListComponent<ItemT, FlatListPropsWithLayout<ItemT>>,
|
||||
'CellRendererComponent'
|
||||
>
|
||||
|
||||
/**
|
||||
* @deprecated use `Layout` components
|
||||
*/
|
||||
export const ScrollView = Animated.ScrollView
|
||||
export type ScrollView = typeof Animated.ScrollView
|
||||
|
||||
/**
|
||||
* @deprecated use `Layout` components
|
||||
*/
|
||||
export const CenteredView = forwardRef<
|
||||
View,
|
||||
React.PropsWithChildren<
|
||||
|
||||
@@ -31,10 +31,12 @@ interface AddedProps {
|
||||
desktopFixedHeight?: boolean | number
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use `Layout` components
|
||||
*/
|
||||
export const CenteredView = React.forwardRef(function CenteredView(
|
||||
{
|
||||
style,
|
||||
sideBorders,
|
||||
topBorder,
|
||||
...props
|
||||
}: React.PropsWithChildren<
|
||||
@@ -47,13 +49,6 @@ export const CenteredView = React.forwardRef(function CenteredView(
|
||||
if (!isMobile) {
|
||||
style = addStyle(style, styles.container)
|
||||
}
|
||||
if (sideBorders && !isMobile) {
|
||||
style = addStyle(style, {
|
||||
borderLeftWidth: StyleSheet.hairlineWidth,
|
||||
borderRightWidth: StyleSheet.hairlineWidth,
|
||||
})
|
||||
style = addStyle(style, pal.border)
|
||||
}
|
||||
if (topBorder) {
|
||||
style = addStyle(style, {
|
||||
borderTopWidth: 1,
|
||||
@@ -75,7 +70,6 @@ export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl<ItemT>(
|
||||
>,
|
||||
ref: React.Ref<FlatList<ItemT>>,
|
||||
) {
|
||||
const pal = usePalette('default')
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
if (!isMobile) {
|
||||
contentContainerStyle = addStyle(
|
||||
@@ -123,11 +117,7 @@ export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl<ItemT>(
|
||||
return (
|
||||
<Animated.FlatList
|
||||
ref={ref}
|
||||
contentContainerStyle={[
|
||||
styles.contentContainer,
|
||||
contentContainerStyle,
|
||||
pal.border,
|
||||
]}
|
||||
contentContainerStyle={[styles.contentContainer, contentContainerStyle]}
|
||||
style={style}
|
||||
contentOffset={contentOffset}
|
||||
{...props}
|
||||
@@ -135,12 +125,13 @@ export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl<ItemT>(
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* @deprecated use `Layout` components
|
||||
*/
|
||||
export const ScrollView = React.forwardRef(function ScrollViewImpl(
|
||||
{contentContainerStyle, ...props}: React.PropsWithChildren<ScrollViewProps>,
|
||||
ref: React.Ref<Animated.ScrollView>,
|
||||
) {
|
||||
const pal = usePalette('default')
|
||||
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
if (!isMobile) {
|
||||
contentContainerStyle = addStyle(
|
||||
@@ -150,11 +141,7 @@ export const ScrollView = React.forwardRef(function ScrollViewImpl(
|
||||
}
|
||||
return (
|
||||
<Animated.ScrollView
|
||||
contentContainerStyle={[
|
||||
styles.contentContainer,
|
||||
contentContainerStyle,
|
||||
pal.border,
|
||||
]}
|
||||
contentContainerStyle={[styles.contentContainer, contentContainerStyle]}
|
||||
// @ts-ignore something is wrong with the reanimated types -prf
|
||||
ref={ref}
|
||||
{...props}
|
||||
@@ -164,8 +151,6 @@ export const ScrollView = React.forwardRef(function ScrollViewImpl(
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
contentContainer: {
|
||||
borderLeftWidth: StyleSheet.hairlineWidth,
|
||||
borderRightWidth: StyleSheet.hairlineWidth,
|
||||
// @ts-ignore web only
|
||||
minHeight: '100vh',
|
||||
},
|
||||
|
||||
@@ -36,7 +36,9 @@ export function ErrorScreen({
|
||||
|
||||
return (
|
||||
<>
|
||||
{showHeader && isMobile && <ViewHeader title="Error" showBorder />}
|
||||
{showHeader && isMobile && (
|
||||
<ViewHeader title={_(msg`Error`)} showBorder />
|
||||
)}
|
||||
<CenteredView testID={testID} style={[styles.outer, pal.view]}>
|
||||
<View style={styles.errorIconContainer}>
|
||||
<View
|
||||
|
||||
@@ -31,6 +31,7 @@ export type ButtonType =
|
||||
// Augment type for react-native-web (see https://github.com/necolas/react-native-web/issues/1684#issuecomment-766451866)
|
||||
declare module 'react-native' {
|
||||
interface PressableStateCallbackType {
|
||||
// @ts-ignore web only
|
||||
hovered?: boolean
|
||||
focused?: boolean
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ export function DropdownButton({
|
||||
}: PropsWithChildren<DropdownButtonProps>) {
|
||||
const {_} = useLingui()
|
||||
|
||||
const ref1 = useRef<TouchableOpacity>(null)
|
||||
const ref1 = useRef<View>(null)
|
||||
const ref2 = useRef<View>(null)
|
||||
|
||||
const onPress = (e: GestureResponderEvent) => {
|
||||
|
||||
@@ -229,6 +229,7 @@ const getKey = (label: string, index: number, id?: string) => {
|
||||
return `${label}_${index}`
|
||||
}
|
||||
|
||||
// @ts-expect-error - web only styles. the only style that should be broken here is `outline`
|
||||
const styles = StyleSheet.create({
|
||||
separator: {
|
||||
height: 1,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react'
|
||||
|
||||
import {isSafari} from '#/lib/browser'
|
||||
import {useVideoVolumeState} from '../../VideoVolumeContext'
|
||||
|
||||
export function useVideoElement(ref: React.RefObject<HTMLVideoElement>) {
|
||||
@@ -37,6 +38,12 @@ export function useVideoElement(ref: React.RefObject<HTMLVideoElement>) {
|
||||
const handleTimeUpdate = () => {
|
||||
if (!ref.current) return
|
||||
setCurrentTime(round(ref.current.currentTime) || 0)
|
||||
// HACK: Safari randomly fires `stalled` events when changing between segments
|
||||
// let's just clear the buffering state if the video is still progressing -sfn
|
||||
if (isSafari) {
|
||||
if (bufferingTimeout) clearTimeout(bufferingTimeout)
|
||||
setBuffering(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDurationChange = () => {
|
||||
|
||||
+50
-86
@@ -24,14 +24,13 @@ import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {useComposerControls} from '#/state/shell/composer'
|
||||
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
|
||||
import {FAB} from '#/view/com/util/fab/FAB'
|
||||
import {TextLink} from '#/view/com/util/Link'
|
||||
import {List, ListMethods} from '#/view/com/util/List'
|
||||
import {FeedFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import {ViewHeader} from '#/view/com/util/ViewHeader'
|
||||
import {NoFollowingFeed} from '#/screens/Feeds/NoFollowingFeed'
|
||||
import {NoSavedFeedsOfAnyType} from '#/screens/Feeds/NoSavedFeedsOfAnyType'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {ButtonIcon} from '#/components/Button'
|
||||
import {Divider} from '#/components/Divider'
|
||||
import * as FeedCard from '#/components/FeedCard'
|
||||
import {SearchInput} from '#/components/forms/SearchInput'
|
||||
@@ -40,7 +39,9 @@ import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components
|
||||
import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline'
|
||||
import {ListMagnifyingGlass_Stroke2_Corner0_Rounded} from '#/components/icons/ListMagnifyingGlass'
|
||||
import {ListSparkle_Stroke2_Corner0_Rounded} from '#/components/icons/ListSparkle'
|
||||
import {SettingsGear2_Stroke2_Corner0_Rounded as Gear} from '#/components/icons/SettingsGear2'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {Link} from '#/components/Link'
|
||||
import * as ListCard from '#/components/ListCard'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Feeds'>
|
||||
@@ -102,7 +103,7 @@ type FlatlistSlice =
|
||||
export function FeedsScreen(_props: Props) {
|
||||
const pal = usePalette('default')
|
||||
const {openComposer} = useComposerControls()
|
||||
const {isMobile, isTabletOrDesktop} = useWebMediaQueries()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const [query, setQuery] = React.useState('')
|
||||
const [isPTR, setIsPTR] = React.useState(false)
|
||||
const {
|
||||
@@ -374,22 +375,6 @@ export function FeedsScreen(_props: Props) {
|
||||
isUserSearching,
|
||||
])
|
||||
|
||||
const renderHeaderBtn = React.useCallback(() => {
|
||||
return (
|
||||
<View style={styles.headerBtnGroup}>
|
||||
<TextLink
|
||||
testID="editFeedsBtn"
|
||||
type="lg-medium"
|
||||
href="/settings/saved-feeds"
|
||||
accessibilityLabel={_(msg`Edit My Feeds`)}
|
||||
accessibilityHint=""
|
||||
text={_(msg`Edit`)}
|
||||
style={[pal.link, a.pr_xs]}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}, [pal, _])
|
||||
|
||||
const searchBarIndex = items.findIndex(
|
||||
item => item.type === 'popularFeedsHeader',
|
||||
)
|
||||
@@ -430,36 +415,7 @@ export function FeedsScreen(_props: Props) {
|
||||
</View>
|
||||
)
|
||||
} else if (item.type === 'savedFeedsHeader') {
|
||||
return (
|
||||
<>
|
||||
{!isMobile && (
|
||||
<View
|
||||
style={[
|
||||
pal.view,
|
||||
styles.header,
|
||||
pal.border,
|
||||
{
|
||||
borderBottomWidth: 1,
|
||||
},
|
||||
]}>
|
||||
<Text type="title-lg" style={[pal.text, s.bold]}>
|
||||
<Trans>Feeds</Trans>
|
||||
</Text>
|
||||
<View style={styles.headerBtnGroup}>
|
||||
<TextLink
|
||||
type="lg"
|
||||
href="/settings/saved-feeds"
|
||||
accessibilityLabel={_(msg`Edit My Feeds`)}
|
||||
accessibilityHint=""
|
||||
text={_(msg`Edit`)}
|
||||
style={[pal.link]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
<FeedsSavedHeader />
|
||||
</>
|
||||
)
|
||||
return <FeedsSavedHeader />
|
||||
} else if (item.type === 'savedFeedNoResults') {
|
||||
return (
|
||||
<View
|
||||
@@ -482,6 +438,7 @@ export function FeedsScreen(_props: Props) {
|
||||
<FeedsAboutHeader />
|
||||
<View style={{paddingHorizontal: 12, paddingBottom: 4}}>
|
||||
<SearchInput
|
||||
placeholder={_(msg`Search feeds`)}
|
||||
value={query}
|
||||
onChangeText={onChangeQuery}
|
||||
onClearText={onPressCancelSearch}
|
||||
@@ -530,13 +487,9 @@ export function FeedsScreen(_props: Props) {
|
||||
return null
|
||||
},
|
||||
[
|
||||
isMobile,
|
||||
pal.view,
|
||||
pal.border,
|
||||
pal.text,
|
||||
pal.textLight,
|
||||
pal.link,
|
||||
_,
|
||||
pal.border,
|
||||
pal.textLight,
|
||||
query,
|
||||
onChangeQuery,
|
||||
onPressCancelSearch,
|
||||
@@ -547,31 +500,45 @@ export function FeedsScreen(_props: Props) {
|
||||
|
||||
return (
|
||||
<Layout.Screen testID="FeedsScreen">
|
||||
{isMobile && (
|
||||
<ViewHeader
|
||||
title={_(msg`Feeds`)}
|
||||
renderButton={renderHeaderBtn}
|
||||
showBorder
|
||||
/>
|
||||
)}
|
||||
<Layout.Center>
|
||||
<Layout.Header.Outer>
|
||||
<Layout.Header.BackButton />
|
||||
<Layout.Header.Content>
|
||||
<Layout.Header.TitleText>
|
||||
<Trans>Feeds</Trans>
|
||||
</Layout.Header.TitleText>
|
||||
</Layout.Header.Content>
|
||||
<Layout.Header.Slot>
|
||||
<Link
|
||||
testID="editFeedsBtn"
|
||||
to="/settings/saved-feeds"
|
||||
label={_(msg`Edit My Feeds`)}
|
||||
size="small"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
shape="round"
|
||||
style={[a.justify_center, {right: -3}]}>
|
||||
<ButtonIcon icon={Gear} size="lg" />
|
||||
</Link>
|
||||
</Layout.Header.Slot>
|
||||
</Layout.Header.Outer>
|
||||
|
||||
<List
|
||||
ref={listRef}
|
||||
style={[!isTabletOrDesktop && s.flex1, styles.list]}
|
||||
data={items}
|
||||
keyExtractor={item => item.key}
|
||||
contentContainerStyle={styles.contentContainer}
|
||||
renderItem={renderItem}
|
||||
refreshing={isPTR}
|
||||
onRefresh={isUserSearching ? undefined : onPullToRefresh}
|
||||
initialNumToRender={10}
|
||||
onEndReached={onEndReached}
|
||||
// @ts-ignore our .web version only -prf
|
||||
desktopFixedHeight
|
||||
scrollIndicatorInsets={{right: 1}}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
keyboardDismissMode="on-drag"
|
||||
/>
|
||||
<List
|
||||
ref={listRef}
|
||||
data={items}
|
||||
keyExtractor={item => item.key}
|
||||
contentContainerStyle={styles.contentContainer}
|
||||
renderItem={renderItem}
|
||||
refreshing={isPTR}
|
||||
onRefresh={isUserSearching ? undefined : onPullToRefresh}
|
||||
initialNumToRender={10}
|
||||
onEndReached={onEndReached}
|
||||
desktopFixedHeight
|
||||
keyboardShouldPersistTaps="handled"
|
||||
keyboardDismissMode="on-drag"
|
||||
sideBorders={false}
|
||||
/>
|
||||
</Layout.Center>
|
||||
|
||||
{hasSession && (
|
||||
<FAB
|
||||
@@ -728,7 +695,7 @@ function FeedsSavedHeader() {
|
||||
}>
|
||||
<IconCircle icon={ListSparkle_Stroke2_Corner0_Rounded} size="lg" />
|
||||
<View style={[a.flex_1, a.gap_xs]}>
|
||||
<Text style={[a.flex_1, a.text_2xl, a.font_bold, t.atoms.text]}>
|
||||
<Text style={[a.flex_1, a.text_2xl, a.font_heavy, t.atoms.text]}>
|
||||
<Trans>My Feeds</Trans>
|
||||
</Text>
|
||||
<Text style={[t.atoms.text_contrast_high]}>
|
||||
@@ -754,13 +721,13 @@ function FeedsAboutHeader() {
|
||||
size="lg"
|
||||
/>
|
||||
<View style={[a.flex_1, a.gap_sm]}>
|
||||
<Text style={[a.flex_1, a.text_2xl, a.font_bold, t.atoms.text]}>
|
||||
<Text style={[a.flex_1, a.text_2xl, a.font_heavy, t.atoms.text]}>
|
||||
<Trans>Discover New Feeds</Trans>
|
||||
</Text>
|
||||
<Text style={[t.atoms.text_contrast_high]}>
|
||||
<Trans>
|
||||
Custom feeds built by the community bring you new experiences and
|
||||
help you find the content you love.
|
||||
Choose your own timeline! Feeds built by the community help you find
|
||||
content you love.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
@@ -769,9 +736,6 @@ function FeedsAboutHeader() {
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
list: {
|
||||
height: '100%',
|
||||
},
|
||||
contentContainer: {
|
||||
paddingBottom: 100,
|
||||
},
|
||||
|
||||
+26
-46
@@ -1,33 +1,26 @@
|
||||
import React from 'react'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import {AtUri} from '@atproto/api'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useFocusEffect, useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {useEmail} from '#/lib/hooks/useEmail'
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
import {s} from '#/lib/styles'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {MyLists} from '#/view/com/lists/MyLists'
|
||||
import {Button} from '#/view/com/util/forms/Button'
|
||||
import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog'
|
||||
import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
|
||||
import * as Layout from '#/components/Layout'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Lists'>
|
||||
export function ListsScreen({}: Props) {
|
||||
const {_} = useLingui()
|
||||
const pal = usePalette('default')
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {openModal} = useModalControls()
|
||||
const {needsEmailVerification} = useEmail()
|
||||
@@ -62,43 +55,30 @@ export function ListsScreen({}: Props) {
|
||||
|
||||
return (
|
||||
<Layout.Screen testID="listsScreen">
|
||||
<SimpleViewHeader
|
||||
showBackButton={isMobile}
|
||||
style={[
|
||||
pal.border,
|
||||
isMobile
|
||||
? {borderBottomWidth: StyleSheet.hairlineWidth}
|
||||
: {
|
||||
borderLeftWidth: StyleSheet.hairlineWidth,
|
||||
borderRightWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
]}>
|
||||
<View style={{flex: 1}}>
|
||||
<Text type="title-lg" style={[pal.text, {fontWeight: '600'}]}>
|
||||
<Trans>User Lists</Trans>
|
||||
</Text>
|
||||
<Text style={pal.textLight}>
|
||||
<Layout.Header.Outer>
|
||||
<Layout.Header.BackButton />
|
||||
<Layout.Header.Content align="left">
|
||||
<Layout.Header.TitleText>
|
||||
<Trans>Lists</Trans>
|
||||
</Layout.Header.TitleText>
|
||||
<Layout.Header.SubtitleText>
|
||||
<Trans>Public, shareable lists which can drive feeds.</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[{marginLeft: 18}, isMobile && {marginLeft: 12}]}>
|
||||
<Button
|
||||
testID="newUserListBtn"
|
||||
type="default"
|
||||
onPress={onPressNewList}
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
}}>
|
||||
<FontAwesomeIcon icon="plus" color={pal.colors.text} />
|
||||
<Text type="button" style={pal.text}>
|
||||
<Trans context="action">New</Trans>
|
||||
</Text>
|
||||
</Button>
|
||||
</View>
|
||||
</SimpleViewHeader>
|
||||
<MyLists filter="curate" style={s.flexGrow1} />
|
||||
</Layout.Header.SubtitleText>
|
||||
</Layout.Header.Content>
|
||||
<Button
|
||||
label={_(msg`New list`)}
|
||||
testID="newUserListBtn"
|
||||
color="secondary"
|
||||
variant="solid"
|
||||
size="small"
|
||||
onPress={onPressNewList}>
|
||||
<ButtonIcon icon={PlusIcon} />
|
||||
<ButtonText>
|
||||
<Trans context="action">New</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</Layout.Header.Outer>
|
||||
<MyLists filter="curate" style={a.flex_grow} />
|
||||
<VerifyEmailDialog
|
||||
reasonText={_(
|
||||
msg`Before creating a list, you must first verify your email.`,
|
||||
|
||||
@@ -23,7 +23,7 @@ import {ProfileCard} from '#/view/com/profile/ProfileCard'
|
||||
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import {ViewHeader} from '#/view/com/util/ViewHeader'
|
||||
import {CenteredView} from '#/view/com/util/Views'
|
||||
import {atoms as a} from '#/alf'
|
||||
import * as Layout from '#/components/Layout'
|
||||
|
||||
type Props = NativeStackScreenProps<
|
||||
@@ -97,14 +97,7 @@ export function ModerationBlockedAccounts({}: Props) {
|
||||
)
|
||||
return (
|
||||
<Layout.Screen testID="blockedAccountsScreen">
|
||||
<CenteredView
|
||||
style={[
|
||||
styles.container,
|
||||
isTabletOrDesktop && styles.containerDesktop,
|
||||
pal.view,
|
||||
pal.border,
|
||||
]}
|
||||
testID="blockedAccountsScreen">
|
||||
<Layout.Center style={[a.flex_1, {paddingBottom: 100}]}>
|
||||
<ViewHeader title={_(msg`Blocked Accounts`)} showOnDesktop />
|
||||
<Text
|
||||
type="sm"
|
||||
@@ -112,6 +105,9 @@ export function ModerationBlockedAccounts({}: Props) {
|
||||
styles.description,
|
||||
pal.text,
|
||||
isTabletOrDesktop && styles.descriptionDesktop,
|
||||
{
|
||||
marginTop: 20,
|
||||
},
|
||||
]}>
|
||||
<Trans>
|
||||
Blocked accounts cannot reply in your threads, mention you, or
|
||||
@@ -120,7 +116,7 @@ export function ModerationBlockedAccounts({}: Props) {
|
||||
</Trans>
|
||||
</Text>
|
||||
{isEmpty ? (
|
||||
<View style={[pal.border, !isTabletOrDesktop && styles.flex1]}>
|
||||
<View style={[pal.border]}>
|
||||
{isError ? (
|
||||
<ErrorScreen
|
||||
title="Oops!"
|
||||
@@ -166,21 +162,12 @@ export function ModerationBlockedAccounts({}: Props) {
|
||||
desktopFixedHeight
|
||||
/>
|
||||
)}
|
||||
</CenteredView>
|
||||
</Layout.Center>
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingBottom: 100,
|
||||
},
|
||||
containerDesktop: {
|
||||
borderLeftWidth: 1,
|
||||
borderRightWidth: 1,
|
||||
paddingBottom: 0,
|
||||
},
|
||||
title: {
|
||||
textAlign: 'center',
|
||||
marginTop: 12,
|
||||
|
||||
@@ -1,33 +1,26 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {AtUri} from '@atproto/api'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useFocusEffect, useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {useEmail} from '#/lib/hooks/useEmail'
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
import {s} from '#/lib/styles'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {MyLists} from '#/view/com/lists/MyLists'
|
||||
import {Button} from '#/view/com/util/forms/Button'
|
||||
import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog'
|
||||
import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
|
||||
import * as Layout from '#/components/Layout'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ModerationModlists'>
|
||||
export function ModerationModlistsScreen({}: Props) {
|
||||
const {_} = useLingui()
|
||||
const pal = usePalette('default')
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {openModal} = useModalControls()
|
||||
const {needsEmailVerification} = useEmail()
|
||||
@@ -62,39 +55,32 @@ export function ModerationModlistsScreen({}: Props) {
|
||||
|
||||
return (
|
||||
<Layout.Screen testID="moderationModlistsScreen">
|
||||
<SimpleViewHeader
|
||||
showBackButton={isMobile}
|
||||
style={
|
||||
!isMobile && [pal.border, {borderLeftWidth: 1, borderRightWidth: 1}]
|
||||
}>
|
||||
<View style={{flex: 1}}>
|
||||
<Text type="title-lg" style={[pal.text, {fontWeight: '600'}]}>
|
||||
<Layout.Header.Outer>
|
||||
<Layout.Header.BackButton />
|
||||
<Layout.Header.Content align="left">
|
||||
<Layout.Header.TitleText>
|
||||
<Trans>Moderation Lists</Trans>
|
||||
</Text>
|
||||
<Text style={pal.textLight}>
|
||||
</Layout.Header.TitleText>
|
||||
<Layout.Header.SubtitleText>
|
||||
<Trans>
|
||||
Public, shareable lists of users to mute or block in bulk.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[{marginLeft: 18}, isMobile && {marginLeft: 12}]}>
|
||||
<Button
|
||||
testID="newModListBtn"
|
||||
type="default"
|
||||
onPress={onPressNewList}
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
}}>
|
||||
<FontAwesomeIcon icon="plus" color={pal.colors.text} />
|
||||
<Text type="button" style={pal.text}>
|
||||
<Trans>New</Trans>
|
||||
</Text>
|
||||
</Button>
|
||||
</View>
|
||||
</SimpleViewHeader>
|
||||
<MyLists filter="mod" style={s.flexGrow1} />
|
||||
</Layout.Header.SubtitleText>
|
||||
</Layout.Header.Content>
|
||||
<Button
|
||||
label={_(msg`New list`)}
|
||||
testID="newModListBtn"
|
||||
color="secondary"
|
||||
variant="solid"
|
||||
size="small"
|
||||
onPress={onPressNewList}>
|
||||
<ButtonIcon icon={PlusIcon} />
|
||||
<ButtonText>
|
||||
<Trans context="action">New</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</Layout.Header.Outer>
|
||||
<MyLists filter="mod" style={a.flex_grow} />
|
||||
<VerifyEmailDialog
|
||||
reasonText={_(
|
||||
msg`Before creating a list, you must first verify your email.`,
|
||||
|
||||
@@ -23,7 +23,7 @@ import {ProfileCard} from '#/view/com/profile/ProfileCard'
|
||||
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import {ViewHeader} from '#/view/com/util/ViewHeader'
|
||||
import {CenteredView} from '#/view/com/util/Views'
|
||||
import {atoms as a} from '#/alf'
|
||||
import * as Layout from '#/components/Layout'
|
||||
|
||||
type Props = NativeStackScreenProps<
|
||||
@@ -97,21 +97,17 @@ export function ModerationMutedAccounts({}: Props) {
|
||||
)
|
||||
return (
|
||||
<Layout.Screen testID="mutedAccountsScreen">
|
||||
<CenteredView
|
||||
style={[
|
||||
styles.container,
|
||||
isTabletOrDesktop && styles.containerDesktop,
|
||||
pal.view,
|
||||
pal.border,
|
||||
]}
|
||||
testID="mutedAccountsScreen">
|
||||
<ViewHeader title={_(msg`Muted Accounts`)} showOnDesktop />
|
||||
<ViewHeader title={_(msg`Muted Accounts`)} showOnDesktop />
|
||||
<Layout.Center style={[a.flex_1, {paddingBottom: 100}]}>
|
||||
<Text
|
||||
type="sm"
|
||||
style={[
|
||||
styles.description,
|
||||
pal.text,
|
||||
isTabletOrDesktop && styles.descriptionDesktop,
|
||||
{
|
||||
marginTop: 20,
|
||||
},
|
||||
]}>
|
||||
<Trans>
|
||||
Muted accounts have their posts removed from your feed and from your
|
||||
@@ -119,7 +115,7 @@ export function ModerationMutedAccounts({}: Props) {
|
||||
</Trans>
|
||||
</Text>
|
||||
{isEmpty ? (
|
||||
<View style={[pal.border, !isTabletOrDesktop && styles.flex1]}>
|
||||
<View style={[pal.border]}>
|
||||
{isError ? (
|
||||
<ErrorScreen
|
||||
title="Oops!"
|
||||
@@ -165,21 +161,12 @@ export function ModerationMutedAccounts({}: Props) {
|
||||
desktopFixedHeight
|
||||
/>
|
||||
)}
|
||||
</CenteredView>
|
||||
</Layout.Center>
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingBottom: 100,
|
||||
},
|
||||
containerDesktop: {
|
||||
borderLeftWidth: 1,
|
||||
borderRightWidth: 1,
|
||||
paddingBottom: 0,
|
||||
},
|
||||
title: {
|
||||
textAlign: 'center',
|
||||
marginTop: 12,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {useCallback} from 'react'
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
@@ -6,7 +6,6 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {ComposeIcon2} from '#/lib/icons'
|
||||
import {
|
||||
NativeStackScreenProps,
|
||||
@@ -14,7 +13,7 @@ import {
|
||||
} from '#/lib/routes/types'
|
||||
import {s} from '#/lib/styles'
|
||||
import {logger} from '#/logger'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {isNative, isWeb} from '#/platform/detection'
|
||||
import {emitSoftReset, listenSoftReset} from '#/state/events'
|
||||
import {RQKEY as NOTIFS_RQKEY} from '#/state/queries/notifications/feed'
|
||||
import {
|
||||
@@ -29,28 +28,25 @@ import {FAB} from '#/view/com/util/fab/FAB'
|
||||
import {ListMethods} from '#/view/com/util/List'
|
||||
import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
|
||||
import {MainScrollProvider} from '#/view/com/util/MainScrollProvider'
|
||||
import {ViewHeader} from '#/view/com/util/ViewHeader'
|
||||
import {CenteredView} from '#/view/com/util/Views'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {Button, ButtonIcon} from '#/components/Button'
|
||||
import {SettingsGear2_Stroke2_Corner0_Rounded as SettingsIcon} from '#/components/icons/SettingsGear2'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {Link} from '#/components/Link'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
type Props = NativeStackScreenProps<
|
||||
NotificationsTabNavigatorParams,
|
||||
'Notifications'
|
||||
>
|
||||
export function NotificationsScreen({route: {params}}: Props) {
|
||||
const t = useTheme()
|
||||
const {gtTablet} = useBreakpoints()
|
||||
const {_} = useLingui()
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
|
||||
const [isLoadingLatest, setIsLoadingLatest] = React.useState(false)
|
||||
const scrollElRef = React.useRef<ListMethods>(null)
|
||||
const t = useTheme()
|
||||
const {isDesktop} = useWebMediaQueries()
|
||||
const queryClient = useQueryClient()
|
||||
const unreadNotifs = useUnreadNotifications()
|
||||
const unreadApi = useUnreadNotificationsApi()
|
||||
@@ -110,121 +106,77 @@ export function NotificationsScreen({route: {params}}: Props) {
|
||||
return listenSoftReset(onPressLoadLatest)
|
||||
}, [onPressLoadLatest, isScreenFocused])
|
||||
|
||||
const renderButton = useCallback(() => {
|
||||
return (
|
||||
<Link
|
||||
to="/notifications/settings"
|
||||
label={_(msg`Notification settings`)}
|
||||
size="small"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
shape="square"
|
||||
style={[a.justify_center]}>
|
||||
<SettingsIcon size="md" style={t.atoms.text_contrast_medium} />
|
||||
</Link>
|
||||
)
|
||||
}, [_, t])
|
||||
|
||||
const ListHeaderComponent = React.useCallback(() => {
|
||||
if (isDesktop) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
t.atoms.bg,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.justify_between,
|
||||
a.gap_lg,
|
||||
a.px_lg,
|
||||
a.pr_md,
|
||||
a.py_sm,
|
||||
]}>
|
||||
return (
|
||||
<Layout.Screen testID="notificationsScreen">
|
||||
<Layout.Header.Outer>
|
||||
<Layout.Header.MenuButton />
|
||||
<Layout.Header.Content>
|
||||
<Button
|
||||
label={_(msg`Notifications`)}
|
||||
accessibilityHint={_(msg`Refresh notifications`)}
|
||||
onPress={emitSoftReset}>
|
||||
{({hovered, pressed}) => (
|
||||
<Text
|
||||
style={[
|
||||
a.text_2xl,
|
||||
a.font_bold,
|
||||
(hovered || pressed) && a.underline,
|
||||
]}>
|
||||
onPress={emitSoftReset}
|
||||
style={[a.justify_start]}>
|
||||
{({hovered}) => (
|
||||
<Layout.Header.TitleText
|
||||
style={[a.w_full, hovered && a.underline]}>
|
||||
<Trans>Notifications</Trans>
|
||||
{hasNew && (
|
||||
{isWeb && gtTablet && hasNew && (
|
||||
<View
|
||||
style={{
|
||||
left: 4,
|
||||
top: -8,
|
||||
backgroundColor: t.palette.primary_500,
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
style={[
|
||||
a.rounded_full,
|
||||
{
|
||||
width: 8,
|
||||
height: 8,
|
||||
bottom: 3,
|
||||
left: 6,
|
||||
backgroundColor: t.palette.primary_500,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</Text>
|
||||
</Layout.Header.TitleText>
|
||||
)}
|
||||
</Button>
|
||||
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
|
||||
{isLoadingLatest ? <Loader size="md" /> : <></>}
|
||||
{renderButton()}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
return <></>
|
||||
}, [isDesktop, t, hasNew, renderButton, _, isLoadingLatest])
|
||||
</Layout.Header.Content>
|
||||
<Layout.Header.Slot>
|
||||
<Link
|
||||
to="/notifications/settings"
|
||||
label={_(msg`Notification settings`)}
|
||||
size="small"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
shape="round"
|
||||
style={[a.justify_center]}>
|
||||
<ButtonIcon
|
||||
icon={isLoadingLatest ? Loader : SettingsIcon}
|
||||
size="lg"
|
||||
/>
|
||||
</Link>
|
||||
</Layout.Header.Slot>
|
||||
</Layout.Header.Outer>
|
||||
|
||||
const renderHeaderSpinner = React.useCallback(() => {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
{width: 30, height: 20},
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.justify_end,
|
||||
a.gap_md,
|
||||
]}>
|
||||
{isLoadingLatest ? <Loader width={20} /> : <></>}
|
||||
{renderButton()}
|
||||
</View>
|
||||
)
|
||||
}, [renderButton, isLoadingLatest])
|
||||
|
||||
return (
|
||||
<Layout.Screen testID="notificationsScreen">
|
||||
<CenteredView style={[a.flex_1, {paddingTop: 2}]} sideBorders={true}>
|
||||
<ViewHeader
|
||||
title={_(msg`Notifications`)}
|
||||
canGoBack={false}
|
||||
showBorder={true}
|
||||
renderButton={renderHeaderSpinner}
|
||||
<MainScrollProvider>
|
||||
<Feed
|
||||
onScrolledDownChange={setIsScrolledDown}
|
||||
scrollElRef={scrollElRef}
|
||||
overridePriorityNotifications={params?.show === 'all'}
|
||||
/>
|
||||
<MainScrollProvider>
|
||||
<Feed
|
||||
onScrolledDownChange={setIsScrolledDown}
|
||||
scrollElRef={scrollElRef}
|
||||
ListHeaderComponent={ListHeaderComponent}
|
||||
overridePriorityNotifications={params?.show === 'all'}
|
||||
/>
|
||||
</MainScrollProvider>
|
||||
{(isScrolledDown || hasNew) && (
|
||||
<LoadLatestBtn
|
||||
onPress={onPressLoadLatest}
|
||||
label={_(msg`Load new notifications`)}
|
||||
showIndicator={hasNew}
|
||||
/>
|
||||
)}
|
||||
<FAB
|
||||
testID="composeFAB"
|
||||
onPress={() => openComposer({})}
|
||||
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`New post`)}
|
||||
accessibilityHint=""
|
||||
</MainScrollProvider>
|
||||
{(isScrolledDown || hasNew) && (
|
||||
<LoadLatestBtn
|
||||
onPress={onPressLoadLatest}
|
||||
label={_(msg`Load new notifications`)}
|
||||
showIndicator={hasNew}
|
||||
/>
|
||||
</CenteredView>
|
||||
)}
|
||||
<FAB
|
||||
testID="composeFAB"
|
||||
onPress={() => openComposer({})}
|
||||
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`New post`)}
|
||||
accessibilityHint=""
|
||||
/>
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
|
||||
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
||||
import {makeRecordUri} from '#/lib/strings/url-helpers'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {PostThread as PostThreadComponent} from '#/view/com/post-thread/PostThread'
|
||||
import {atoms as a} from '#/alf'
|
||||
import * as Layout from '#/components/Layout'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostThread'>
|
||||
@@ -24,9 +22,7 @@ export function PostThreadScreen({route}: Props) {
|
||||
|
||||
return (
|
||||
<Layout.Screen testID="postThreadScreen">
|
||||
<View style={a.flex_1}>
|
||||
<PostThreadComponent uri={uri} />
|
||||
</View>
|
||||
<PostThreadComponent uri={uri} />
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -40,11 +40,9 @@ import {PagerWithHeader} from '#/view/com/pager/PagerWithHeader'
|
||||
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
|
||||
import {FAB} from '#/view/com/util/fab/FAB'
|
||||
import {ListRef} from '#/view/com/util/List'
|
||||
import {CenteredView} from '#/view/com/util/Views'
|
||||
import {ProfileHeader, ProfileHeaderLoading} from '#/screens/Profile/Header'
|
||||
import {ProfileFeedSection} from '#/screens/Profile/Sections/Feed'
|
||||
import {ProfileLabelsSection} from '#/screens/Profile/Sections/Labels'
|
||||
import {web} from '#/alf'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {ScreenHider} from '#/components/moderation/ScreenHider'
|
||||
import {ProfileStarterPacks} from '#/components/StarterPack/ProfileStarterPacks'
|
||||
@@ -116,9 +114,9 @@ function ProfileScreenInner({route}: Props) {
|
||||
// Most pushes will happen here, since we will have only placeholder data
|
||||
if (isLoadingDid || isLoadingProfile || starterPacksQuery.isLoading) {
|
||||
return (
|
||||
<CenteredView sideBorders style={web({height: '100vh'})}>
|
||||
<Layout.Content>
|
||||
<ProfileHeaderLoading />
|
||||
</CenteredView>
|
||||
</Layout.Content>
|
||||
)
|
||||
}
|
||||
if (resolveError || profileError) {
|
||||
|
||||
@@ -49,7 +49,6 @@ import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
|
||||
import {LoadingScreen} from '#/view/com/util/LoadingScreen'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {CenteredView} from '#/view/com/util/Views'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button as NewButton, ButtonText} from '#/components/Button'
|
||||
import {useRichText} from '#/components/hooks/useRichText'
|
||||
@@ -98,7 +97,7 @@ export function ProfileFeedScreen(props: Props) {
|
||||
if (error) {
|
||||
return (
|
||||
<Layout.Screen testID="profileFeedScreenError">
|
||||
<CenteredView>
|
||||
<Layout.Content>
|
||||
<View style={[pal.view, pal.border, styles.notFoundContainer]}>
|
||||
<Text type="title-lg" style={[pal.text, s.mb10]}>
|
||||
<Trans>Could not load feed</Trans>
|
||||
@@ -120,7 +119,7 @@ export function ProfileFeedScreen(props: Props) {
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</CenteredView>
|
||||
</Layout.Content>
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
@@ -394,7 +393,7 @@ export function ProfileFeedScreenInner({
|
||||
])
|
||||
|
||||
return (
|
||||
<View style={s.hContentRegion}>
|
||||
<>
|
||||
<ReportDialog
|
||||
control={reportDialogControl}
|
||||
params={{
|
||||
@@ -434,7 +433,7 @@ export function ProfileFeedScreenInner({
|
||||
accessibilityHint=""
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import {ProfileFollowers as ProfileFollowersComponent} from '#/view/com/profile/
|
||||
import {ViewHeader} from '#/view/com/util/ViewHeader'
|
||||
import {CenteredView} from '#/view/com/util/Views'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {ListHeaderDesktop} from '#/components/Lists'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollowers'>
|
||||
export const ProfileFollowersScreen = ({route}: Props) => {
|
||||
@@ -27,7 +26,6 @@ export const ProfileFollowersScreen = ({route}: Props) => {
|
||||
return (
|
||||
<Layout.Screen testID="profileFollowersScreen">
|
||||
<CenteredView sideBorders={true}>
|
||||
<ListHeaderDesktop title={_(msg`Followers`)} />
|
||||
<ViewHeader title={_(msg`Followers`)} showBorder={!isWeb} />
|
||||
<ProfileFollowersComponent name={name} />
|
||||
</CenteredView>
|
||||
|
||||
@@ -10,7 +10,6 @@ import {ProfileFollows as ProfileFollowsComponent} from '#/view/com/profile/Prof
|
||||
import {ViewHeader} from '#/view/com/util/ViewHeader'
|
||||
import {CenteredView} from '#/view/com/util/Views'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {ListHeaderDesktop} from '#/components/Lists'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollows'>
|
||||
export const ProfileFollowsScreen = ({route}: Props) => {
|
||||
@@ -27,7 +26,6 @@ export const ProfileFollowsScreen = ({route}: Props) => {
|
||||
return (
|
||||
<Layout.Screen testID="profileFollowsScreen">
|
||||
<CenteredView sideBorders={true}>
|
||||
<ListHeaderDesktop title={_(msg`Following`)} />
|
||||
<ViewHeader title={_(msg`Following`)} showBorder={!isWeb} />
|
||||
<ProfileFollowsComponent name={name} />
|
||||
</CenteredView>
|
||||
|
||||
@@ -69,7 +69,6 @@ import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
|
||||
import {LoadingScreen} from '#/view/com/util/LoadingScreen'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {CenteredView} from '#/view/com/util/Views'
|
||||
import {ListHiddenScreen} from '#/screens/List/ListHiddenScreen'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
@@ -107,20 +106,20 @@ function ProfileListScreenInner(props: Props) {
|
||||
|
||||
if (resolveError) {
|
||||
return (
|
||||
<CenteredView>
|
||||
<Layout.Content>
|
||||
<ErrorScreen
|
||||
error={_(
|
||||
msg`We're sorry, but we were unable to resolve this list. If this persists, please contact the list creator, @${handleOrDid}.`,
|
||||
)}
|
||||
/>
|
||||
</CenteredView>
|
||||
</Layout.Content>
|
||||
)
|
||||
}
|
||||
if (listError) {
|
||||
return (
|
||||
<CenteredView>
|
||||
<Layout.Content>
|
||||
<ErrorScreen error={cleanError(listError)} />
|
||||
</CenteredView>
|
||||
</Layout.Content>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1010,7 +1009,6 @@ function ErrorScreen({error}: {error: string}) {
|
||||
pal.view,
|
||||
pal.border,
|
||||
{
|
||||
marginTop: 10,
|
||||
paddingHorizontal: 18,
|
||||
paddingVertical: 14,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
|
||||
+117
-127
@@ -25,13 +25,12 @@ import {FeedSourceCard} from '#/view/com/feeds/FeedSourceCard'
|
||||
import {TextLink} from '#/view/com/util/Link'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {ViewHeader} from '#/view/com/util/ViewHeader'
|
||||
import {CenteredView, ScrollView} from '#/view/com/util/Views'
|
||||
import {NoFollowingFeed} from '#/screens/Feeds/NoFollowingFeed'
|
||||
import {NoSavedFeedsOfAnyType} from '#/screens/Feeds/NoSavedFeedsOfAnyType'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline'
|
||||
import {FloppyDisk_Stroke2_Corner0_Rounded as SaveIcon} from '#/components/icons/FloppyDisk'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {Loader} from '#/components/Loader'
|
||||
|
||||
@@ -51,7 +50,7 @@ function SavedFeedsInner({
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {isMobile, isTabletOrDesktop, isDesktop} = useWebMediaQueries()
|
||||
const {isMobile, isDesktop} = useWebMediaQueries()
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {mutateAsync: overwriteSavedFeeds, isPending: isOverwritePending} =
|
||||
useOverwriteSavedFeedsMutation()
|
||||
@@ -88,136 +87,128 @@ function SavedFeedsInner({
|
||||
}
|
||||
}, [_, overwriteSavedFeeds, currentFeeds, navigation])
|
||||
|
||||
const renderHeaderBtn = React.useCallback(() => {
|
||||
return (
|
||||
<Button
|
||||
size="small"
|
||||
variant={hasUnsavedChanges ? 'solid' : 'solid'}
|
||||
color={hasUnsavedChanges ? 'primary' : 'secondary'}
|
||||
onPress={onSaveChanges}
|
||||
label={_(msg`Save changes`)}
|
||||
disabled={isOverwritePending || !hasUnsavedChanges}
|
||||
style={[isDesktop && a.mt_sm]}
|
||||
testID="saveChangesBtn">
|
||||
<ButtonText style={[isDesktop && a.text_md]}>
|
||||
{isDesktop ? <Trans>Save changes</Trans> : <Trans>Save</Trans>}
|
||||
</ButtonText>
|
||||
{isOverwritePending && <ButtonIcon icon={Loader} />}
|
||||
</Button>
|
||||
)
|
||||
}, [_, isDesktop, onSaveChanges, hasUnsavedChanges, isOverwritePending])
|
||||
|
||||
return (
|
||||
<Layout.Screen>
|
||||
<CenteredView
|
||||
style={[a.util_screen_outer]}
|
||||
sideBorders={isTabletOrDesktop}>
|
||||
<ViewHeader
|
||||
title={_(msg`Edit My Feeds`)}
|
||||
showOnDesktop
|
||||
showBorder
|
||||
renderButton={renderHeaderBtn}
|
||||
/>
|
||||
<ScrollView style={[a.flex_1]} contentContainerStyle={[a.border_0]}>
|
||||
{noSavedFeedsOfAnyType && (
|
||||
<View style={[pal.border, a.border_b]}>
|
||||
<NoSavedFeedsOfAnyType />
|
||||
<Layout.Header.Outer>
|
||||
<Layout.Header.BackButton />
|
||||
<Layout.Header.Content align="left">
|
||||
<Layout.Header.TitleText>
|
||||
<Trans>Feeds</Trans>
|
||||
</Layout.Header.TitleText>
|
||||
</Layout.Header.Content>
|
||||
<Button
|
||||
testID="saveChangesBtn"
|
||||
size="small"
|
||||
variant={hasUnsavedChanges ? 'solid' : 'solid'}
|
||||
color={hasUnsavedChanges ? 'primary' : 'secondary'}
|
||||
onPress={onSaveChanges}
|
||||
label={_(msg`Save changes`)}
|
||||
disabled={isOverwritePending || !hasUnsavedChanges}>
|
||||
<ButtonIcon icon={isOverwritePending ? Loader : SaveIcon} />
|
||||
<ButtonText>
|
||||
{isDesktop ? <Trans>Save changes</Trans> : <Trans>Save</Trans>}
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</Layout.Header.Outer>
|
||||
|
||||
<Layout.Content>
|
||||
{noSavedFeedsOfAnyType && (
|
||||
<View style={[pal.border, a.border_b]}>
|
||||
<NoSavedFeedsOfAnyType />
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={[pal.text, pal.border, styles.title]}>
|
||||
<Text type="title" style={pal.text}>
|
||||
<Trans>Pinned Feeds</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{preferences ? (
|
||||
!pinnedFeeds.length ? (
|
||||
<View
|
||||
style={[
|
||||
pal.border,
|
||||
isMobile && s.flex1,
|
||||
pal.viewLight,
|
||||
styles.empty,
|
||||
]}>
|
||||
<Text type="lg" style={[pal.text]}>
|
||||
<Trans>You don't have any pinned feeds.</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={[pal.text, pal.border, styles.title]}>
|
||||
<Text type="title" style={pal.text}>
|
||||
<Trans>Pinned Feeds</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{preferences ? (
|
||||
!pinnedFeeds.length ? (
|
||||
<View
|
||||
style={[
|
||||
pal.border,
|
||||
isMobile && s.flex1,
|
||||
pal.viewLight,
|
||||
styles.empty,
|
||||
]}>
|
||||
<Text type="lg" style={[pal.text]}>
|
||||
<Trans>You don't have any pinned feeds.</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
pinnedFeeds.map(f => (
|
||||
<ListItem
|
||||
key={f.id}
|
||||
feed={f}
|
||||
isPinned
|
||||
currentFeeds={currentFeeds}
|
||||
setCurrentFeeds={setCurrentFeeds}
|
||||
preferences={preferences}
|
||||
/>
|
||||
))
|
||||
)
|
||||
) : (
|
||||
<ActivityIndicator style={{marginTop: 20}} />
|
||||
)}
|
||||
pinnedFeeds.map(f => (
|
||||
<ListItem
|
||||
key={f.id}
|
||||
feed={f}
|
||||
isPinned
|
||||
currentFeeds={currentFeeds}
|
||||
setCurrentFeeds={setCurrentFeeds}
|
||||
preferences={preferences}
|
||||
/>
|
||||
))
|
||||
)
|
||||
) : (
|
||||
<ActivityIndicator style={{marginTop: 20}} />
|
||||
)}
|
||||
|
||||
{noFollowingFeed && (
|
||||
<View style={[pal.border, a.border_b]}>
|
||||
<NoFollowingFeed />
|
||||
{noFollowingFeed && (
|
||||
<View style={[pal.border, a.border_b]}>
|
||||
<NoFollowingFeed />
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={[pal.text, pal.border, styles.title]}>
|
||||
<Text type="title" style={pal.text}>
|
||||
<Trans>Saved Feeds</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
{preferences ? (
|
||||
!unpinnedFeeds.length ? (
|
||||
<View
|
||||
style={[
|
||||
pal.border,
|
||||
isMobile && s.flex1,
|
||||
pal.viewLight,
|
||||
styles.empty,
|
||||
]}>
|
||||
<Text type="lg" style={[pal.text]}>
|
||||
<Trans>You don't have any saved feeds.</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={[pal.text, pal.border, styles.title]}>
|
||||
<Text type="title" style={pal.text}>
|
||||
<Trans>Saved Feeds</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
{preferences ? (
|
||||
!unpinnedFeeds.length ? (
|
||||
<View
|
||||
style={[
|
||||
pal.border,
|
||||
isMobile && s.flex1,
|
||||
pal.viewLight,
|
||||
styles.empty,
|
||||
]}>
|
||||
<Text type="lg" style={[pal.text]}>
|
||||
<Trans>You don't have any saved feeds.</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
unpinnedFeeds.map(f => (
|
||||
<ListItem
|
||||
key={f.id}
|
||||
feed={f}
|
||||
isPinned={false}
|
||||
currentFeeds={currentFeeds}
|
||||
setCurrentFeeds={setCurrentFeeds}
|
||||
preferences={preferences}
|
||||
/>
|
||||
))
|
||||
)
|
||||
) : (
|
||||
<ActivityIndicator style={{marginTop: 20}} />
|
||||
)}
|
||||
unpinnedFeeds.map(f => (
|
||||
<ListItem
|
||||
key={f.id}
|
||||
feed={f}
|
||||
isPinned={false}
|
||||
currentFeeds={currentFeeds}
|
||||
setCurrentFeeds={setCurrentFeeds}
|
||||
preferences={preferences}
|
||||
/>
|
||||
))
|
||||
)
|
||||
) : (
|
||||
<ActivityIndicator style={{marginTop: 20}} />
|
||||
)}
|
||||
|
||||
<View style={styles.footerText}>
|
||||
<Text type="sm" style={pal.textLight}>
|
||||
<Trans>
|
||||
Feeds are custom algorithms that users build with a little
|
||||
coding expertise.{' '}
|
||||
<TextLink
|
||||
type="sm"
|
||||
style={pal.link}
|
||||
href="https://github.com/bluesky-social/feed-generator"
|
||||
text={_(msg`See this guide`)}
|
||||
/>{' '}
|
||||
for more information.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
<View style={{height: 100}} />
|
||||
</ScrollView>
|
||||
</CenteredView>
|
||||
<View style={styles.footerText}>
|
||||
<Text type="sm" style={pal.textLight}>
|
||||
<Trans>
|
||||
Feeds are custom algorithms that users build with a little coding
|
||||
expertise.{' '}
|
||||
<TextLink
|
||||
type="sm"
|
||||
style={pal.link}
|
||||
href="https://github.com/bluesky-social/feed-generator"
|
||||
text={_(msg`See this guide`)}
|
||||
/>{' '}
|
||||
for more information.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</Layout.Content>
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
@@ -456,7 +447,6 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
footerText: {
|
||||
paddingHorizontal: 26,
|
||||
paddingTop: 22,
|
||||
paddingBottom: 100,
|
||||
paddingVertical: 22,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -388,7 +388,7 @@ export function Explore() {
|
||||
key: 'suggested-feeds-header',
|
||||
title: _(msg`Discover new feeds`),
|
||||
description: _(
|
||||
msg`Custom feeds built by the community bring you new experiences and help you find the content you love.`,
|
||||
msg`Choose your own timeline! Feeds built by the community help you find content you love.`,
|
||||
),
|
||||
style: [a.pt_5xl],
|
||||
icon: ListSparkle,
|
||||
|
||||
+105
-131
@@ -55,7 +55,6 @@ 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'
|
||||
import {Explore} from '#/view/screens/Search/Explore'
|
||||
import {SearchLinkCard, SearchProfileCard} from '#/view/shell/desktop/Search'
|
||||
import {makeSearchQuery, parseSearchQuery} from '#/screens/Search/utils'
|
||||
@@ -68,63 +67,46 @@ import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
|
||||
import * as Layout from '#/components/Layout'
|
||||
|
||||
function Loader() {
|
||||
const pal = usePalette('default')
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
return (
|
||||
<CenteredView
|
||||
style={[
|
||||
// @ts-ignore web only -prf
|
||||
{
|
||||
padding: 18,
|
||||
height: isWeb ? '100vh' : undefined,
|
||||
},
|
||||
pal.border,
|
||||
]}
|
||||
sideBorders={!isMobile}>
|
||||
<ActivityIndicator />
|
||||
</CenteredView>
|
||||
<Layout.Content>
|
||||
<View style={[a.py_xl]}>
|
||||
<ActivityIndicator />
|
||||
</View>
|
||||
</Layout.Content>
|
||||
)
|
||||
}
|
||||
|
||||
function EmptyState({message, error}: {message: string; error?: string}) {
|
||||
const pal = usePalette('default')
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
|
||||
return (
|
||||
<CenteredView
|
||||
sideBorders={!isMobile}
|
||||
style={[
|
||||
pal.border,
|
||||
// @ts-ignore web only -prf
|
||||
{
|
||||
padding: 18,
|
||||
height: isWeb ? '100vh' : undefined,
|
||||
},
|
||||
]}>
|
||||
<View style={[pal.viewLight, {padding: 18, borderRadius: 8}]}>
|
||||
<Text style={[pal.text]}>{message}</Text>
|
||||
<Layout.Content>
|
||||
<View style={[a.p_xl]}>
|
||||
<View style={[pal.viewLight, {padding: 18, borderRadius: 8}]}>
|
||||
<Text style={[pal.text]}>{message}</Text>
|
||||
|
||||
{error && (
|
||||
<>
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
marginVertical: 12,
|
||||
height: 1,
|
||||
width: '100%',
|
||||
backgroundColor: pal.text.color,
|
||||
opacity: 0.2,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{error && (
|
||||
<>
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
marginVertical: 12,
|
||||
height: 1,
|
||||
width: '100%',
|
||||
backgroundColor: pal.text.color,
|
||||
opacity: 0.2,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Text style={[pal.textLight]}>
|
||||
<Trans>Error:</Trans> {error}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
<Text style={[pal.textLight]}>
|
||||
<Trans>Error:</Trans> {error}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</CenteredView>
|
||||
</Layout.Content>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -224,7 +206,7 @@ let SearchScreenPostResults = ({
|
||||
if (item.type === 'post') {
|
||||
return <Post post={item.post} />
|
||||
} else {
|
||||
return <Loader />
|
||||
return null
|
||||
}
|
||||
}}
|
||||
keyExtractor={item => item.key}
|
||||
@@ -550,19 +532,13 @@ let SearchScreenInner = ({
|
||||
<Pager
|
||||
onPageSelected={onPageSelected}
|
||||
renderTabBar={props => (
|
||||
<CenteredView
|
||||
sideBorders
|
||||
<Layout.Center
|
||||
style={[
|
||||
pal.border,
|
||||
pal.view,
|
||||
web({
|
||||
position: isWeb ? 'sticky' : '',
|
||||
zIndex: 1,
|
||||
}),
|
||||
web([a.sticky, a.z_10]),
|
||||
{top: isWeb ? headerHeight : undefined},
|
||||
]}>
|
||||
<TabBar items={sections.map(section => section.title)} {...props} />
|
||||
</CenteredView>
|
||||
</Layout.Center>
|
||||
)}
|
||||
initialPage={0}>
|
||||
{sections.map((section, i) => (
|
||||
@@ -572,7 +548,7 @@ let SearchScreenInner = ({
|
||||
) : hasSession ? (
|
||||
<Explore />
|
||||
) : (
|
||||
<CenteredView sideBorders style={pal.border}>
|
||||
<Layout.Center>
|
||||
<View
|
||||
// @ts-ignore web only -esb
|
||||
style={{
|
||||
@@ -614,7 +590,7 @@ let SearchScreenInner = ({
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</CenteredView>
|
||||
</Layout.Center>
|
||||
)
|
||||
}
|
||||
SearchScreenInner = React.memo(SearchScreenInner)
|
||||
@@ -650,7 +626,7 @@ export function SearchScreen(
|
||||
* Arbitrary sizing, so guess and check, used for sticky header alignment and
|
||||
* sizing.
|
||||
*/
|
||||
const headerHeight = 64 + (showFilters ? 40 : 0)
|
||||
const headerHeight = 60 + (showFilters ? 40 : 0)
|
||||
|
||||
useFocusEffect(
|
||||
useNonReactiveCallback(() => {
|
||||
@@ -861,73 +837,79 @@ export function SearchScreen(
|
||||
|
||||
return (
|
||||
<Layout.Screen testID="searchScreen">
|
||||
<CenteredView
|
||||
<View
|
||||
style={[
|
||||
a.p_md,
|
||||
a.pb_sm,
|
||||
a.gap_sm,
|
||||
t.atoms.bg,
|
||||
web({
|
||||
height: headerHeight,
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 1,
|
||||
}),
|
||||
]}
|
||||
sideBorders={gtMobile}>
|
||||
<View style={[a.flex_row, a.gap_sm]}>
|
||||
{!gtMobile && (
|
||||
<Button
|
||||
testID="viewHeaderBackOrMenuBtn"
|
||||
onPress={onPressMenu}
|
||||
hitSlop={HITSLOP_10}
|
||||
label={_(msg`Menu`)}
|
||||
accessibilityHint={_(msg`Access navigation links and settings`)}
|
||||
size="large"
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
shape="square">
|
||||
<ButtonIcon icon={Menu} size="lg" />
|
||||
</Button>
|
||||
)}
|
||||
<View style={[a.flex_1]}>
|
||||
<SearchInput
|
||||
ref={textInput}
|
||||
value={searchText}
|
||||
onFocus={onSearchInputFocus}
|
||||
onChangeText={onChangeText}
|
||||
onClearText={onPressClearQuery}
|
||||
onSubmitEditing={onSubmit}
|
||||
/>
|
||||
</View>
|
||||
{showAutocomplete && (
|
||||
<Button
|
||||
label={_(msg`Cancel search`)}
|
||||
size="large"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
style={[a.px_sm]}
|
||||
onPress={onPressCancelSearch}
|
||||
hitSlop={HITSLOP_10}>
|
||||
<ButtonText>
|
||||
<Trans>Cancel</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{showFilters && (
|
||||
<View
|
||||
style={[a.flex_row, a.align_center, a.justify_between, a.gap_sm]}>
|
||||
<View style={[{width: 140}]}>
|
||||
<SearchLanguageDropdown
|
||||
value={params.lang}
|
||||
onChange={params.setLang}
|
||||
/>
|
||||
]}>
|
||||
<Layout.Center>
|
||||
<View style={[a.p_md, a.pb_sm, a.gap_sm, t.atoms.bg]}>
|
||||
<View style={[a.flex_row, a.gap_sm]}>
|
||||
{!gtMobile && (
|
||||
<Button
|
||||
testID="viewHeaderBackOrMenuBtn"
|
||||
onPress={onPressMenu}
|
||||
hitSlop={HITSLOP_10}
|
||||
label={_(msg`Menu`)}
|
||||
accessibilityHint={_(
|
||||
msg`Access navigation links and settings`,
|
||||
)}
|
||||
size="large"
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
shape="square">
|
||||
<ButtonIcon icon={Menu} size="lg" />
|
||||
</Button>
|
||||
)}
|
||||
<View style={[a.flex_1]}>
|
||||
<SearchInput
|
||||
ref={textInput}
|
||||
value={searchText}
|
||||
onFocus={onSearchInputFocus}
|
||||
onChangeText={onChangeText}
|
||||
onClearText={onPressClearQuery}
|
||||
onSubmitEditing={onSubmit}
|
||||
/>
|
||||
</View>
|
||||
{showAutocomplete && (
|
||||
<Button
|
||||
label={_(msg`Cancel search`)}
|
||||
size="large"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
style={[a.px_sm]}
|
||||
onPress={onPressCancelSearch}
|
||||
hitSlop={HITSLOP_10}>
|
||||
<ButtonText>
|
||||
<Trans>Cancel</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{showFilters && (
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.justify_between,
|
||||
a.gap_sm,
|
||||
]}>
|
||||
<View style={[{width: 140}]}>
|
||||
<SearchLanguageDropdown
|
||||
value={params.lang}
|
||||
onChange={params.setLang}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</CenteredView>
|
||||
</Layout.Center>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
@@ -992,10 +974,7 @@ let AutocompleteResults = ({
|
||||
!moderationOpts ? (
|
||||
<Loader />
|
||||
) : (
|
||||
<ScrollView
|
||||
style={{height: '100%'}}
|
||||
// @ts-ignore web only -prf
|
||||
dataSet={{stableGutters: '1'}}
|
||||
<Layout.Content
|
||||
keyboardShouldPersistTaps="handled"
|
||||
keyboardDismissMode="on-drag">
|
||||
<SearchLinkCard
|
||||
@@ -1020,7 +999,7 @@ let AutocompleteResults = ({
|
||||
/>
|
||||
))}
|
||||
<View style={{height: 200}} />
|
||||
</ScrollView>
|
||||
</Layout.Content>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
@@ -1042,17 +1021,12 @@ function SearchHistory({
|
||||
onRemoveItemClick: (item: string) => void
|
||||
onRemoveProfileClick: (profile: AppBskyActorDefs.ProfileViewBasic) => void
|
||||
}) {
|
||||
const {isTabletOrDesktop, isMobile} = useWebMediaQueries()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
|
||||
return (
|
||||
<CenteredView
|
||||
sideBorders={isTabletOrDesktop}
|
||||
// @ts-ignore web only -prf
|
||||
style={{
|
||||
height: isWeb ? '100vh' : undefined,
|
||||
}}>
|
||||
<Layout.Content>
|
||||
<View style={styles.searchHistoryContainer}>
|
||||
{(searchHistory.length > 0 || selectedProfiles.length > 0) && (
|
||||
<Text style={[pal.text, styles.searchHistoryTitle]}>
|
||||
@@ -1152,7 +1126,7 @@ function SearchHistory({
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</CenteredView>
|
||||
</Layout.Content>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ import {StyleSheet, View} from 'react-native'
|
||||
import {DismissableLayer} from '@radix-ui/react-dismissable-layer'
|
||||
import {useFocusGuards} from '@radix-ui/react-focus-guards'
|
||||
import {FocusScope} from '@radix-ui/react-focus-scope'
|
||||
import {RemoveScrollBar} from 'react-remove-scroll-bar'
|
||||
|
||||
import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
|
||||
import {useModals} from '#/state/modals'
|
||||
import {ComposerOpts, useComposerState} from '#/state/shell/composer'
|
||||
import {
|
||||
@@ -20,8 +20,6 @@ export function Composer({}: {winHeight: number}) {
|
||||
const state = useComposerState()
|
||||
const isActive = !!state
|
||||
|
||||
useWebBodyScrollLock(isActive)
|
||||
|
||||
// rendering
|
||||
// =
|
||||
|
||||
@@ -29,7 +27,12 @@ export function Composer({}: {winHeight: number}) {
|
||||
return <View />
|
||||
}
|
||||
|
||||
return <Inner state={state} />
|
||||
return (
|
||||
<>
|
||||
<RemoveScrollBar />
|
||||
<Inner state={state} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function Inner({state}: {state: ComposerOpts}) {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import React from 'react'
|
||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconStyle,
|
||||
} from '@fortawesome/react-native-fontawesome'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {
|
||||
@@ -14,9 +11,9 @@ import {
|
||||
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {getCurrentRoute, isStateAtTabRoot, isTab} from '#/lib/routes/helpers'
|
||||
import {getCurrentRoute, isTab} from '#/lib/routes/helpers'
|
||||
import {makeProfileLink} from '#/lib/routes/links'
|
||||
import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types'
|
||||
import {CommonNavigatorParams} from '#/lib/routes/types'
|
||||
import {isInvalidHandle} from '#/lib/strings/handles'
|
||||
import {emitSoftReset} from '#/state/events'
|
||||
import {useFetchHandle} from '#/state/queries/handle'
|
||||
@@ -101,47 +98,6 @@ function ProfileCard() {
|
||||
)
|
||||
}
|
||||
|
||||
const HIDDEN_BACK_BNT_ROUTES = ['StarterPackWizard', 'StarterPackEdit']
|
||||
|
||||
function BackBtn() {
|
||||
const {isTablet} = useWebMediaQueries()
|
||||
const pal = usePalette('default')
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {_} = useLingui()
|
||||
const shouldShow = useNavigationState(
|
||||
state =>
|
||||
!isStateAtTabRoot(state) &&
|
||||
!HIDDEN_BACK_BNT_ROUTES.includes(getCurrentRoute(state).name),
|
||||
)
|
||||
|
||||
const onPressBack = React.useCallback(() => {
|
||||
if (navigation.canGoBack()) {
|
||||
navigation.goBack()
|
||||
} else {
|
||||
navigation.navigate('Home')
|
||||
}
|
||||
}, [navigation])
|
||||
|
||||
if (!shouldShow || isTablet) {
|
||||
return <></>
|
||||
}
|
||||
return (
|
||||
<TouchableOpacity
|
||||
testID="viewHeaderBackOrMenuBtn"
|
||||
onPress={onPressBack}
|
||||
style={styles.backBtn}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Go back`)}
|
||||
accessibilityHint="">
|
||||
<FontAwesomeIcon
|
||||
size={24}
|
||||
icon="angle-left"
|
||||
style={pal.text as FontAwesomeIconStyle}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
}
|
||||
|
||||
interface NavItemProps {
|
||||
count?: string
|
||||
href: string
|
||||
@@ -220,35 +176,44 @@ function NavItem({count, href, icon, iconFilled, label}: NavItemProps) {
|
||||
]}>
|
||||
{isCurrent ? iconFilled : icon}
|
||||
{typeof count === 'string' && count ? (
|
||||
<Text
|
||||
accessibilityLabel={_(msg`${count} unread items`)}
|
||||
accessibilityHint=""
|
||||
accessible={true}
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.text_xs,
|
||||
a.font_bold,
|
||||
a.rounded_full,
|
||||
a.text_center,
|
||||
{
|
||||
top: '-10%',
|
||||
left: count.length === 1 ? '50%' : '40%',
|
||||
backgroundColor: t.palette.primary_500,
|
||||
color: t.palette.white,
|
||||
lineHeight: a.text_sm.fontSize,
|
||||
paddingHorizontal: 4,
|
||||
paddingVertical: 1,
|
||||
minWidth: 16,
|
||||
},
|
||||
isTablet && [
|
||||
{
|
||||
top: '10%',
|
||||
left: count.length === 1 ? '50%' : '40%',
|
||||
},
|
||||
],
|
||||
a.inset_0,
|
||||
{right: -20}, // more breathing room
|
||||
]}>
|
||||
{count}
|
||||
</Text>
|
||||
<Text
|
||||
accessibilityLabel={_(msg`${count} unread items`)}
|
||||
accessibilityHint=""
|
||||
accessible={true}
|
||||
numberOfLines={1}
|
||||
style={[
|
||||
a.absolute,
|
||||
a.text_xs,
|
||||
a.font_bold,
|
||||
a.rounded_full,
|
||||
a.text_center,
|
||||
a.leading_tight,
|
||||
{
|
||||
top: '-10%',
|
||||
left: count.length === 1 ? 12 : 8,
|
||||
backgroundColor: t.palette.primary_500,
|
||||
color: t.palette.white,
|
||||
lineHeight: a.text_sm.fontSize,
|
||||
paddingHorizontal: 4,
|
||||
paddingVertical: 1,
|
||||
minWidth: 16,
|
||||
},
|
||||
isTablet && [
|
||||
{
|
||||
top: '10%',
|
||||
left: count.length === 1 ? 20 : 16,
|
||||
},
|
||||
],
|
||||
]}>
|
||||
{count}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
{gtTablet && (
|
||||
@@ -366,9 +331,9 @@ export function DesktopLeftNav() {
|
||||
<View
|
||||
role="navigation"
|
||||
style={[
|
||||
a.px_xl,
|
||||
styles.leftNav,
|
||||
isTablet && styles.leftNavTablet,
|
||||
pal.view,
|
||||
pal.border,
|
||||
]}>
|
||||
{hasSession ? (
|
||||
@@ -381,8 +346,6 @@ export function DesktopLeftNav() {
|
||||
|
||||
{hasSession && (
|
||||
<>
|
||||
<BackBtn />
|
||||
|
||||
<NavItem
|
||||
href="/"
|
||||
icon={
|
||||
@@ -525,8 +488,17 @@ const styles = StyleSheet.create({
|
||||
position: 'fixed',
|
||||
top: 10,
|
||||
// @ts-ignore web only
|
||||
left: 'calc(50vw - 300px - 220px - 20px)',
|
||||
width: 220,
|
||||
left: '50%',
|
||||
transform: [
|
||||
{
|
||||
translateX: -300,
|
||||
},
|
||||
{
|
||||
translateX: '-100%',
|
||||
},
|
||||
...a.scrollbar_offset.transform,
|
||||
],
|
||||
width: 240,
|
||||
// @ts-ignore web only
|
||||
maxHeight: 'calc(100vh - 10px)',
|
||||
overflowY: 'auto',
|
||||
@@ -538,7 +510,10 @@ const styles = StyleSheet.create({
|
||||
borderRightWidth: 1,
|
||||
height: '100%',
|
||||
width: 76,
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
alignItems: 'center',
|
||||
transform: [],
|
||||
},
|
||||
|
||||
profileCard: {
|
||||
|
||||
+117
-209
@@ -1,72 +1,26 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {FEEDBACK_FORM_URL, HELP_DESK_URL} from '#/lib/constants'
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {s} from '#/lib/styles'
|
||||
import {useKawaiiMode} from '#/state/preferences/kawaii'
|
||||
import {usePurchases} from '#/state/purchases'
|
||||
import {EntitlementId} from '#/state/purchases/types'
|
||||
import {Nux, useNux, useResetNuxs, useSaveNux} from '#/state/queries/nuxs'
|
||||
import {useSession} from '#/state/session'
|
||||
import {DesktopFeeds} from '#/view/shell/desktop/Feeds'
|
||||
import {DesktopSearch} from '#/view/shell/desktop/Search'
|
||||
import {atoms as a, tokens, useTheme, web} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
import {BlueskyPlusCore} from '#/components/dialogs/BlueskyPlusCore'
|
||||
import {Divider} from '#/components/Divider'
|
||||
import {GradientFill} from '#/components/GradientFill'
|
||||
import {Full as BlueskyPlusLogo} from '#/components/icons/BlueskyPlus'
|
||||
import {SquareArrowTopRight_Stroke2_Corner0_Rounded as SquareArrowTopRight} from '#/components/icons/SquareArrowTopRight'
|
||||
import {createStaticClick, InlineLinkText} from '#/components/Link'
|
||||
import {TextLink} from '#/view/com/util/Link'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {ProgressGuideList} from '#/components/ProgressGuide/List'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {IS_DEV} from '#/env'
|
||||
import {DesktopFeeds} from './Feeds'
|
||||
import {DesktopSearch} from './Search'
|
||||
|
||||
export function DesktopRightNav({routeName}: {routeName: string}) {
|
||||
const t = useTheme()
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {hasSession, currentAccount} = useSession()
|
||||
|
||||
const kawaii = useKawaiiMode()
|
||||
const bskyPlusDialogControl = useDialogControl()
|
||||
|
||||
const {mutateAsync: resetNuxs} = useResetNuxs()
|
||||
const nux = useNux(Nux.BlueskyPlus_Core_RightNav)
|
||||
const {mutateAsync: saveNux} = useSaveNux()
|
||||
const purchases = usePurchases()
|
||||
const [hideNux, setHideNux] = React.useState(false)
|
||||
const bskyPlusNux = React.useMemo(() => {
|
||||
const purchasesReady = purchases.status === 'ready'
|
||||
const nuxReady = nux.status === 'ready'
|
||||
const ready = purchasesReady && nuxReady
|
||||
const subscribed =
|
||||
purchasesReady &&
|
||||
purchases.entitlements.some(e => e.id === EntitlementId.Core)
|
||||
const nuxSeen = Boolean(nuxReady && nux.nux)
|
||||
const active = !ready
|
||||
? false
|
||||
: subscribed || nuxSeen
|
||||
? false
|
||||
: hideNux
|
||||
? false
|
||||
: true
|
||||
return {
|
||||
active,
|
||||
nuxSeen,
|
||||
}
|
||||
}, [hideNux, nux, purchases])
|
||||
|
||||
const onPressBlueskyPlusNux = () => {
|
||||
setHideNux(true)
|
||||
bskyPlusDialogControl.open()
|
||||
saveNux({
|
||||
id: Nux.BlueskyPlus_Core_RightNav,
|
||||
completed: true,
|
||||
data: undefined,
|
||||
}).catch(_e => {})
|
||||
}
|
||||
|
||||
const {isTablet} = useWebMediaQueries()
|
||||
if (isTablet) {
|
||||
@@ -74,168 +28,122 @@ export function DesktopRightNav({routeName}: {routeName: string}) {
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
web({
|
||||
position: 'fixed',
|
||||
left: 'calc(50vw + 300px + 20px)',
|
||||
width: 300,
|
||||
maxHeight: '100%',
|
||||
overflowY: 'auto',
|
||||
}),
|
||||
]}>
|
||||
<View style={[a.py_xl]}>
|
||||
{routeName !== 'Search' && (
|
||||
<View style={[a.pb_lg]}>
|
||||
<DesktopSearch />
|
||||
</View>
|
||||
)}
|
||||
{hasSession && (
|
||||
<>
|
||||
<ProgressGuideList style={[a.mt_xl, a.mb_sm]} />
|
||||
<View
|
||||
style={[
|
||||
a.pb_lg,
|
||||
a.mb_lg,
|
||||
a.border_b,
|
||||
t.atoms.border_contrast_low,
|
||||
]}>
|
||||
<DesktopFeeds />
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
{bskyPlusNux.active ? (
|
||||
<View style={[a.pb_lg]}>
|
||||
<Button
|
||||
onPress={onPressBlueskyPlusNux}
|
||||
label={_(msg`Subscribe to Bluesky Plus`)}
|
||||
style={[a.p_lg, a.overflow_hidden, a.rounded_md]}>
|
||||
{({hovered}) => (
|
||||
<>
|
||||
<GradientFill gradient={tokens.gradients.nordic} />
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
t.atoms.bg,
|
||||
{
|
||||
borderRadius: 8,
|
||||
top: a.pt_xs.paddingTop,
|
||||
bottom: a.pt_xs.paddingTop,
|
||||
left: a.pt_xs.paddingTop,
|
||||
right: a.pt_xs.paddingTop,
|
||||
},
|
||||
]}>
|
||||
{hovered && (
|
||||
<GradientFill
|
||||
gradient={tokens.gradients.nordic}
|
||||
style={{opacity: 0.1}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={[a.flex_1, a.relative, a.z_10, a.gap_sm]}>
|
||||
<View style={[a.flex_row, a.justify_between]}>
|
||||
<BlueskyPlusLogo width={100} gradient="nordic" />
|
||||
<SquareArrowTopRight
|
||||
fill={t.atoms.text_contrast_low.color}
|
||||
size="md"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Divider />
|
||||
|
||||
<View style={[a.flex_row, a.justify_between]}>
|
||||
<Text
|
||||
style={[
|
||||
a.text_sm,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
<Trans>Subscribe now</Trans>
|
||||
</Text>
|
||||
<Text style={[a.text_sm, a.font_heavy, a.leading_snug]}>
|
||||
<Trans>$8 / month</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<View style={[a.px_xl, styles.rightNav]}>
|
||||
<View style={{paddingVertical: 20}}>
|
||||
{routeName === 'Search' ? (
|
||||
<View style={{marginBottom: 18}}>
|
||||
<DesktopFeeds />
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
{bskyPlusNux.nuxSeen && IS_DEV && (
|
||||
<View
|
||||
style={[
|
||||
a.rounded_sm,
|
||||
a.p_md,
|
||||
a.border,
|
||||
a.mb_lg,
|
||||
t.atoms.border_contrast_low,
|
||||
]}>
|
||||
<Text style={[a.font_bold]}>
|
||||
Dev only:{' '}
|
||||
<InlineLinkText
|
||||
label="Dev only reset NUX"
|
||||
{...createStaticClick(() => {
|
||||
setHideNux(false)
|
||||
resetNuxs([Nux.BlueskyPlus_Core_RightNav])
|
||||
})}>
|
||||
reset Bluesky+ NUX
|
||||
</InlineLinkText>
|
||||
</Text>
|
||||
</View>
|
||||
<DesktopSearch />
|
||||
|
||||
{hasSession && (
|
||||
<>
|
||||
<ProgressGuideList style={[{marginTop: 22, marginBottom: 8}]} />
|
||||
<View style={[pal.border, styles.desktopFeedsContainer]}>
|
||||
<DesktopFeeds />
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<BlueskyPlusCore control={bskyPlusDialogControl} />
|
||||
|
||||
<Text style={[a.leading_snug, t.atoms.text_contrast_low]}>
|
||||
{hasSession && (
|
||||
<>
|
||||
<InlineLinkText
|
||||
to={FEEDBACK_FORM_URL({
|
||||
email: currentAccount?.email,
|
||||
handle: currentAccount?.handle,
|
||||
})}
|
||||
label={_(msg`Feedback`)}>
|
||||
{_(msg`Feedback`)}
|
||||
</InlineLinkText>
|
||||
{' • '}
|
||||
</>
|
||||
<View
|
||||
style={[
|
||||
styles.message,
|
||||
{
|
||||
paddingTop: hasSession ? 0 : 18,
|
||||
},
|
||||
]}>
|
||||
<View style={[{flexWrap: 'wrap'}, s.flexRow, a.gap_xs]}>
|
||||
{hasSession && (
|
||||
<>
|
||||
<TextLink
|
||||
type="md"
|
||||
style={pal.link}
|
||||
href={FEEDBACK_FORM_URL({
|
||||
email: currentAccount?.email,
|
||||
handle: currentAccount?.handle,
|
||||
})}
|
||||
text={_(msg`Feedback`)}
|
||||
/>
|
||||
<Text type="md" style={pal.textLight}>
|
||||
·
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
<TextLink
|
||||
type="md"
|
||||
style={pal.link}
|
||||
href="https://bsky.social/about/support/privacy-policy"
|
||||
text={_(msg`Privacy`)}
|
||||
/>
|
||||
<Text type="md" style={pal.textLight}>
|
||||
·
|
||||
</Text>
|
||||
<TextLink
|
||||
type="md"
|
||||
style={pal.link}
|
||||
href="https://bsky.social/about/support/tos"
|
||||
text={_(msg`Terms`)}
|
||||
/>
|
||||
<Text type="md" style={pal.textLight}>
|
||||
·
|
||||
</Text>
|
||||
<TextLink
|
||||
type="md"
|
||||
style={pal.link}
|
||||
href={HELP_DESK_URL}
|
||||
text={_(msg`Help`)}
|
||||
/>
|
||||
</View>
|
||||
{kawaii && (
|
||||
<Text type="md" style={[pal.textLight, {marginTop: 12}]}>
|
||||
<Trans>
|
||||
Logo by{' '}
|
||||
<TextLink
|
||||
type="md"
|
||||
href="/profile/sawaratsuki.bsky.social"
|
||||
text="@sawaratsuki.bsky.social"
|
||||
style={pal.link}
|
||||
/>
|
||||
</Trans>
|
||||
</Text>
|
||||
)}
|
||||
<InlineLinkText
|
||||
to="https://bsky.social/about/support/privacy-policy"
|
||||
label={_(msg`Privacy`)}>
|
||||
{_(msg`Privacy`)}
|
||||
</InlineLinkText>
|
||||
{' • '}
|
||||
<InlineLinkText
|
||||
to="https://bsky.social/about/support/tos"
|
||||
label={_(msg`Terms`)}>
|
||||
{_(msg`Terms`)}
|
||||
</InlineLinkText>
|
||||
{' • '}
|
||||
<InlineLinkText label={_(msg`Help`)} to={HELP_DESK_URL}>
|
||||
{_(msg`Help`)}
|
||||
</InlineLinkText>
|
||||
</Text>
|
||||
|
||||
{kawaii && (
|
||||
<Text style={[t.atoms.text_contrast_medium, {marginTop: 12}]}>
|
||||
<Trans>
|
||||
Logo by{' '}
|
||||
<InlineLinkText
|
||||
label={_(msg`Logo by @sawaratsuki.bsky.social`)}
|
||||
to="/profile/sawaratsuki.bsky.social">
|
||||
@sawaratsuki.bsky.social
|
||||
</InlineLinkText>
|
||||
</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
rightNav: {
|
||||
// @ts-ignore web only
|
||||
position: 'fixed',
|
||||
// @ts-ignore web only
|
||||
left: '50%',
|
||||
transform: [
|
||||
{
|
||||
translateX: 300,
|
||||
},
|
||||
...a.scrollbar_offset.transform,
|
||||
],
|
||||
maxHeight: '100%',
|
||||
overflowY: 'auto',
|
||||
},
|
||||
|
||||
message: {
|
||||
paddingVertical: 18,
|
||||
paddingHorizontal: 12,
|
||||
},
|
||||
messageLine: {
|
||||
marginBottom: 10,
|
||||
},
|
||||
desktopFeedsContainer: {
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
marginTop: 18,
|
||||
marginBottom: 18,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -3,10 +3,10 @@ import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
import {RemoveScrollBar} from 'react-remove-scroll-bar'
|
||||
|
||||
import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle'
|
||||
import {useIntentHandler} from '#/lib/hooks/useIntentHandler'
|
||||
import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
import {colors} from '#/lib/styles'
|
||||
@@ -34,7 +34,6 @@ function ShellInner() {
|
||||
const {_} = useLingui()
|
||||
const showDrawer = !isDesktop && isDrawerOpen
|
||||
|
||||
useWebBodyScrollLock(showDrawer)
|
||||
useComposerKeyboardShortcut()
|
||||
useIntentHandler()
|
||||
|
||||
@@ -58,31 +57,34 @@ function ShellInner() {
|
||||
<PortalOutlet />
|
||||
|
||||
{showDrawer && (
|
||||
<TouchableWithoutFeedback
|
||||
onPress={ev => {
|
||||
// Only close if press happens outside of the drawer
|
||||
if (ev.target === ev.currentTarget) {
|
||||
setDrawerOpen(false)
|
||||
}
|
||||
}}
|
||||
accessibilityLabel={_(msg`Close navigation footer`)}
|
||||
accessibilityHint={_(msg`Closes bottom navigation bar`)}>
|
||||
<View
|
||||
style={[
|
||||
styles.drawerMask,
|
||||
{
|
||||
backgroundColor: select(t.name, {
|
||||
light: 'rgba(0, 57, 117, 0.1)',
|
||||
dark: 'rgba(1, 82, 168, 0.1)',
|
||||
dim: 'rgba(10, 13, 16, 0.8)',
|
||||
}),
|
||||
},
|
||||
]}>
|
||||
<View style={styles.drawerContainer}>
|
||||
<DrawerContent />
|
||||
<>
|
||||
<RemoveScrollBar />
|
||||
<TouchableWithoutFeedback
|
||||
onPress={ev => {
|
||||
// Only close if press happens outside of the drawer
|
||||
if (ev.target === ev.currentTarget) {
|
||||
setDrawerOpen(false)
|
||||
}
|
||||
}}
|
||||
accessibilityLabel={_(msg`Close navigation footer`)}
|
||||
accessibilityHint={_(msg`Closes bottom navigation bar`)}>
|
||||
<View
|
||||
style={[
|
||||
styles.drawerMask,
|
||||
{
|
||||
backgroundColor: select(t.name, {
|
||||
light: 'rgba(0, 57, 117, 0.1)',
|
||||
dark: 'rgba(1, 82, 168, 0.1)',
|
||||
dim: 'rgba(10, 13, 16, 0.8)',
|
||||
}),
|
||||
},
|
||||
]}>
|
||||
<View style={styles.drawerContainer}>
|
||||
<DrawerContent />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</TouchableWithoutFeedback>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user