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