Live video on the AT Protocol

replace everything with formatHandle

+49 -48
+2 -2
js/app/components/create-livestream.tsx
··· 1 - import { zero } from "@streamplace/components"; 1 + import { formatHandleWithAt, zero } from "@streamplace/components"; 2 2 import ThumbnailSelector from "components/thumbnail-selector"; 3 3 import { useCaptureVideoFrame } from "hooks/useCaptureVideoFrame"; 4 4 import { useLiveUser } from "hooks/useLiveUser"; ··· 123 123 Streamer 124 124 </Text> 125 125 <Text style={[{ paddingBottom: 8, fontWeight: "bold" }]}> 126 - @{profile?.handle} 126 + {profile && formatHandleWithAt(profile)} 127 127 </Text> 128 128 </View> 129 129
+7 -2
js/app/components/edit-livestream.tsx
··· 1 - import { Text, useLivestream, zero } from "@streamplace/components"; 1 + import { 2 + formatHandleWithAt, 3 + Text, 4 + useLivestream, 5 + zero, 6 + } from "@streamplace/components"; 2 7 import { useLiveUser } from "hooks/useLiveUser"; 3 8 import { useEffect, useState } from "react"; 4 9 import { Pressable, ScrollView, TextInput, View } from "react-native"; ··· 86 91 Streamer 87 92 </Text> 88 93 <Text style={[{ paddingBottom: 8, fontWeight: "bold" }]}> 89 - @{profile?.handle} 94 + {profile && formatHandleWithAt(profile)} 90 95 </Text> 91 96 </View> 92 97
+4 -2
js/app/components/live-dashboard/livestream-panel.tsx
··· 2 2 Button, 3 3 Checkbox, 4 4 ContentMetadataForm, 5 + formatHandle, 6 + formatHandleWithAt, 5 7 Input, 6 8 Textarea, 7 9 Tooltip, ··· 184 186 livestream?.record.canonicalUrl || "", 185 187 ); 186 188 const defaultCanonicalUrl = useMemo(() => { 187 - return `${url}/${profile?.handle}`; 189 + return `${url}/${profile && formatHandle(profile)}`; 188 190 }, [url, profile?.handle]); 189 191 190 192 useEffect(() => { ··· 418 420 { fontWeight: "bold", paddingBottom: 8 }, 419 421 ]} 420 422 > 421 - @{profile?.handle || "streamer"} 423 + {profile && formatHandleWithAt(profile)} 422 424 </Text> 423 425 </View> 424 426 <View
+4 -2
js/app/components/mobile/bottom-metadata.tsx
··· 2 2 Button, 3 3 ContentRights, 4 4 ContentWarnings, 5 + formatHandle, 6 + formatHandleWithAt, 5 7 layout, 6 8 PlayerUI, 7 9 ShareSheet, ··· 90 92 <Pressable 91 93 onPress={() => { 92 94 if (profile?.handle) { 93 - const url = `https://bsky.app/profile/${profile.handle}`; 95 + const url = `https://bsky.app/profile/${formatHandle(profile)}`; 94 96 Linking.openURL(url); 95 97 } 96 98 }} 97 99 > 98 100 <Text style={{ color: "white", fontWeight: "600" }}> 99 - @{profile?.handle || "user"} 101 + {profile ? formatHandleWithAt(profile) : "@user"} 100 102 </Text> 101 103 </Pressable> 102 104 {did && profile && (
+2 -2
js/app/components/name-color-picker/name-color-picker.tsx
··· 1 - import { Button, zero } from "@streamplace/components"; 1 + import { Button, formatHandleWithAt, zero } from "@streamplace/components"; 2 2 import { Palette, SwatchBook, X } from "lucide-react-native"; 3 3 import { useEffect, useState } from "react"; 4 4 import { ··· 185 185 ]} 186 186 > 187 187 <Text style={[{ color: tempColor, fontWeight: "600" }]}> 188 - @{profile.handle} 188 + {formatHandleWithAt(profile)} 189 189 </Text> 190 190 <Text 191 191 style={[
+3 -2
js/components/src/components/chat/chat-message.tsx
··· 8 8 import { ChatMessageViewHydrated } from "streamplace"; 9 9 import { RichtextSegment, segmentize } from "../../lib/facet"; 10 10 import { borders, flex, gap, ml, mr, opacity, pl } from "../../lib/theme/atoms"; 11 + import { formatHandleWithAt } from "../../utils/format-handle"; 11 12 import { atoms, colors, layout } from "../ui"; 12 13 13 14 interface Facet { ··· 138 139 fontWeight: "thin", 139 140 }} 140 141 > 141 - @{(replyTo.author as any).handle} 142 + {formatHandleWithAt(replyTo.author)} 142 143 </Text>{" "} 143 144 <Text 144 145 style={{ ··· 181 182 }, 182 183 ]} 183 184 > 184 - @{item.author.handle} 185 + {formatHandleWithAt(item.author)} 185 186 </Text> 186 187 :{" "} 187 188 <RichTextMessage
+4 -3
js/components/src/components/chat/mod-view.tsx
··· 12 12 import { ChatMessageViewHydrated } from "streamplace"; 13 13 import { useDeleteChatMessage } from "../../livestream-store"; 14 14 import { useStreamplaceStore } from "../../streamplace-store"; 15 + import { formatHandle, formatHandleWithAt } from "../../utils/format-handle"; 15 16 import { 16 17 atoms, 17 18 DropdownMenu, ··· 113 114 minute: "2-digit", 114 115 hour12: false, 115 116 })}{" "} 116 - @{message.author.handle}: {message.record.text} 117 + {formatHandleWithAt(message.author)}: {message.record.text} 117 118 </Text> 118 119 </View> 119 120 </DropdownMenuItem> ··· 159 160 <Text color="destructive"> 160 161 {isBlockLoading 161 162 ? "Blocking..." 162 - : `Block user @${message.author.handle} from this channel`} 163 + : `Block user ${formatHandleWithAt(message.author)} from this channel`} 163 164 </Text> 164 165 )} 165 166 </DropdownMenuItem> ··· 170 171 <DropdownMenuItem 171 172 onPress={() => { 172 173 Linking.openURL( 173 - `https://${BSKY_FRONTEND_DOMAIN}/profile/${message.author.handle}`, 174 + `https://${BSKY_FRONTEND_DOMAIN}/profile/${formatHandle(message.author)}`, 174 175 ); 175 176 }} 176 177 >
+5 -3
js/components/src/components/mobile-player/ui/viewer-context-menu.tsx
··· 4 4 import { 5 5 ContentRights, 6 6 ContentWarnings, 7 + formatHandle, 8 + formatHandleWithAt, 7 9 useAvatars, 8 10 useLivestreamInfo, 9 11 zero, ··· 113 115 <Pressable 114 116 onPress={() => { 115 117 if (profile?.handle) { 116 - const url = `https://bsky.app/profile/${profile.handle}`; 118 + const url = `https://bsky.app/profile/${formatHandle(profile)}`; 117 119 Linking.openURL(url); 118 120 } 119 121 }} 120 122 > 121 - <Text>@{profile?.handle || "user"}</Text> 123 + <Text>{profile && formatHandleWithAt(profile)}</Text> 122 124 </Pressable> 123 125 {/*{did && profile && ( 124 126 <FollowButton streamerDID={profile?.did} currentUserDID={did} /> ··· 163 165 <DropdownMenuItem 164 166 onPress={() => { 165 167 if (profile?.handle) { 166 - const url = `https://bsky.app/profile/${profile.handle}`; 168 + const url = `https://bsky.app/profile/${formatHandle(profile)}`; 167 169 Linking.openURL(url); 168 170 } 169 171 }}
+11 -27
js/components/src/components/share/sharesheet.tsx
··· 4 4 import { colors } from "../../lib/theme"; 5 5 import { useLivestreamStore } from "../../livestream-store"; 6 6 import { useUrl } from "../../streamplace-store"; 7 + import { formatHandle } from "../../utils/format-handle"; 7 8 import { BlueskyIcon } from "../icons/bluesky-icon"; 8 9 import { 9 10 DropdownMenu, ··· 26 27 27 28 // Get the current stream URL 28 29 const getStreamUrl = useCallback(() => { 29 - return url + (profile ? `/@${profile.handle}` : ""); 30 + return url + (profile ? `/${formatHandle(profile)}` : ""); 30 31 }, [profile]); 31 32 32 33 // Get the embed URL 33 34 const getEmbedUrl = useCallback(() => { 34 - return url + (profile ? `/embed/${profile.handle}` : ""); 35 + return url + (profile ? `/embed/${formatHandle(profile)}` : ""); 35 36 }, [profile]); 36 37 37 38 // Get embed code ··· 63 64 // Share to Bluesky 64 65 const shareToBluesky = useCallback(() => { 65 66 const streamUrl = getStreamUrl(); 66 - const text = profile 67 - ? `Check out @${profile.handle} live on Streamplace! ${streamUrl}` 68 - : `Check out this stream on Streamplace! ${streamUrl}`; 67 + const text = 68 + profile && profile.handle 69 + ? `Check out @${profile.handle} live on Streamplace! ${streamUrl}` 70 + : `Check out this stream on Streamplace! ${streamUrl}`; 69 71 const blueskyUrl = `https://bsky.app/intent/compose?text=${encodeURIComponent(text)}`; 70 72 Linking.openURL(blueskyUrl); 71 73 onShare?.("share_bluesky", true); 72 74 }, [profile, getStreamUrl, onShare]); 73 75 74 - // Share to Twitter/X 75 - const shareToTwitter = useCallback(() => { 76 - const streamUrl = getStreamUrl(); 77 - const text = profile 78 - ? `Check out @${profile.handle} live on Streamplace!` 79 - : `Check out this stream on Streamplace!`; 80 - const twitterUrl = `https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}&url=${encodeURIComponent(streamUrl)}`; 81 - Linking.openURL(twitterUrl); 82 - onShare?.("share_twitter", true); 83 - }, [profile, getStreamUrl, onShare]); 84 - 85 76 // Native share (mobile) 86 77 const nativeShare = useCallback(async () => { 87 78 const streamUrl = getStreamUrl(); 88 - const text = profile 89 - ? `Check out @${profile.handle} live on Streamplace!` 90 - : `Check out this stream on Streamplace!`; 79 + const text = 80 + profile && profile.handle 81 + ? `Check out @${profile.handle} live on Streamplace!` 82 + : `Check out this stream on Streamplace!`; 91 83 92 84 if (Platform.OS === "web" && navigator.share) { 93 85 try { ··· 119 111 <Text>Share to Bluesky</Text> 120 112 </View> 121 113 </DropdownMenuItem> 122 - {/* <DropdownMenuItem onPress={shareToTwitter}> 123 - <View 124 - style={{ flexDirection: "row", alignItems: "center", gap: 12 }} 125 - > 126 - <MessageCircle size={20} color={colors.gray[400]} /> 127 - <Text>Share to X</Text> 128 - </View> 129 - </DropdownMenuItem> */} 130 114 {/* navigator isn't on non-web */} 131 115 {Platform.OS !== "web" || (navigator && (navigator as any).share) ? ( 132 116 <DropdownMenuItem onPress={nativeShare}>
+2
js/components/src/index.tsx
··· 37 37 export { default as VideoRetry } from "./components/mobile-player/video-retry"; 38 38 export * from "./lib/system-messages"; 39 39 40 + export * from "./utils/format-handle"; 41 + 40 42 export { DanmuOverlay } from "./components/danmu/danmu-overlay"; 41 43 export { DanmuOverlayOBS } from "./components/danmu/danmu-overlay-obs"; 42 44
+5 -3
js/components/src/utils/format-handle.ts
··· 5 5 */ 6 6 export function formatHandle( 7 7 profile: Pick<AppBskyActorDefs.ProfileViewBasic, "handle" | "did">, 8 + prefix: string = "", 8 9 ): string { 9 10 if (profile.handle === "handle.invalid") { 10 11 return profile.did; 11 12 } 12 - return profile.handle; 13 + return prefix + profile.handle; 13 14 } 14 15 15 16 /** 16 - * formats a user's handle with @ prefix for display, falling back to DID if handle is invalid 17 + * convenience function for formatting a user's handle with @ prefix for display, 18 + * falling back to DID if handle is invalid 17 19 */ 18 20 export function formatHandleWithAt( 19 21 profile: Pick<AppBskyActorDefs.ProfileViewBasic, "handle" | "did">, 20 22 ): string { 21 - return `@${formatHandle(profile)}`; 23 + return formatHandle(profile, "@"); 22 24 }