diff options
Diffstat (limited to 'src/view')
-rw-r--r-- | src/view/com/composer/Composer.tsx | 11 | ||||
-rw-r--r-- | src/view/com/feeds/FeedPage.tsx | 70 | ||||
-rw-r--r-- | src/view/com/home/HomeHeader.tsx | 11 | ||||
-rw-r--r-- | src/view/com/home/HomeHeaderLayout.web.tsx | 46 | ||||
-rw-r--r-- | src/view/com/home/HomeHeaderLayoutMobile.tsx | 1 | ||||
-rw-r--r-- | src/view/com/pager/TabBar.tsx | 78 | ||||
-rw-r--r-- | src/view/com/post-thread/PostThreadItem.tsx | 2 | ||||
-rw-r--r-- | src/view/com/posts/FeedItem.tsx | 2 | ||||
-rw-r--r-- | src/view/com/util/MainScrollProvider.tsx | 16 | ||||
-rw-r--r-- | src/view/com/util/forms/NativeDropdown.tsx | 3 | ||||
-rw-r--r-- | src/view/com/util/forms/NativeDropdown.web.tsx | 7 | ||||
-rw-r--r-- | src/view/screens/Storybook/Typography.tsx | 6 | ||||
-rw-r--r-- | src/view/shell/Composer.tsx | 2 | ||||
-rw-r--r-- | src/view/shell/Composer.web.tsx | 3 |
14 files changed, 162 insertions, 96 deletions
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 1ed6b98a5..2855d4232 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -71,6 +71,8 @@ export const ComposePost = observer(function ComposePost({ quote: initQuote, mention: initMention, openPicker, + text: initText, + imageUris: initImageUris, }: Props) { const {currentAccount} = useSession() const {data: currentProfile} = useProfileQuery({did: currentAccount!.did}) @@ -91,7 +93,9 @@ export const ComposePost = observer(function ComposePost({ const [error, setError] = useState('') const [richtext, setRichText] = useState( new RichText({ - text: initMention + text: initText + ? initText + : initMention ? insertMentionAt( `@${initMention}`, initMention.length + 1, @@ -110,7 +114,10 @@ export const ComposePost = observer(function ComposePost({ const [labels, setLabels] = useState<string[]>([]) const [threadgate, setThreadgate] = useState<ThreadgateSetting[]>([]) const [suggestedLinks, setSuggestedLinks] = useState<Set<string>>(new Set()) - const gallery = useMemo(() => new GalleryModel(), []) + const gallery = useMemo( + () => new GalleryModel(initImageUris), + [initImageUris], + ) const onClose = useCallback(() => { closeComposer() }, [closeComposer]) diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx index 2aacdb89d..e6b5d1fb6 100644 --- a/src/view/com/feeds/FeedPage.tsx +++ b/src/view/com/feeds/FeedPage.tsx @@ -1,30 +1,24 @@ import React from 'react' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' import {useNavigation} from '@react-navigation/native' import {useAnalytics} from 'lib/analytics/analytics' import {useQueryClient} from '@tanstack/react-query' import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' import {MainScrollProvider} from '../util/MainScrollProvider' -import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useSetMinimalShellMode} from '#/state/shell' import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed' import {ComposeIcon2} from 'lib/icons' -import {colors, s} from 'lib/styles' +import {s} from 'lib/styles' import {View, useWindowDimensions} from 'react-native' import {ListMethods} from '../util/List' import {Feed} from '../posts/Feed' -import {TextLink} from '../util/Link' import {FAB} from '../util/fab/FAB' import {LoadLatestBtn} from '../util/load-latest/LoadLatestBtn' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useSession} from '#/state/session' import {useComposerControls} from '#/state/shell/composer' -import {listenSoftReset, emitSoftReset} from '#/state/events' +import {listenSoftReset} from '#/state/events' import {truncateAndInvalidate} from '#/state/queries/util' import {TabState, getTabState, getRootNavigation} from '#/lib/routes/helpers' import {isNative} from '#/platform/detection' @@ -47,10 +41,8 @@ export function FeedPage({ renderEndOfFeed?: () => JSX.Element }) { const {hasSession} = useSession() - const pal = usePalette('default') const {_} = useLingui() const navigation = useNavigation() - const {isDesktop} = useWebMediaQueries() const queryClient = useQueryClient() const {openComposer} = useComposerControls() const [isScrolledDown, setIsScrolledDown] = React.useState(false) @@ -99,63 +91,6 @@ export function FeedPage({ setHasNew(false) }, [scrollToTop, feed, queryClient, setHasNew]) - const ListHeaderComponent = React.useCallback(() => { - if (isDesktop) { - return ( - <View - style={[ - pal.view, - { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingHorizontal: 18, - paddingVertical: 12, - }, - ]}> - <TextLink - type="title-lg" - href="/" - style={[pal.text, {fontWeight: 'bold'}]} - text={ - <> - Bluesky{' '} - {hasNew && ( - <View - style={{ - top: -8, - backgroundColor: colors.blue3, - width: 8, - height: 8, - borderRadius: 4, - }} - /> - )} - </> - } - onPress={emitSoftReset} - /> - {hasSession && ( - <TextLink - type="title-lg" - href="/settings/following-feed" - style={{fontWeight: 'bold'}} - accessibilityLabel={_(msg`Feed Preferences`)} - accessibilityHint="" - text={ - <FontAwesomeIcon - icon="sliders" - style={pal.textLight as FontAwesomeIconStyle} - /> - } - /> - )} - </View> - ) - } - return <></> - }, [isDesktop, pal.view, pal.text, pal.textLight, hasNew, _, hasSession]) - return ( <View testID={testID} style={s.h100pct}> <MainScrollProvider> @@ -171,7 +106,6 @@ export function FeedPage({ onHasNew={setHasNew} renderEmptyState={renderEmptyState} renderEndOfFeed={renderEndOfFeed} - ListHeaderComponent={ListHeaderComponent} headerOffset={headerOffset} /> </MainScrollProvider> diff --git a/src/view/com/home/HomeHeader.tsx b/src/view/com/home/HomeHeader.tsx index 5ffa31f39..3df3858ba 100644 --- a/src/view/com/home/HomeHeader.tsx +++ b/src/view/com/home/HomeHeader.tsx @@ -1,7 +1,6 @@ import React from 'react' import {RenderTabBarFnProps} from 'view/com/pager/Pager' import {HomeHeaderLayout} from './HomeHeaderLayout' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {usePinnedFeedsInfos} from '#/state/queries/feed' import {useNavigation} from '@react-navigation/native' import {NavigationProp} from 'lib/routes/types' @@ -12,16 +11,6 @@ import {usePalette} from '#/lib/hooks/usePalette' export function HomeHeader( props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void}, ) { - const {isDesktop} = useWebMediaQueries() - if (isDesktop) { - return null - } - return <HomeHeaderInner {...props} /> -} - -export function HomeHeaderInner( - props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void}, -) { const navigation = useNavigation<NavigationProp>() const {feeds, hasPinnedCustom} = usePinnedFeedsInfos() const pal = usePalette('default') diff --git a/src/view/com/home/HomeHeaderLayout.web.tsx b/src/view/com/home/HomeHeaderLayout.web.tsx index 47cb00235..fbb55e6bc 100644 --- a/src/view/com/home/HomeHeaderLayout.web.tsx +++ b/src/view/com/home/HomeHeaderLayout.web.tsx @@ -1,11 +1,20 @@ import React from 'react' -import {StyleSheet} from 'react-native' +import {StyleSheet, View} from 'react-native' import Animated from 'react-native-reanimated' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {HomeHeaderLayoutMobile} from './HomeHeaderLayoutMobile' import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' import {useShellLayout} from '#/state/shell/shell-layout' +import {Logo} from '#/view/icons/Logo' +import {Link, TextLink} from '../util/Link' +import { + FontAwesomeIcon, + FontAwesomeIconStyle, +} from '@fortawesome/react-native-fontawesome' +import {useLingui} from '@lingui/react' +import {msg} from '@lingui/macro' +import {CogIcon} from '#/lib/icons' export function HomeHeaderLayout({children}: {children: React.ReactNode}) { const {isMobile} = useWebMediaQueries() @@ -20,6 +29,7 @@ function HomeHeaderLayoutTablet({children}: {children: React.ReactNode}) { const pal = usePalette('default') const {headerMinimalShellTransform} = useMinimalShellMode() const {headerHeight} = useShellLayout() + const {_} = useLingui() return ( // @ts-ignore the type signature for transform wrong here, translateX and translateY need to be in separate objects -prf @@ -28,12 +38,44 @@ function HomeHeaderLayoutTablet({children}: {children: React.ReactNode}) { onLayout={e => { headerHeight.value = e.nativeEvent.layout.height }}> + <View style={[pal.view, styles.topBar]}> + <TextLink + type="title-lg" + href="/settings/following-feed" + accessibilityLabel={_(msg`Following Feed Preferences`)} + accessibilityHint="" + text={ + <FontAwesomeIcon + icon="sliders" + style={pal.textLight as FontAwesomeIconStyle} + /> + } + /> + <Logo width={28} /> + <Link + href="/settings/saved-feeds" + hitSlop={10} + accessibilityRole="button" + accessibilityLabel={_(msg`Edit Saved Feeds`)} + accessibilityHint={_(msg`Opens screen to edit Saved Feeds`)}> + <CogIcon size={22} strokeWidth={2} style={pal.textLight} /> + </Link> + </View> {children} </Animated.View> ) } const styles = StyleSheet.create({ + topBar: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: 18, + paddingVertical: 8, + marginTop: 8, + width: '100%', + }, tabBar: { // @ts-ignore Web only position: 'sticky', @@ -42,7 +84,7 @@ const styles = StyleSheet.create({ left: 'calc(50% - 300px)', width: 600, top: 0, - flexDirection: 'row', + flexDirection: 'column', alignItems: 'center', borderLeftWidth: 1, borderRightWidth: 1, diff --git a/src/view/com/home/HomeHeaderLayoutMobile.tsx b/src/view/com/home/HomeHeaderLayoutMobile.tsx index 6c4b911f0..f51efb7b4 100644 --- a/src/view/com/home/HomeHeaderLayoutMobile.tsx +++ b/src/view/com/home/HomeHeaderLayoutMobile.tsx @@ -103,7 +103,6 @@ const styles = StyleSheet.create({ right: 0, top: 0, flexDirection: 'column', - borderBottomWidth: 1, }, topBar: { flexDirection: 'row', diff --git a/src/view/com/pager/TabBar.tsx b/src/view/com/pager/TabBar.tsx index 3204bb23e..ff8acd60c 100644 --- a/src/view/com/pager/TabBar.tsx +++ b/src/view/com/pager/TabBar.tsx @@ -5,6 +5,7 @@ import {PressableWithHover} from '../util/PressableWithHover' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {DraggableScrollView} from './DraggableScrollView' +import {isNative} from '#/platform/detection' export interface TabBarProps { testID?: string @@ -15,6 +16,10 @@ export interface TabBarProps { onPressSelected?: (index: number) => void } +// How much of the previous/next item we're showing +// to give the user a hint there's more to scroll. +const OFFSCREEN_ITEM_WIDTH = 20 + export function TabBar({ testID, selectedPage, @@ -25,6 +30,7 @@ export function TabBar({ }: TabBarProps) { const pal = usePalette('default') const scrollElRef = useRef<ScrollView>(null) + const itemRefs = useRef<Array<Element>>([]) const [itemXs, setItemXs] = useState<number[]>([]) const indicatorStyle = useMemo( () => ({borderBottomColor: indicatorColor || pal.colors.link}), @@ -33,12 +39,58 @@ export function TabBar({ const {isDesktop, isTablet} = useWebMediaQueries() const styles = isDesktop || isTablet ? desktopStyles : mobileStyles - // scrolls to the selected item when the page changes useEffect(() => { - scrollElRef.current?.scrollTo({ - x: - (itemXs[selectedPage] || 0) - styles.contentContainer.paddingHorizontal, - }) + if (isNative) { + // On native, the primary interaction is swiping. + // We adjust the scroll little by little on every tab change. + // Scroll into view but keep the end of the previous item visible. + let x = itemXs[selectedPage] || 0 + x = Math.max(0, x - OFFSCREEN_ITEM_WIDTH) + scrollElRef.current?.scrollTo({x}) + } else { + // On the web, the primary interaction is tapping. + // Scrolling under tap feels disorienting so only adjust the scroll offset + // when tapping on an item out of view--and we adjust by almost an entire page. + const parent = scrollElRef?.current?.getScrollableNode?.() + if (!parent) { + return + } + const parentRect = parent.getBoundingClientRect() + if (!parentRect) { + return + } + const { + left: parentLeft, + right: parentRight, + width: parentWidth, + } = parentRect + const child = itemRefs.current[selectedPage] + if (!child) { + return + } + const childRect = child.getBoundingClientRect?.() + if (!childRect) { + return + } + const {left: childLeft, right: childRight, width: childWidth} = childRect + let dx = 0 + if (childRight >= parentRight) { + dx += childRight - parentRight + dx += parentWidth - childWidth - OFFSCREEN_ITEM_WIDTH + } else if (childLeft <= parentLeft) { + dx -= parentLeft - childLeft + dx -= parentWidth - childWidth - OFFSCREEN_ITEM_WIDTH + } + let x = parent.scrollLeft + dx + x = Math.max(0, x) + x = Math.min(x, parent.scrollWidth - parentWidth) + if (dx !== 0) { + parent.scroll({ + left: x, + behavior: 'smooth', + }) + } + } }, [scrollElRef, itemXs, selectedPage, styles]) const onPressItem = useCallback( @@ -78,6 +130,7 @@ export function TabBar({ <PressableWithHover testID={`${testID}-selector-${i}`} key={`${item}-${i}`} + ref={node => (itemRefs.current[i] = node)} onLayout={e => onItemLayout(e, i)} style={styles.item} hoverStyle={pal.viewLight} @@ -94,6 +147,7 @@ export function TabBar({ ) })} </DraggableScrollView> + <View style={[pal.border, styles.outerBottomBorder]} /> </View> ) } @@ -117,6 +171,13 @@ const desktopStyles = StyleSheet.create({ borderBottomWidth: 3, borderBottomColor: 'transparent', }, + outerBottomBorder: { + position: 'absolute', + left: 0, + right: 0, + bottom: -1, + borderBottomWidth: 1, + }, }) const mobileStyles = StyleSheet.create({ @@ -137,4 +198,11 @@ const mobileStyles = StyleSheet.create({ borderBottomWidth: 3, borderBottomColor: 'transparent', }, + outerBottomBorder: { + position: 'absolute', + left: 0, + right: 0, + bottom: -1, + borderBottomWidth: 1, + }, }) diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 0d50104cd..e0baabed9 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -94,6 +94,8 @@ export function PostThreadItem({ if (richText && moderation) { return ( <PostThreadItemLoaded + // Safeguard from clobbering per-post state below: + key={postShadowed.uri} post={postShadowed} prevPost={prevPost} nextPost={nextPost} diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index 47a964ab1..7d29703e2 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -70,6 +70,8 @@ export function FeedItem({ if (richText && moderation) { return ( <FeedItemInner + // Safeguard from clobbering per-post state below: + key={postShadowed.uri} post={postShadowed} record={record} reason={reason} diff --git a/src/view/com/util/MainScrollProvider.tsx b/src/view/com/util/MainScrollProvider.tsx index 2c90e33ff..01b8a954d 100644 --- a/src/view/com/util/MainScrollProvider.tsx +++ b/src/view/com/util/MainScrollProvider.tsx @@ -20,12 +20,14 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) { const setMode = useSetMinimalShellMode() const startDragOffset = useSharedValue<number | null>(null) const startMode = useSharedValue<number | null>(null) + const didJustRestoreScroll = useSharedValue<boolean>(false) useEffect(() => { if (isWeb) { return listenToForcedWindowScroll(() => { startDragOffset.value = null startMode.value = null + didJustRestoreScroll.value = true }) } }) @@ -86,6 +88,11 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) { mode.value = newValue } } else { + if (didJustRestoreScroll.value) { + didJustRestoreScroll.value = false + // Don't hide/show navbar based on scroll restoratoin. + return + } // On the web, we don't try to follow the drag because we don't know when it ends. // Instead, show/hide immediately based on whether we're scrolling up or down. const dy = e.contentOffset.y - (startDragOffset.value ?? 0) @@ -98,7 +105,14 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) { } } }, - [headerHeight, mode, setMode, startDragOffset, startMode], + [ + headerHeight, + mode, + setMode, + startDragOffset, + startMode, + didJustRestoreScroll, + ], ) return ( diff --git a/src/view/com/util/forms/NativeDropdown.tsx b/src/view/com/util/forms/NativeDropdown.tsx index 082285064..0a47569f2 100644 --- a/src/view/com/util/forms/NativeDropdown.tsx +++ b/src/view/com/util/forms/NativeDropdown.tsx @@ -1,7 +1,7 @@ import React from 'react' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import * as DropdownMenu from 'zeego/dropdown-menu' -import {Pressable, StyleSheet, Platform, View} from 'react-native' +import {Pressable, StyleSheet, Platform, View, ViewStyle} from 'react-native' import {IconProp} from '@fortawesome/fontawesome-svg-core' import {MenuItemCommonProps} from 'zeego/lib/typescript/menu' import {usePalette} from 'lib/hooks/usePalette' @@ -151,6 +151,7 @@ type Props = { testID?: string accessibilityLabel?: string accessibilityHint?: string + triggerStyle?: ViewStyle } /* The `NativeDropdown` function uses native iOS and Android dropdown menus. diff --git a/src/view/com/util/forms/NativeDropdown.web.tsx b/src/view/com/util/forms/NativeDropdown.web.tsx index 052e7ca13..6abeb16cc 100644 --- a/src/view/com/util/forms/NativeDropdown.web.tsx +++ b/src/view/com/util/forms/NativeDropdown.web.tsx @@ -1,7 +1,7 @@ import React from 'react' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import * as DropdownMenu from '@radix-ui/react-dropdown-menu' -import {Pressable, StyleSheet, View, Text} from 'react-native' +import {Pressable, StyleSheet, View, Text, ViewStyle} from 'react-native' import {IconProp} from '@fortawesome/fontawesome-svg-core' import {MenuItemCommonProps} from 'zeego/lib/typescript/menu' import {usePalette} from 'lib/hooks/usePalette' @@ -53,6 +53,7 @@ type Props = { testID?: string accessibilityLabel?: string accessibilityHint?: string + triggerStyle?: ViewStyle } export function NativeDropdown({ @@ -61,6 +62,7 @@ export function NativeDropdown({ testID, accessibilityLabel, accessibilityHint, + triggerStyle, }: React.PropsWithChildren<Props>) { const pal = usePalette('default') const theme = useTheme() @@ -120,7 +122,8 @@ export function NativeDropdown({ accessibilityLabel={accessibilityLabel} accessibilityHint={accessibilityHint} onPress={() => setOpen(o => !o)} - hitSlop={HITSLOP_10}> + hitSlop={HITSLOP_10} + style={triggerStyle}> {children} </Pressable> </DropdownMenu.Trigger> diff --git a/src/view/screens/Storybook/Typography.tsx b/src/view/screens/Storybook/Typography.tsx index 8ee4270b2..f0d67c528 100644 --- a/src/view/screens/Storybook/Typography.tsx +++ b/src/view/screens/Storybook/Typography.tsx @@ -22,12 +22,14 @@ export function Typography() { <Text style={[a.text_2xs]}>atoms.text_2xs</Text> <RichText - resolveFacets + // TODO: This only supports already resolved facets. + // Resolving them on read is bad anyway. value={`This is rich text. It can have mentions like @bsky.app or links like https://bsky.social`} /> <RichText selectable - resolveFacets + // TODO: This only supports already resolved facets. + // Resolving them on read is bad anyway. value={`This is rich text. It can have mentions like @bsky.app or links like https://bsky.social`} style={[a.text_xl]} /> diff --git a/src/view/shell/Composer.tsx b/src/view/shell/Composer.tsx index d37ff4fb7..1937fcb6e 100644 --- a/src/view/shell/Composer.tsx +++ b/src/view/shell/Composer.tsx @@ -55,6 +55,8 @@ export const Composer = observer(function ComposerImpl({ onPost={state.onPost} quote={state.quote} mention={state.mention} + text={state.text} + imageUris={state.imageUris} /> </Animated.View> ) diff --git a/src/view/shell/Composer.web.tsx b/src/view/shell/Composer.web.tsx index 99e659d62..00233f66a 100644 --- a/src/view/shell/Composer.web.tsx +++ b/src/view/shell/Composer.web.tsx @@ -9,7 +9,7 @@ import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock' import { EmojiPicker, EmojiPickerState, -} from 'view/com/composer/text-input/web/EmojiPicker.web.tsx' +} from 'view/com/composer/text-input/web/EmojiPicker.web' const BOTTOM_BAR_HEIGHT = 61 @@ -69,6 +69,7 @@ export function Composer({}: {winHeight: number}) { onPost={state.onPost} mention={state.mention} openPicker={onOpenPicker} + text={state.text} /> </Animated.View> <EmojiPicker state={pickerState} close={onClosePicker} /> |