Live video on the AT Protocol

redo live dashboard UI w color and style updates

+235 -171
+2 -8
js/app/components/live-dashboard/bento-grid.tsx
··· 22 22 const isWeb = Platform.OS === "web"; 23 23 24 24 return ( 25 - <View style={[flex.values[1], gap.all[4], p[4]]}> 26 - <View 27 - style={[ 28 - layout.flex.column, 29 - bg.gray[900], 30 - { minWidth: isWeb ? 400 : "100%" }, 31 - ]} 32 - > 25 + <View style={[flex.values[1], gap.all[4], p[4], bg.black]}> 26 + <View style={[layout.flex.column, { minWidth: isWeb ? 400 : "100%" }]}> 33 27 <Header isLive={isLive} /> 34 28 </View> 35 29 <View style={[flex.values[1], layout.flex.row, gap.all[4]]}>
+26 -22
js/app/components/live-dashboard/chat-panel.tsx
··· 9 9 import { useState } from "react"; 10 10 import { Text, View } from "react-native"; 11 11 import { useLiveUser } from "../../hooks/useLiveUser"; 12 - 13 - const { flex, bg, r, borders, p, text, layout } = zero; 12 + const { flex, bg, r, borders, p, px, py, text, layout } = zero; 14 13 15 14 export default function ChatPanel() { 16 15 // Get real data from hooks ··· 38 37 <View 39 38 style={[ 40 39 flex.values[1], 41 - bg.gray[800], 42 - r[3], 40 + bg.neutral[900], 43 41 borders.width.thin, 44 - borders.color.gray[700], 42 + borders.color.neutral[700], 45 43 layout.flex.column, 44 + r.lg, 45 + { minWidth: 300, maxWidth: 600, flexShrink: 0 }, 46 46 ]} 47 47 > 48 48 <View ··· 50 50 layout.flex.row, 51 51 layout.flex.spaceBetween, 52 52 layout.flex.alignCenter, 53 - p[4], 54 53 borders.bottom.width.thin, 55 - borders.bottom.color.gray[700], 54 + borders.bottom.color.neutral[700], 55 + p[4], 56 56 ]} 57 57 > 58 58 <Text style={[text.white, { fontSize: 18, fontWeight: "600" }]}> 59 59 Live Chat 60 60 </Text> 61 - <View style={[layout.flex.row, layout.flex.alignCenter, { gap: 8 }]}> 61 + <View style={[layout.flex.row, layout.flex.alignCenter]}> 62 62 <View 63 63 style={[ 64 64 { width: 6, height: 6, borderRadius: 3 }, 65 65 isLive && isConnected ? bg.green[500] : bg.gray[500], 66 66 ]} 67 67 /> 68 - <Text style={[text.gray[400], { fontSize: 12 }]}> 68 + <Text style={[text.gray[400], { fontSize: 12, marginLeft: 8 }]}> 69 69 {messagesPerMinute} msg/min 70 70 </Text> 71 71 </View> 72 72 </View> 73 - <View style={[flex.values[1], p[2]]}> 74 - <Chat canModerate={canModerate} shownMessages={50} /> 75 - <ChatBox 76 - emojiData={emojiData} 77 - chatBoxStyle={[ 78 - bg.gray[700], 79 - borders.width.thin, 80 - borders.color.gray[600], 81 - r[2], 82 - p[3], 83 - !isConnected && { opacity: 0.6 }, 84 - ]} 85 - /> 73 + <View style={[flex.values[1], px[2], { minHeight: 0 }]}> 74 + <View style={[flex.values[1], { minHeight: 0 }]}> 75 + <Chat canModerate={canModerate} shownMessages={50} /> 76 + </View> 77 + <View style={[{ flexShrink: 0 }]}> 78 + <ChatBox 79 + emojiData={emojiData} 80 + chatBoxStyle={[ 81 + bg.gray[700], 82 + borders.width.thin, 83 + borders.color.gray[600], 84 + r.md, 85 + p[3], 86 + !isConnected && { opacity: 0.6 }, 87 + ]} 88 + /> 89 + </View> 86 90 </View> 87 91 </View> 88 92 );
+5 -5
js/app/components/live-dashboard/header.tsx
··· 10 10 import { useLiveUser } from "../../hooks/useLiveUser"; 11 11 import { useSegmentTiming } from "../../hooks/useSegmentTiming"; 12 12 13 - const { flex, bg, r, borders, p, px, py, text, layout, gap } = zero; 13 + const { bg, r, borders, px, py, text, layout, gap } = zero; 14 14 15 15 interface MetricItemProps { 16 16 icon: any; ··· 151 151 return ( 152 152 <View 153 153 style={[ 154 - bg.gray[800], 155 - borders.bottom.width.thin, 156 - borders.bottom.color.gray[700], 157 154 px[4], 158 155 py[3], 156 + r.lg, 159 157 layout.flex.row, 160 - layout.flex.alignCenter, 161 158 layout.flex.spaceBetween, 159 + bg.neutral[900], 160 + borders.width.thin, 161 + borders.color.neutral[700], 162 162 ]} 163 163 > 164 164 {/* Left side - Stream title and status */}
+58 -52
js/app/components/live-dashboard/livestream-panel/index.tsx
··· 1 - import { useLivestream, useToast, zero } from "@streamplace/components"; 1 + import { 2 + Button, 3 + Textarea, 4 + useLivestream, 5 + useToast, 6 + zero, 7 + } from "@streamplace/components"; 2 8 import ThumbnailSelector from "components/thumbnail-selector"; 3 9 import ButtonSelector from "components/ui/button-selector"; 4 10 import { ··· 12 18 import { useEffect, useState } from "react"; 13 19 import { 14 20 Platform, 15 - Pressable, 16 21 ScrollView, 17 22 Text, 18 - TextInput, 19 23 useWindowDimensions, 20 24 View, 21 25 } from "react-native"; 22 26 import { useAppDispatch, useAppSelector } from "store/hooks"; 23 27 24 - const { flex, p, px, py, mt, gap, layout, bg, borders, text, r, w } = zero; 28 + const { 29 + flex, 30 + p, 31 + px, 32 + py, 33 + h, 34 + gap, 35 + layout, 36 + bg, 37 + borders, 38 + mt, 39 + pt, 40 + pb, 41 + text, 42 + r, 43 + w, 44 + typography, 45 + } = zero; 25 46 const isWeb = Platform.OS === "web"; 26 47 27 48 interface LivestreamPanelProps { ··· 161 182 <View 162 183 style={[ 163 184 flex.values[1], 164 - bg.gray[800], 165 - r[3], 185 + bg.neutral[900], 186 + r.lg, 166 187 borders.width.thin, 167 - borders.color.gray[700], 188 + borders.color.neutral[700], 168 189 layout.flex.column, 169 190 ]} 170 191 > ··· 172 193 style={[ 173 194 layout.flex.row, 174 195 layout.flex.spaceBetween, 175 - layout.flex.alignCenter, 176 - p[4], 196 + layout.flex.align.center, 197 + px[4], 198 + pt[4], 199 + pb[4], 177 200 borders.bottom.width.thin, 178 - borders.bottom.color.gray[700], 201 + borders.bottom.color.neutral[700], 179 202 ]} 180 203 > 181 204 <Text style={[text.white, { fontSize: 18, fontWeight: "600" }]}> 182 - Live Chat 205 + Stream settings 183 206 </Text> 184 207 <ButtonSelector 185 208 values={[ 186 209 { label: "Create", value: "create" }, 187 210 { label: "Edit", value: "edit" }, 188 211 ]} 212 + style={[{ marginVertical: -2 }]} 189 213 selectedValue={mode} 190 214 setSelectedValue={setSelectedMode} 191 215 disabledValues={livestream ? [] : ["edit"]} 192 216 /> 193 217 </View> 194 218 {mode === "edit" && ( 195 - <Text 196 - style={[px[4], text.white, { fontSize: 20, fontWeight: "bold" }]} 197 - > 219 + <Text style={[p[4], text.white, typography.universal["xl"]]}> 198 220 Change your Current Livestream Title 199 221 </Text> 200 222 )} 201 223 {mode === "edit" && noLivestream ? ( 202 224 <View style={[layout.flex.center, p[4]]}> 203 - <Text style={[text.gray[400], { fontSize: 16 }]}> 225 + <Text style={[text.neutral[400], { fontSize: 16 }]}> 204 226 No active livestream to edit. Start a livestream first! 205 227 </Text> 206 228 </View> ··· 208 230 <View 209 231 style={[ 210 232 { flexDirection: useTwoColumns ? "row" : "column" }, 211 - useTwoColumns ? gap.row[12] : gap.column[4], 233 + useTwoColumns ? gap.row[12] : gap.all[8], 212 234 w.percent[100], 213 235 { alignSelf: "center" }, 214 236 p[4], ··· 219 241 ]} 220 242 > 221 243 {/* Left column: labels and fields */} 222 - <View 223 - style={[ 224 - flex.values[2], 225 - { minWidth: 0 }, 226 - gap.column[3], 227 - w.percent[100], 228 - ]} 229 - > 244 + <View style={[{ minWidth: 0 }, gap.column[3], w.percent[100]]}> 230 245 <View 231 246 style={[ 232 247 layout.flex.row, ··· 236 251 > 237 252 <Text 238 253 style={[ 239 - text.gray[300], 254 + text.neutral[300], 240 255 { minWidth: 100, textAlign: "left", paddingBottom: 8 }, 241 256 ]} 242 257 > ··· 260 275 > 261 276 <Text 262 277 style={[ 263 - text.gray[300], 278 + text.neutral[300], 264 279 { minWidth: 100, textAlign: "left", paddingBottom: 8 }, 265 280 ]} 266 281 > 267 282 Title 268 283 </Text> 269 284 <View style={[flex.values[1]]}> 270 - <TextInput 285 + <Textarea 271 286 value={title} 272 287 onChangeText={setTitle} 273 288 style={[ 274 289 p[2], 275 290 r[1], 276 - bg.gray[900], 291 + bg.neutral[800], 277 292 text.white, 278 293 w.percent[100], 279 294 { minHeight: 100, fontSize: 16 }, ··· 288 303 style={[ 289 304 layout.flex.row, 290 305 layout.flex.alignCenter, 306 + mt[2], 291 307 w.percent[100], 292 - { marginTop: -16 }, 293 308 ]} 294 309 > 295 310 <View style={[flex.values[1]]}> 296 - <Text style={[text.gray[400], { fontSize: 12 }]}> 311 + <Text style={[text.neutral[400], { fontSize: 12 }]}> 297 312 Updating will not send out notifications to viewers or 298 313 create a new social media post. 299 314 </Text> ··· 310 325 gap.column[4], 311 326 { alignItems: "center" }, 312 327 { justifyContent: "flex-start" }, 313 - { marginTop: 12 }, 328 + h.percent[100], 314 329 ]} 315 330 > 316 331 <Text ··· 328 343 </View> 329 344 </View> 330 345 )} 331 - <View 346 + <Button 347 + disabled={disabled} 332 348 style={[ 349 + bg.primary[500], 350 + r[1], 351 + py[2], 333 352 w.percent[100], 334 353 { alignItems: "center" }, 335 - mode === "edit" ? { marginTop: -16 } : mt[4], 354 + { opacity: disabled ? 0.5 : 1 }, 336 355 ]} 356 + onPress={handleSubmit} 337 357 > 338 - <Pressable 339 - disabled={disabled} 340 - style={[ 341 - bg.primary[500], 342 - r[1], 343 - px[4], 344 - py[2], 345 - w.percent[100], 346 - { alignItems: "center" }, 347 - { opacity: disabled ? 0.5 : 1 }, 348 - ]} 349 - onPress={handleSubmit} 358 + <Text 359 + style={[text.white, { fontSize: 16, fontWeight: "bold" }]} 350 360 > 351 - <Text 352 - style={[text.white, { fontSize: 16, fontWeight: "bold" }]} 353 - > 354 - {buttonText} 355 - </Text> 356 - </Pressable> 357 - </View> 361 + {buttonText} 362 + </Text> 363 + </Button> 358 364 </View> 359 365 )} 360 366 </View>
+57 -31
js/app/components/live-dashboard/stream-monitor.tsx
··· 12 12 import { useSegmentTiming } from "../../hooks/useSegmentTiming"; 13 13 import StreamScreen from "./live-selector"; 14 14 15 - const { flex, bg, r, borders, layout, p, text, w, h } = zero; 15 + const { flex, bg, r, borders, layout, p, text, w, h, mt } = zero; 16 16 17 17 interface StreamMonitorProps { 18 18 userProfile?: any; ··· 89 89 style={[ 90 90 flex.values[2], 91 91 bg.gray[800], 92 - r[3], 92 + r.lg, 93 + bg.neutral[900], 93 94 borders.width.thin, 94 - borders.color.gray[700], 95 + borders.color.neutral[700], 95 96 layout.flex.column, 96 97 ]} 97 98 > 98 - <View style={[flex.values[1], layout.flex.center, bg.gray[900]]}> 99 + <View style={[flex.values[1], layout.flex.center, bg.neutral[900]]}> 99 100 {isLive && userProfile ? ( 100 101 isStreamVisible ? ( 101 102 <Player src={userProfile.did} name={userProfile.handle} /> ··· 141 142 layout.flex.spaceBetween, 142 143 layout.flex.alignCenter, 143 144 p[4], 144 - borders.bottom.width.thin, 145 - borders.bottom.color.gray[700], 145 + borders.top.width.thin, 146 + borders.top.color.gray[700], 146 147 ]} 147 148 > 148 - <View style={[layout.flex.row, layout.flex.alignCenter, { gap: 12 }]}> 149 + <View 150 + style={[ 151 + layout.flex.row, 152 + layout.flex.spaceBetween, 153 + layout.flex.alignCenter, 154 + flex.grow[1], 155 + { gap: 12 }, 156 + ]} 157 + > 149 158 <Text style={[text.white, { fontSize: 18, fontWeight: "600" }]}> 150 159 {ls?.record.title || "Stream Title"} 151 160 </Text> 152 - {isLive && userProfile && ( 153 - <TouchableOpacity 154 - onPress={() => setIsStreamVisible(!isStreamVisible)} 155 - style={{ 156 - padding: 4, 157 - borderRadius: 4, 158 - backgroundColor: "rgba(255, 255, 255, 0.1)", 159 - }} 160 - > 161 - {isStreamVisible ? ( 162 - <EyeOff size={16} color="#9ca3af" /> 163 - ) : ( 164 - <Eye size={16} color="#9ca3af" /> 165 - )} 166 - </TouchableOpacity> 167 - )} 168 - <View style={[w[2], h[2], r[1], bg[getConnectionColor()][500]]} /> 169 - <Text style={[text.gray[400], { fontSize: 14 }]}> 170 - {isLive ? "LIVE" : "OFFLINE"} 171 - </Text> 172 - {isLive && segmentTiming.timeBetweenSegments && ( 173 - <Text style={[text.gray[500], { fontSize: 12 }]}> 174 - {Math.round(segmentTiming.timeBetweenSegments)}ms 161 + <View 162 + style={[ 163 + layout.flex.row, 164 + layout.flex.justify.end, 165 + layout.flex.align.start, 166 + { gap: 8, flexShrink: 0 }, 167 + ]} 168 + > 169 + {isLive && userProfile && ( 170 + <TouchableOpacity 171 + onPress={() => setIsStreamVisible(!isStreamVisible)} 172 + style={{ 173 + padding: 4, 174 + borderRadius: 4, 175 + backgroundColor: "rgba(255, 255, 255, 0.1)", 176 + }} 177 + > 178 + {isStreamVisible ? ( 179 + <EyeOff size={16} color="#9ca3af" /> 180 + ) : ( 181 + <Eye size={16} color="#9ca3af" /> 182 + )} 183 + </TouchableOpacity> 184 + )} 185 + <View 186 + style={[ 187 + w[3], 188 + h[3], 189 + r.full, 190 + { marginTop: 3 }, 191 + bg[getConnectionColor()][500], 192 + ]} 193 + /> 194 + <Text style={[text.gray[400], { fontSize: 14 }]}> 195 + {isLive ? "LIVE" : "OFFLINE"} 175 196 </Text> 176 - )} 197 + {isLive && segmentTiming.timeBetweenSegments && ( 198 + <Text style={[text.gray[500], { fontSize: 12 }]}> 199 + {Math.round(segmentTiming.timeBetweenSegments)}ms 200 + </Text> 201 + )} 202 + </View> 177 203 </View> 178 204 </View> 179 205 </View>
+43 -32
js/app/components/ui/button-selector.tsx
··· 1 - import { Button, Text, XStack, YStack, YStackProps } from "tamagui"; 1 + import { 2 + Button, 3 + Text, 4 + useTheme, 5 + View, 6 + ViewProps, 7 + zero, 8 + } from "@streamplace/components"; 9 + 10 + const { gap, pt, w, bg, r, spacing, layout, colors } = zero; 2 11 3 12 export default function ButtonSelector({ 4 13 text, ··· 6 15 selectedValue, 7 16 setSelectedValue, 8 17 disabledValues, 18 + style, 9 19 ...props 10 20 }: { 11 21 text?: string; ··· 13 23 selectedValue: string; 14 24 setSelectedValue: (value: any) => void; 15 25 disabledValues?: string[]; 16 - } & YStackProps) { 26 + } & ViewProps) { 27 + let theme = useTheme(); 17 28 return ( 18 - <YStack ai="flex-start" gap="$2" pt="$2" {...props}> 29 + <View align="start" style={[gap.all[2], style as any]} {...props}> 19 30 {text && ( 20 - <Text fontSize="$base" fontWeight="semibold"> 31 + <Text variant="body1" weight="semibold"> 21 32 {text} 22 33 </Text> 23 34 )} 24 - <XStack 25 - ai="center" 26 - jc="space-around" 27 - gap="$1" 28 - w="100%" 29 - bg="$background" 30 - borderRadius="$xl" 35 + <View 36 + direction="row" 37 + align="center" 38 + justify="around" 39 + style={[gap.all[1], w.percent[100], r.full]} 31 40 > 32 - {values.map(({ label, value }) => ( 33 - <Button 34 - key={value} 35 - onPress={() => setSelectedValue(value)} 36 - f={1} 37 - height="$2" 38 - disabled={disabledValues?.includes(value)} 39 - opacity={disabledValues?.includes(value) ? 0.5 : 1} 40 - variant={selectedValue === value ? "outlined" : undefined} 41 - > 42 - <Text 43 - color={ 44 - selectedValue === value 45 - ? "$color.foreground" 46 - : "$color.mutedForeground" 47 - } 41 + {values.map(({ label, value }) => { 42 + const isSelected = selectedValue === value; 43 + const isDisabled = disabledValues?.includes(value); 44 + 45 + return ( 46 + <Button 47 + key={value} 48 + onPress={() => setSelectedValue(value)} 49 + variant={isSelected ? "outline" : "ghost"} 50 + size="pill" 51 + disabled={isDisabled} 52 + style={[ 53 + { flex: 1, maxHeight: 20 }, 54 + isSelected 55 + ? { backgroundColor: theme.theme.colors.primary } 56 + : { backgroundColor: theme.theme.colors.secondary }, 57 + isDisabled && { opacity: 0.5 }, 58 + ]} 48 59 > 49 60 {label} 50 - </Text> 51 - </Button> 52 - ))} 53 - </XStack> 54 - </YStack> 61 + </Button> 62 + ); 63 + })} 64 + </View> 65 + </View> 55 66 ); 56 67 }
+22 -14
js/components/src/components/chat/chat-message.tsx
··· 7 7 import { Linking, View } from "react-native"; 8 8 import { ChatMessageViewHydrated } from "streamplace"; 9 9 import { RichtextSegment, segmentize } from "../../lib/facet"; 10 - import { 11 - borders, 12 - flex, 13 - gap, 14 - ml, 15 - mr, 16 - opacity, 17 - pl, 18 - w, 19 - } from "../../lib/theme/atoms"; 10 + import { borders, flex, gap, ml, mr, opacity, pl } from "../../lib/theme/atoms"; 20 11 import { atoms, colors, layout } from "../ui"; 21 12 22 13 interface Facet { ··· 121 112 style={[ 122 113 gap.all[2], 123 114 layout.flex.row, 124 - w.percent[100], 115 + { minWidth: 0, maxWidth: "100%" }, 125 116 borders.left.width.medium, 126 117 borders.left.color.gray[700], 127 118 ml[4], ··· 129 120 opacity[80], 130 121 ]} 131 122 > 132 - <Text numberOfLines={1} style={[flex.shrink[1], mr[4]]}> 123 + <Text 124 + numberOfLines={1} 125 + style={[ 126 + flex.shrink[1], 127 + mr[4], 128 + { minWidth: 0, overflow: "hidden" }, 129 + ]} 130 + > 133 131 <Text 134 132 style={{ 135 133 color: getRgbColor((item.replyTo.chatProfile as any).color), ··· 149 147 </Text> 150 148 </View> 151 149 )} 152 - <View style={[gap.all[2], layout.flex.row, w.percent[100]]}> 150 + <View 151 + style={[ 152 + gap.all[2], 153 + layout.flex.row, 154 + { minWidth: 0, maxWidth: "100%" }, 155 + ]} 156 + > 153 157 {showTime && ( 154 158 <Text 155 159 style={{ ··· 160 164 {formatTime(item.record.createdAt)} 161 165 </Text> 162 166 )} 163 - <Text weight="bold" color="default" style={[flex.shrink[1]]}> 167 + <Text 168 + weight="bold" 169 + color="default" 170 + style={[flex.shrink[1], { minWidth: 0, overflow: "hidden" }]} 171 + > 164 172 <Text 165 173 style={[ 166 174 {
+21 -6
js/components/src/components/chat/chat.tsx
··· 17 17 useSetReplyToMessage, 18 18 View, 19 19 } from "../../"; 20 - import { bg, flex, px, py, w } from "../../lib/theme/atoms"; 20 + import { bg, flex, px, py } from "../../lib/theme/atoms"; 21 21 import { RenderChatMessage } from "./chat-message"; 22 22 import { ModView } from "./mod-view"; 23 23 ··· 87 87 padding: 1, 88 88 gap: 4, 89 89 zIndex: 10, 90 + maxWidth: 120, 91 + flexShrink: 0, 90 92 }, 91 93 ]} 92 94 > ··· 184 186 style={[ 185 187 py[1], 186 188 px[2], 187 - { position: "relative", borderRadius: 8 }, 189 + { 190 + position: "relative", 191 + borderRadius: 8, 192 + minWidth: 0, 193 + maxWidth: "100%", 194 + }, 188 195 isHovered && bg.gray[950], 189 196 ]} 190 197 onPointerEnter={handleHoverIn} 191 198 onPointerLeave={handleHoverOut} 192 199 > 193 - <Pressable> 200 + <Pressable style={[{ minWidth: 0, maxWidth: "100%" }]}> 194 201 <RenderChatMessage item={item} /> 195 202 </Pressable> 196 203 <ActionsBar ··· 253 260 254 261 if (!chat) 255 262 return ( 256 - <View style={[flex.shrink[1]]}> 263 + <View style={[flex.shrink[1], { minWidth: 0, maxWidth: "100%" }]}> 257 264 <Text>Loading chaat...</Text> 258 265 </View> 259 266 ); 260 267 261 268 return ( 262 - <View style={[flex.shrink[1]].concat(propsStyle || [])}> 269 + <View 270 + style={[flex.shrink[1], { minWidth: 0, maxWidth: "100%" }].concat( 271 + propsStyle || [], 272 + )} 273 + > 263 274 <FlatList 264 - style={[flex.grow[1], flex.shrink[1], w.percent[100]]} 275 + style={[ 276 + flex.grow[1], 277 + flex.shrink[1], 278 + { minWidth: 0, maxWidth: "100%" }, 279 + ]} 265 280 data={chat.slice(0, shownMessages)} 266 281 inverted={true} 267 282 keyExtractor={keyExtractor}
+1 -1
js/components/src/lib/theme/tokens.ts
··· 427 427 428 428 export const borderRadius = { 429 429 none: 0, 430 - sm: 4, 430 + sm: 3, 431 431 md: 8, 432 432 lg: 12, 433 433 xl: 16,