Live video on the AT Protocol

Merge pull request #641 from streamplace/natb/a-href-in-sidebar

fix: add proper web link behavior in sidebar

authored by natalie and committed by GitHub 481326da c657e0e5

+81 -26
+19
js/app/components/aqlink.tsx
··· 2 2 Link, 3 3 NavigationProp, 4 4 ParamListBase, 5 + useLinkBuilder, 5 6 useNavigation, 6 7 } from "@react-navigation/native"; 7 8 import usePlatform from "hooks/usePlatform"; ··· 54 55 }, []); 55 56 return <Loading />; 56 57 } 58 + 59 + // generates the proper href for a given LinkParams object, for better web support 60 + export function useAQLinkHref(to: LinkParams): { href?: string } { 61 + const { isWeb } = usePlatform(); 62 + const buildLink = useLinkBuilder(); 63 + 64 + if (!isWeb) { 65 + return { href: undefined }; 66 + } 67 + 68 + try { 69 + const href = buildLink(to.screen, to.params); 70 + return { href }; 71 + } catch (e) { 72 + console.warn("Failed to build link for", to, e); 73 + return { href: undefined }; 74 + } 75 + }
+29 -11
js/app/components/sidebar/sidebar-item.tsx
··· 1 - import { Text, useTheme } from "@streamplace/components"; 1 + import { DrawerNavigationState, ParamListBase } from "@react-navigation/native"; 2 + import { Text, useTheme, zero } from "@streamplace/components"; 3 + import { useAQLinkHref } from "components/aqlink"; 2 4 import React, { ReactNode, useState } from "react"; 3 5 import { 6 + GestureResponderEvent, 7 + Pressable, 4 8 PressableStateCallbackType, 5 9 StyleProp, 6 10 View, 7 11 ViewStyle, 8 12 } from "react-native"; 9 - import { Pressable } from "react-native-gesture-handler"; 10 13 11 14 export default function SidebarItem({ 12 15 icon, ··· 14 17 collapsed, 15 18 active, 16 19 onPress, 20 + route, 17 21 style = null, 18 22 tint = "rgba(189, 110, 134)", 19 23 }: { ··· 24 28 label: string | ReactNode; 25 29 collapsed: boolean; 26 30 active: boolean; 27 - onPress: () => void; 31 + onPress: (event: GestureResponderEvent) => void; 32 + route?: DrawerNavigationState<ParamListBase>["routes"][number]; 28 33 style?: 29 34 | StyleProp<ViewStyle> 30 35 | ((state: PressableStateCallbackType) => StyleProp<ViewStyle>); ··· 32 37 }) { 33 38 const [hover, setHover] = useState<boolean>(false); 34 39 const theme = useTheme(); 40 + const { href } = useAQLinkHref({ 41 + screen: route?.name || "Home", 42 + params: route?.params as any, 43 + }); 35 44 36 45 // Handle different icon types - component, JSX element, or function returning JSX 37 46 const renderIcon = () => { ··· 62 71 } 63 72 64 73 // Fallback 65 - console.log("tried to render item, but couldn't", (icon as any).$$typeof); 74 + console.log( 75 + "tried to render item for route", 76 + label, 77 + href, 78 + ", but couldn't", 79 + (icon as any).$$typeof, 80 + ); 66 81 67 82 return <Text>📄</Text>; 68 83 }; ··· 73 88 style={style} 74 89 onHoverIn={() => setHover(true)} 75 90 onHoverOut={() => setHover(false)} 91 + role="link" 92 + accessibilityLabel={typeof label === "string" ? label : "Link to " + href} 93 + // @ts-ignore This makes it render as <a> on web! 94 + href={href} 76 95 > 77 96 <View 78 97 style={[ 98 + zero.r.md, 99 + zero.layout.flex.row, 100 + zero.layout.flex.alignCenter, 101 + zero.px[3], 102 + zero.gap.all[2], 79 103 { 80 104 backgroundColor: 81 105 hover || active ··· 84 108 ", " + (active && !hover ? "0.1" : "0.25") + ")", 85 109 ) 86 110 : undefined, 87 - borderRadius: 12, 88 - flexDirection: "row", 89 - justifyContent: "flex-start", 90 - alignItems: "center", 91 - paddingHorizontal: 12, 92 - gap: 8, 93 111 overflow: "hidden", 94 112 }, 95 113 ]} 96 114 > 97 - <View style={[{ width: 32, paddingVertical: 12 }]}>{renderIcon()}</View> 115 + <View style={[zero.w[8], zero.py[3]]}>{renderIcon()}</View> 98 116 {!collapsed && ( 99 117 <View 100 118 style={[
+33 -15
js/app/components/sidebar/sidebar.tsx
··· 6 6 ParamListBase, 7 7 useNavigation, 8 8 } from "@react-navigation/native"; 9 - import { Text, useTheme } from "@streamplace/components"; 9 + import { Text, zero } from "@streamplace/components"; 10 10 import React from "react"; 11 11 import { Image, Platform, View } from "react-native"; 12 12 import Animated, { ··· 44 44 widthAnim, 45 45 externalItems = [], 46 46 }: SidebarProps) { 47 - const theme = useTheme(); 48 47 const navigation = useNavigation(); 49 - // Apply the defined type to the component props 50 48 const animatedSidebarStyle = useAnimatedStyle(() => { 51 49 return { 52 50 minWidth: widthAnim.value, ··· 61 59 return ( 62 60 <Animated.View 63 61 style={[ 64 - animatedSidebarStyle, // Apply the animated style 65 - { padding: 8, gap: 8, flexDirection: "column" }, 62 + animatedSidebarStyle, 63 + zero.p[2], 64 + zero.gap.all[2], 65 + zero.layout.flex.column, 66 66 ]} 67 67 > 68 68 <View 69 69 style={[ 70 + zero.layout.flex.row, 71 + zero.layout.flex.alignCenter, 72 + zero.gap.all[3], 70 73 { 71 - marginTop: Platform.OS === "ios" ? 29 : 12, 74 + marginTop: Platform.OS === "ios" ? 29 : 8, 72 75 marginBottom: 20, 73 - paddingLeft: 10, 74 - gap: 12, 75 - flexDirection: "row", 76 - justifyContent: "flex-start", 77 - alignItems: "center", 76 + paddingLeft: 11, 78 77 }, 79 78 ]} 80 79 > 81 80 <Image 82 81 source={require("../../assets/images/cube.png")} 83 - style={[{ height: 30, width: 28 }]} 82 + height={30} 83 + width={28} 84 + style={{ width: 28, height: 30, resizeMode: "contain" }} 84 85 /> 85 86 {!collapsed && <Text size="2xl">Streamplace</Text>} 86 87 </View> ··· 98 99 | React.ComponentType<any> 99 100 | undefined; 100 101 102 + // if we have style display: none on the drawer item, completely skip rendering it 103 + const drawerItemStyle = options.drawerItemStyle; 104 + let isHidden = false; 105 + if ( 106 + drawerItemStyle && 107 + typeof drawerItemStyle === "object" && 108 + "display" in drawerItemStyle && 109 + (drawerItemStyle as any).display === "none" 110 + ) { 111 + isHidden = true; 112 + } 113 + if (isHidden) { 114 + return null; 115 + } 116 + 101 117 return ( 102 118 <SidebarItem 103 119 key={route.key} ··· 105 121 label={label} 106 122 active={descriptor.navigation.isFocused()} 107 123 collapsed={collapsed} 108 - onPress={() => { 124 + route={route} 125 + onPress={(ev) => { 126 + ev.preventDefault(); 109 127 if (route.name === "Home") { 110 - // copy logic for 'Home' to reset the stack 128 + // reset the stack (b/c streamlist is in the same stack as home) 111 129 navigation.dispatch( 112 130 CommonActions.reset({ 113 131 index: 0, ··· 126 144 } 127 145 }} 128 146 style={options.drawerItemStyle} 129 - tint={options.drawerActiveTintColor as string} // Assuming tint is a string color or undefined 147 + tint={options.drawerActiveTintColor as string} 130 148 /> 131 149 ); 132 150 })}