Live video on the AT Protocol
79
fork

Configure Feed

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

add toggle chat panel

+227 -161
+88 -62
js/app/components/chat/chat-box.tsx
··· 20 20 import { Palette, SquareArrowOutUpRight } from "@tamagui/lucide-icons"; 21 21 import NameColorPicker from "components/name-color-picker/name-color-picker"; 22 22 23 - export default function ChatBox({ isPopout }: { isPopout?: boolean }) { 23 + export default function ChatBox({ 24 + isPopout, 25 + setIsChatVisible, 26 + isChatVisible, 27 + }: { 28 + isPopout?: boolean; 29 + setIsChatVisible?: (visible: boolean) => void; 30 + isChatVisible?: boolean; 31 + }) { 24 32 const [message, setMessage] = useState(""); 25 33 const isReady = useAppSelector(selectIsReady); 26 34 const userProfile = useAppSelector(selectUserProfile); ··· 30 38 const textAreaRef = useRef<Input>(null); 31 39 const dispatch = useAppDispatch(); 32 40 const navigate = useNavigation(); 41 + 33 42 const submit = () => { 34 43 if (!isWeb) { 35 44 Keyboard.dismiss(); ··· 39 48 } 40 49 if (!livestream) { 41 50 throw new Error("No livestream"); 42 - return; 43 51 } 44 52 dispatch(chatMessage({ text: message, livestream })); 45 53 setMessage(""); ··· 63 71 > 64 72 Log in to chat 65 73 </Button> 66 - <PopoutButton livestream={livestream} isPopout={isPopout} /> 74 + <PopoutButton 75 + livestream={livestream} 76 + isPopout={isPopout} 77 + setIsChatVisible={setIsChatVisible} 78 + /> 67 79 </View> 68 80 )} 69 81 {!loggedOut && ( ··· 74 86 alignItems="stretch" 75 87 opacity={loggedOut ? 0 : 1} 76 88 > 77 - <View flexGrow={1} flexShrink={0}> 78 - <TextArea 79 - borderRadius={0} 80 - overflow="hidden" 81 - returnKeyType="done" 82 - submitBehavior="blurAndSubmit" 83 - value={message} 84 - ref={textAreaRef} 85 - multiline={true} 86 - keyboardType="default" 87 - disabled={loggedOut} 88 - rows={1} 89 - onPress={() => { 90 - if (!chatWarned) { 91 - dispatch(chatWarn(true)); 92 - toast.show("Just so you know!", { 93 - message: `Streamplace chat messages are public in the same way that Bluesky posts are public - they create records on your PDS.`, 94 - }); 95 - } 96 - }} 97 - onChangeText={(text) => { 98 - const newMessage = text.replaceAll("\n", ""); 99 - // const rt = new RichText({ text: newMessage }); 100 - // rt.detectFacetsWithoutResolution(); 101 - if (newMessage.length > 300) { 102 - return; 103 - } 104 - setMessage(text.replaceAll("\n", "")); 105 - if (isWeb && textAreaRef.current) { 106 - const textarea = 107 - textAreaRef.current as unknown as HTMLTextAreaElement; 108 - textarea.style.height = ""; 109 - textarea.style.height = textarea.scrollHeight + "px"; 110 - } 111 - }} 112 - onKeyPress={(e) => { 113 - if (e.nativeEvent.key === "Enter") { 114 - e.preventDefault(); 115 - submit(); 116 - } 117 - }} 118 - onSubmitEditing={submit} 119 - /> 120 - </View> 89 + {isChatVisible && ( 90 + <View flexGrow={1} flexShrink={0}> 91 + <TextArea 92 + borderRadius={0} 93 + overflow="hidden" 94 + returnKeyType="done" 95 + submitBehavior="blurAndSubmit" 96 + value={message} 97 + ref={textAreaRef} 98 + multiline={true} 99 + keyboardType="default" 100 + disabled={Boolean(loggedOut)} 101 + rows={1} 102 + onPress={() => { 103 + if (!chatWarned) { 104 + dispatch(chatWarn(true)); 105 + toast.show("Just so you know!", { 106 + message: `Streamplace chat messages are public in the same way that Bluesky posts are public - they create records on your PDS.`, 107 + }); 108 + } 109 + }} 110 + onChangeText={(text) => { 111 + const newMessage = text.replaceAll("\n", ""); 112 + // const rt = new RichText({ text: newMessage }); 113 + // rt.detectFacetsWithoutResolution(); 114 + if (newMessage.length > 300) { 115 + return; 116 + } 117 + setMessage(text.replaceAll("\n", "")); 118 + if (isWeb && textAreaRef.current) { 119 + const textarea = 120 + textAreaRef.current as unknown as HTMLTextAreaElement; 121 + textarea.style.height = ""; 122 + textarea.style.height = textarea.scrollHeight + "px"; 123 + } 124 + }} 125 + onKeyPress={(e) => { 126 + if (e.nativeEvent.key === "Enter") { 127 + e.preventDefault(); 128 + submit(); 129 + } 130 + }} 131 + onSubmitEditing={submit} 132 + /> 133 + </View> 134 + )} 121 135 <View 122 136 flexDirection="row" 123 137 justifyContent="flex-end" 124 138 flexGrow={1} 125 139 flexShrink={0} 140 + gap="$2" 126 141 > 127 - <NameColorPicker 128 - buttonProps={{ backgroundColor: "transparent" }} 129 - text={(color) => <Palette size={16} color={color} />} 130 - /> 131 - <PopoutButton livestream={livestream} isPopout={isPopout} /> 132 - <Button 133 - flexShrink={0} 134 - backgroundColor="transparent" 135 - disabled={loggedOut} 136 - onPress={() => { 137 - submit(); 138 - }} 139 - > 140 - Send 141 - </Button> 142 + {isChatVisible && ( 143 + <> 144 + <NameColorPicker 145 + buttonProps={{ backgroundColor: "transparent" }} 146 + text={(color) => <Palette size={16} color={color} />} 147 + /> 148 + <PopoutButton 149 + livestream={livestream} 150 + isPopout={isPopout} 151 + setIsChatVisible={setIsChatVisible} 152 + /> 153 + <Button 154 + flexShrink={0} 155 + backgroundColor="transparent" 156 + disabled={loggedOut} 157 + onPress={() => { 158 + submit(); 159 + }} 160 + > 161 + Send 162 + </Button> 163 + </> 164 + )} 142 165 </View> 143 166 </Form> 144 167 )} ··· 149 172 const PopoutButton = ({ 150 173 livestream, 151 174 isPopout, 175 + setIsChatVisible, 152 176 }: { 153 177 livestream: LivestreamViewHydrated | null; 154 178 isPopout?: boolean; 179 + setIsChatVisible?: (visible: boolean) => void; 155 180 }) => { 156 181 if (!isWeb || isPopout) { 157 182 return <></>; ··· 164 189 const u = new URL(window.location.href); 165 190 u.pathname = `/chat-popout/${livestream?.author?.did}`; 166 191 window.open(u.toString(), "_blank", "popup=true"); 192 + setIsChatVisible?.(false); 167 193 }} 168 194 > 169 195 <SquareArrowOutUpRight size={16} />
+100 -90
js/app/components/chat/chat.tsx
··· 15 15 import { useAppDispatch, useAppSelector } from "store/hooks"; 16 16 import { Button, ScrollView, Sheet, Text, useMedia, View } from "tamagui"; 17 17 18 - export default function Chat() { 18 + export default function Chat({ 19 + isChatVisible, 20 + setIsChatVisible, 21 + }: { 22 + isChatVisible: boolean; 23 + setIsChatVisible: (visible: boolean) => void; 24 + }) { 19 25 const [open, setOpen] = useState(false); 20 26 const [modMessage, setMessage] = useState<MessageViewHydrated | null>(null); 21 27 const chat = useAppSelector(useChat()); ··· 39 45 const dispatch = useAppDispatch(); 40 46 return ( 41 47 <View f={1} position="relative"> 42 - <Sheet 43 - // forceRemoveScrollEnabled={open} 44 - open={open} 45 - modal={!m.gtXs} 46 - onOpenChange={setOpen} 47 - // snapPoints={snapPoints} 48 - // snapPointsMode={snapPointsMode} 49 - dismissOnSnapToBottom 50 - // position={position} 51 - // onPositionChange={setPosition} 52 - zIndex={100_000} 53 - animation="medium" 54 - > 55 - <Sheet.Overlay 56 - animation="lazy" 57 - backgroundColor="$shadow6" 58 - enterStyle={{ opacity: 0 }} 59 - exitStyle={{ opacity: 0 }} 60 - /> 61 - <Sheet.Frame> 62 - <View 63 - f={1} 64 - alignItems="center" 65 - justifyContent="center" 66 - padding="$4" 67 - gap="$5" 68 - backgroundColor="$accentBackground" 48 + {isChatVisible && ( 49 + <> 50 + <Sheet 51 + // forceRemoveScrollEnabled={open} 52 + open={open} 53 + modal={!m.gtXs} 54 + onOpenChange={setOpen} 55 + // snapPoints={snapPoints} 56 + // snapPointsMode={snapPointsMode} 57 + dismissOnSnapToBottom 58 + // position={position} 59 + // onPositionChange={setPosition} 60 + zIndex={100_000} 61 + animation="medium" 69 62 > 70 - <Button 71 - position="absolute" 72 - top="$0" 73 - right="$0" 74 - onPress={(e) => { 75 - e.stopPropagation(); 76 - setOpen(false); 77 - }} 78 - marginRight={-15} 79 - marginTop={-5} 80 - backgroundColor="transparent" 81 - > 82 - <X /> 83 - </Button> 84 - {modMessage && ( 85 - <> 86 - <ChatMessageText message={modMessage} /> 87 - {modMessage.author.did !== userProfile?.did && ( 88 - <Button 89 - width="100%" 90 - onPress={() => { 91 - setOpen(false); 92 - dispatch( 93 - createBlockRecord({ 94 - subjectDID: modMessage.author.did, 95 - }), 96 - ); 97 - }} 98 - > 99 - <Text>Block @{modMessage.author.handle}</Text> 100 - </Button> 101 - )} 102 - {modMessage.author.did === userProfile?.did && ( 63 + <Sheet.Overlay 64 + animation="lazy" 65 + backgroundColor="$shadow6" 66 + enterStyle={{ opacity: 0 }} 67 + exitStyle={{ opacity: 0 }} 68 + /> 69 + <Sheet.Frame> 70 + <View 71 + f={1} 72 + alignItems="center" 73 + justifyContent="center" 74 + padding="$4" 75 + gap="$5" 76 + backgroundColor="$accentBackground" 77 + > 78 + <Button 79 + position="absolute" 80 + top="$0" 81 + right="$0" 82 + onPress={(e) => { 83 + e.stopPropagation(); 84 + setOpen(false); 85 + }} 86 + marginRight={-15} 87 + marginTop={-5} 88 + backgroundColor="transparent" 89 + > 90 + <X /> 91 + </Button> 92 + {modMessage && ( 103 93 <> 104 - <Button width="100%" disabled={true}> 105 - <Text>(You can't block yourself!)</Text> 106 - </Button> 94 + <ChatMessageText message={modMessage} /> 95 + {modMessage.author.did !== userProfile?.did && ( 96 + <Button 97 + width="100%" 98 + onPress={() => { 99 + setOpen(false); 100 + dispatch( 101 + createBlockRecord({ 102 + subjectDID: modMessage.author.did, 103 + }), 104 + ); 105 + }} 106 + > 107 + <Text>Block @{modMessage.author.handle}</Text> 108 + </Button> 109 + )} 110 + {modMessage.author.did === userProfile?.did && ( 111 + <> 112 + <Button width="100%" disabled={true}> 113 + <Text>(You can't block yourself!)</Text> 114 + </Button> 115 + </> 116 + )} 107 117 </> 108 118 )} 109 - </> 110 - )} 111 - </View> 112 - </Sheet.Frame> 113 - </Sheet> 114 - <ScrollView 115 - paddingHorizontal="$4" 116 - invertStickyHeaders={true} 117 - ref={scrollRef} 118 - onContentSizeChange={() => { 119 - scrollRef.current?.scrollToEnd({ animated: true }); 120 - }} 121 - onLayout={() => { 122 - scrollRef.current?.scrollToEnd({ animated: true }); 123 - }} 124 - > 125 - {chat.map((message) => ( 126 - <ChatMessageRow 127 - key={message.cid} 128 - message={message} 129 - setOpen={setOpen} 130 - setMessage={setMessage} 131 - myStream={myStream} 132 - /> 133 - ))} 134 - </ScrollView> 119 + </View> 120 + </Sheet.Frame> 121 + </Sheet> 122 + <ScrollView 123 + paddingHorizontal="$4" 124 + invertStickyHeaders={true} 125 + ref={scrollRef} 126 + onContentSizeChange={() => { 127 + scrollRef.current?.scrollToEnd({ animated: true }); 128 + }} 129 + onLayout={() => { 130 + scrollRef.current?.scrollToEnd({ animated: true }); 131 + }} 132 + > 133 + {chat.map((message) => ( 134 + <ChatMessageRow 135 + key={message.cid} 136 + message={message} 137 + setOpen={setOpen} 138 + setMessage={setMessage} 139 + myStream={myStream} 140 + /> 141 + ))} 142 + </ScrollView> 143 + </> 144 + )} 135 145 </View> 136 146 ); 137 147 }
+37 -7
js/app/components/livestream/livestream.tsx
··· 17 17 import { LayoutChangeEvent, View as RNView, SafeAreaView } from "react-native"; 18 18 import { useAppDispatch, useAppSelector } from "store/hooks"; 19 19 import { Button, H2, H3, Text, useWindowDimensions, View } from "tamagui"; 20 + import { MessageCircleOff, MessageCircleMore } from "@tamagui/lucide-icons"; 20 21 21 22 export default function Livestream(props: Partial<PlayerProps>) { 22 23 return ( ··· 41 42 42 43 const [outerHeight, setOuterHeight] = useState(0); 43 44 const [innerHeight, setInnerHeight] = useState(0); 45 + const [isChatVisible, setIsChatVisible] = useState(true); 44 46 45 47 // this would all be really easy if i had library that would give me the 46 48 // safe area view height and width but i don't. so let's measure ··· 160 162 $gtXs={{ display: "flex" }} 161 163 > 162 164 <H2>{player.livestream?.record.title}</H2> 163 - <View justifyContent="center" paddingRight="$3"> 165 + <View 166 + flexDirection="row" 167 + alignItems="center" 168 + gap="$2" 169 + paddingRight="$3" 170 + > 164 171 <Viewers viewers={player.viewers ?? 0} /> 172 + <Button 173 + backgroundColor="transparent" 174 + onPress={() => setIsChatVisible(!isChatVisible)} 175 + marginLeft="$2" 176 + > 177 + {isChatVisible ? ( 178 + <MessageCircleOff size={22} /> 179 + ) : ( 180 + <MessageCircleMore size={22} /> 181 + )} 182 + </Button> 165 183 </View> 166 184 </View> 167 185 </View> ··· 171 189 fg={1} 172 190 zIndex={1} 173 191 $gtXs={{ 174 - width: 300, 175 - fb: 300, 192 + width: isChatVisible ? 300 : 0, 193 + fb: isChatVisible ? 300 : 0, 176 194 fs: 0, 177 195 borderLeftColor: "#666", 178 - borderLeftWidth: 1, 196 + borderLeftWidth: isChatVisible ? 1 : 0, 197 + overflow: "hidden", 179 198 }} 180 199 backgroundColor="$background2" 181 200 animation={"quick"} ··· 204 223 {player.livestream?.record.title} 205 224 </Text> 206 225 </View> 207 - <View justifyContent="center" paddingRight="$3"> 226 + <View 227 + flexDirection="row" 228 + alignItems="center" 229 + gap="$2" 230 + paddingRight="$3" 231 + > 208 232 <Viewers viewers={player.viewers ?? 0} /> 209 233 </View> 210 234 </View> 211 - <Chat /> 235 + <Chat 236 + isChatVisible={isChatVisible} 237 + setIsChatVisible={setIsChatVisible} 238 + /> 212 239 <View> 213 - <ChatBox /> 240 + <ChatBox 241 + isChatVisible={isChatVisible} 242 + setIsChatVisible={setIsChatVisible} 243 + /> 214 244 </View> 215 245 </View> 216 246 </View>
+1 -1
js/app/components/name-color-picker/name-color-picker.tsx
··· 132 132 setOpen(false); 133 133 }} 134 134 marginRight={-15} 135 - marginTop={-5} 135 + marginTop={5} 136 136 backgroundColor="transparent" 137 137 > 138 138 <X />
+1 -1
js/app/src/screens/chat-popout.tsx
··· 21 21 minHeight="100%" 22 22 bottom={0} 23 23 > 24 - <Chat /> 24 + <Chat isChatVisible={true} setIsChatVisible={() => {}} /> 25 25 {profile && <ChatBox isPopout={true} />} 26 26 </View> 27 27 </View>