Live video on the AT Protocol
79
fork

Configure Feed

Select the types of activity you want to include in your feed.

at eli/fix-context-recursion 215 lines 6.2 kB view raw
1import Viewers from "components/viewers"; 2import useStreamplaceNode from "hooks/useStreamplaceNode"; 3import { Image } from "react-native"; 4import { isWeb, Stack, Text, useMedia, View, XStack, YStack } from "tamagui"; 5 6export type StreamCardSize = "xs" | "sm" | "md" | "lg" | "xl"; 7 8interface StreamCardProps { 9 size?: StreamCardSize; 10 horizontal?: boolean; 11 thumbnailUrl: string; 12 avatarUrl?: string; 13 title?: string; 14 streamerName?: string; 15 viewers?: number; 16 category: string[]; 17 isLive?: boolean; 18} 19 20const StreamCard = ({ 21 size = "sm", 22 horizontal = false, 23 thumbnailUrl, 24 avatarUrl, 25 title, 26 streamerName, 27 viewers = 0, 28 category = [], 29 isLive = true, 30}: StreamCardProps) => { 31 const media = useMedia(); 32 33 const layoutHorizontal = horizontal; 34 const { url } = useStreamplaceNode(); 35 36 // Define dynamic styles 37 const borderRadius = 12; 38 const contentPadding = 12; 39 const avatarSize = 40; 40 const livePillHeight = 30; 41 const livePillPaddingHorizontal = 4; 42 const categoryPillHeight = 16; 43 const categoryPillPaddingHorizontal = 4; 44 45 const MainContainer = layoutHorizontal ? XStack : YStack; 46 const SubContainer = layoutHorizontal ? YStack : XStack; 47 48 const verticalContentSectionHeight = avatarSize + 2 * contentPadding; 49 const horizontalContentSectionWidth = avatarSize * 2 + contentPadding; 50 51 return ( 52 <MainContainer 53 flex={1} 54 backgroundColor="$gray3" 55 borderRadius={borderRadius} 56 overflow="hidden" 57 borderColor="#99889988" 58 borderWidth={2} 59 alignItems={layoutHorizontal ? "center" : "stretch"} 60 hoverStyle={{ 61 backgroundColor: "$gray6", 62 }} 63 > 64 {/* Thumbnail Section */} 65 <Stack 66 flex={layoutHorizontal ? 0 : undefined} 67 minWidth={layoutHorizontal ? "67%" : "100%"} 68 $gtXl={{ 69 minWidth: layoutHorizontal ? "65%" : "100%", 70 }} 71 $gtXxl={{ 72 minWidth: layoutHorizontal ? "62.5%" : "100%", 73 }} 74 // native seems to be unable to adjust widths properly? 75 maxHeight={!isWeb ? "76.5%" : "100%"} 76 borderRadius={borderRadius} 77 overflow="hidden" 78 position="relative" 79 alignSelf={layoutHorizontal ? "auto" : "center"} 80 backgroundColor="$gray6" 81 > 82 <Image 83 source={{ uri: `${url}/${thumbnailUrl}`, width: 160, height: 90 }} 84 style={{ width: "100%", height: "100%", aspectRatio: 16 / 9 }} 85 resizeMode="contain" 86 /> 87 {isLive && ( 88 <XStack 89 position="absolute" 90 top={contentPadding} 91 right={contentPadding} 92 backgroundColor="$background075" 93 borderRadius={999} 94 borderWidth={1} 95 borderColor="#7774" 96 paddingHorizontal={livePillPaddingHorizontal} 97 height={livePillHeight} 98 alignItems="center" 99 justifyContent="center" 100 gap={4} 101 > 102 <Viewers viewers={viewers} /> 103 </XStack> 104 )} 105 </Stack> 106 107 {/* Content Section */} 108 <SubContainer 109 padding={contentPadding} 110 alignItems={layoutHorizontal ? "flex-start" : "center"} 111 justifyContent="flex-end" 112 gap={contentPadding} 113 height="unset" 114 width={layoutHorizontal ? horizontalContentSectionWidth : "unset"} 115 flex={1} 116 > 117 {/* Avatar */} 118 <Stack 119 width={avatarSize} 120 height={avatarSize} 121 borderRadius={avatarSize / 2} 122 overflow="hidden" 123 flexShrink={0} 124 > 125 {/* dynamically switching between these src crashes android */} 126 {avatarUrl && ( 127 <View f={1} key="avatar"> 128 <Image 129 key="avatar" 130 source={{ 131 uri: avatarUrl, 132 }} 133 style={{ width: "100%", height: "100%" }} 134 resizeMode="cover" 135 /> 136 </View> 137 )} 138 {!avatarUrl && ( 139 <View key="avatar-placeholder"> 140 <Image 141 key="avatar" 142 source={require("./../../assets/images/goose.png")} 143 style={{ width: "100%", height: "100%" }} 144 resizeMode="cover" 145 /> 146 </View> 147 )} 148 </Stack> 149 150 {/* Text Content */} 151 <YStack 152 flex={1} 153 justifyContent="space-around" 154 alignItems="flex-start" 155 gap={contentPadding / 4} 156 width={layoutHorizontal ? "100%" : 0} 157 minHeight={0} 158 maxHeight="unset" 159 zIndex={12} 160 > 161 {title && ( 162 <Text 163 fontSize={16} 164 color="$color" 165 fontWeight="400" 166 numberOfLines={1} 167 ellipsizeMode="tail" 168 > 169 {title} 170 </Text> 171 )} 172 {streamerName && ( 173 <Text 174 fontSize={14} 175 color="$color" 176 fontWeight="400" 177 numberOfLines={1} 178 ellipsizeMode="tail" 179 > 180 @{streamerName} 181 </Text> 182 )} 183 {category.length > 0 && ( 184 <XStack flexWrap="wrap" gap={4} alignItems="center"> 185 {category.map((cat, index) => ( 186 <Stack 187 key={index} 188 backgroundColor="$background075" 189 borderRadius={999} 190 paddingHorizontal={categoryPillPaddingHorizontal} 191 height={categoryPillHeight} 192 alignSelf="flex-start" 193 justifyContent="center" 194 > 195 <Text 196 fontSize={12} 197 color="$white075" 198 fontWeight="400" 199 numberOfLines={1} 200 ellipsizeMode="tail" 201 paddingHorizontal={3} 202 > 203 {cat} 204 </Text> 205 </Stack> 206 ))} 207 </XStack> 208 )} 209 </YStack> 210 </SubContainer> 211 </MainContainer> 212 ); 213}; 214 215export default StreamCard;