Live video on the AT Protocol
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;