diff --git a/js/components/src/utils/format-handle.ts b/js/components/src/utils/format-handle.ts new file mode 100644 index 00000000..3c1b7040 --- /dev/null +++ b/js/components/src/utils/format-handle.ts @@ -0,0 +1,22 @@ +import { AppBskyActorDefs } from "@atproto/api"; + +/** + * formats a user's handle for display, falling back to DID if handle is invalid + */ +export function formatHandle( + profile: Pick, +): string { + if (profile.handle === "handle.invalid") { + return profile.did; + } + return profile.handle; +} + +/** + * formats a user's handle with @ prefix for display, falling back to DID if handle is invalid + */ +export function formatHandleWithAt( + profile: Pick, +): string { + return `@${formatHandle(profile)}`; +} -- 2.51.2 From 37fac6c583fd339e37f8c5888742020a4d8dcfcf Mon Sep 17 00:00:00 2001 From: "Natalie B." <22222885+espeon@users.noreply.github.com> Date: Wed, 26 Nov 2025 14:36:39 -0600 Subject: [PATCH 2/2] replace everything with formatHandle --- js/app/components/create-livestream.tsx | 4 +- js/app/components/edit-livestream.tsx | 9 ++++- .../live-dashboard/livestream-panel.tsx | 6 ++- js/app/components/mobile/bottom-metadata.tsx | 6 ++- .../name-color-picker/name-color-picker.tsx | 4 +- .../src/components/chat/chat-message.tsx | 5 ++- .../src/components/chat/mod-view.tsx | 7 ++-- .../mobile-player/ui/viewer-context-menu.tsx | 8 ++-- .../src/components/share/sharesheet.tsx | 38 ++++++------------- js/components/src/index.tsx | 2 + js/components/src/utils/format-handle.ts | 8 ++-- 11 files changed, 49 insertions(+), 48 deletions(-) diff --git a/js/app/components/create-livestream.tsx b/js/app/components/create-livestream.tsx index 99586158..8d3d72ee 100644 --- a/js/app/components/create-livestream.tsx +++ b/js/app/components/create-livestream.tsx @@ -1,4 +1,4 @@ -import { zero } from "@streamplace/components"; +import { formatHandleWithAt, zero } from "@streamplace/components"; import ThumbnailSelector from "components/thumbnail-selector"; import { useCaptureVideoFrame } from "hooks/useCaptureVideoFrame"; import { useLiveUser } from "hooks/useLiveUser"; @@ -123,7 +123,7 @@ export default function CreateLivestream() { Streamer - @{profile?.handle} + {profile && formatHandleWithAt(profile)} diff --git a/js/app/components/edit-livestream.tsx b/js/app/components/edit-livestream.tsx index 4cbc7704..50bb0e3b 100644 --- a/js/app/components/edit-livestream.tsx +++ b/js/app/components/edit-livestream.tsx @@ -1,4 +1,9 @@ -import { Text, useLivestream, zero } from "@streamplace/components"; +import { + formatHandleWithAt, + Text, + useLivestream, + zero, +} from "@streamplace/components"; import { useLiveUser } from "hooks/useLiveUser"; import { useEffect, useState } from "react"; import { Pressable, ScrollView, TextInput, View } from "react-native"; @@ -86,7 +91,7 @@ export default function UpdateLivestream() { Streamer - @{profile?.handle} + {profile && formatHandleWithAt(profile)} diff --git a/js/app/components/live-dashboard/livestream-panel.tsx b/js/app/components/live-dashboard/livestream-panel.tsx index 0416cd3f..cac46725 100644 --- a/js/app/components/live-dashboard/livestream-panel.tsx +++ b/js/app/components/live-dashboard/livestream-panel.tsx @@ -2,6 +2,8 @@ import { Button, Checkbox, ContentMetadataForm, + formatHandle, + formatHandleWithAt, Input, Textarea, Tooltip, @@ -184,7 +186,7 @@ function LivestreamPanel({ scrollable = true }: { scrollable?: boolean }) { livestream?.record.canonicalUrl || "", ); const defaultCanonicalUrl = useMemo(() => { - return `${url}/${profile?.handle}`; + return `${url}/${profile && formatHandle(profile)}`; }, [url, profile?.handle]); useEffect(() => { @@ -418,7 +420,7 @@ function LivestreamPanel({ scrollable = true }: { scrollable?: boolean }) { { fontWeight: "bold", paddingBottom: 8 }, ]} > - @{profile?.handle || "streamer"} + {profile && formatHandleWithAt(profile)} { if (profile?.handle) { - const url = `https://bsky.app/profile/${profile.handle}`; + const url = `https://bsky.app/profile/${formatHandle(profile)}`; Linking.openURL(url); } }} > - @{profile?.handle || "user"} + {profile ? formatHandleWithAt(profile) : "@user"} {did && profile && ( diff --git a/js/app/components/name-color-picker/name-color-picker.tsx b/js/app/components/name-color-picker/name-color-picker.tsx index 14dd47a7..f12cfc59 100644 --- a/js/app/components/name-color-picker/name-color-picker.tsx +++ b/js/app/components/name-color-picker/name-color-picker.tsx @@ -1,4 +1,4 @@ -import { Button, zero } from "@streamplace/components"; +import { Button, formatHandleWithAt, zero } from "@streamplace/components"; import { Palette, SwatchBook, X } from "lucide-react-native"; import { useEffect, useState } from "react"; import { @@ -185,7 +185,7 @@ export default function NameColorPicker({ ]} > - @{profile.handle} + {formatHandleWithAt(profile)} - @{(replyTo.author as any).handle} + {formatHandleWithAt(replyTo.author)} {" "} - @{item.author.handle} + {formatHandleWithAt(item.author)} :{" "} (() => { minute: "2-digit", hour12: false, })}{" "} - @{message.author.handle}: {message.record.text} + {formatHandleWithAt(message.author)}: {message.record.text} @@ -159,7 +160,7 @@ export const ModView = forwardRef(() => { {isBlockLoading ? "Blocking..." - : `Block user @${message.author.handle} from this channel`} + : `Block user ${formatHandleWithAt(message.author)} from this channel`} )} @@ -170,7 +171,7 @@ export const ModView = forwardRef(() => { { Linking.openURL( - `https://${BSKY_FRONTEND_DOMAIN}/profile/${message.author.handle}`, + `https://${BSKY_FRONTEND_DOMAIN}/profile/${formatHandle(message.author)}`, ); }} > diff --git a/js/components/src/components/mobile-player/ui/viewer-context-menu.tsx b/js/components/src/components/mobile-player/ui/viewer-context-menu.tsx index 53a38441..9db03af8 100644 --- a/js/components/src/components/mobile-player/ui/viewer-context-menu.tsx +++ b/js/components/src/components/mobile-player/ui/viewer-context-menu.tsx @@ -4,6 +4,8 @@ import { Image, Linking, Platform, Pressable, View } from "react-native"; import { ContentRights, ContentWarnings, + formatHandle, + formatHandleWithAt, useAvatars, useLivestreamInfo, zero, @@ -113,12 +115,12 @@ export function ContextMenu({ { if (profile?.handle) { - const url = `https://bsky.app/profile/${profile.handle}`; + const url = `https://bsky.app/profile/${formatHandle(profile)}`; Linking.openURL(url); } }} > - @{profile?.handle || "user"} + {profile && formatHandleWithAt(profile)} {/*{did && profile && ( @@ -163,7 +165,7 @@ export function ContextMenu({ { if (profile?.handle) { - const url = `https://bsky.app/profile/${profile.handle}`; + const url = `https://bsky.app/profile/${formatHandle(profile)}`; Linking.openURL(url); } }} diff --git a/js/components/src/components/share/sharesheet.tsx b/js/components/src/components/share/sharesheet.tsx index 3565f914..562a25e3 100644 --- a/js/components/src/components/share/sharesheet.tsx +++ b/js/components/src/components/share/sharesheet.tsx @@ -4,6 +4,7 @@ import { Clipboard, Linking, Platform, View } from "react-native"; import { colors } from "../../lib/theme"; import { useLivestreamStore } from "../../livestream-store"; import { useUrl } from "../../streamplace-store"; +import { formatHandle } from "../../utils/format-handle"; import { BlueskyIcon } from "../icons/bluesky-icon"; import { DropdownMenu, @@ -26,12 +27,12 @@ export function ShareSheet({ onShare }: ShareSheetProps = {}) { // Get the current stream URL const getStreamUrl = useCallback(() => { - return url + (profile ? `/@${profile.handle}` : ""); + return url + (profile ? `/${formatHandle(profile)}` : ""); }, [profile]); // Get the embed URL const getEmbedUrl = useCallback(() => { - return url + (profile ? `/embed/${profile.handle}` : ""); + return url + (profile ? `/embed/${formatHandle(profile)}` : ""); }, [profile]); // Get embed code @@ -63,31 +64,22 @@ export function ShareSheet({ onShare }: ShareSheetProps = {}) { // Share to Bluesky const shareToBluesky = useCallback(() => { const streamUrl = getStreamUrl(); - const text = profile - ? `Check out @${profile.handle} live on Streamplace! ${streamUrl}` - : `Check out this stream on Streamplace! ${streamUrl}`; + const text = + profile && profile.handle + ? `Check out @${profile.handle} live on Streamplace! ${streamUrl}` + : `Check out this stream on Streamplace! ${streamUrl}`; const blueskyUrl = `https://bsky.app/intent/compose?text=${encodeURIComponent(text)}`; Linking.openURL(blueskyUrl); onShare?.("share_bluesky", true); }, [profile, getStreamUrl, onShare]); - // Share to Twitter/X - const shareToTwitter = useCallback(() => { - const streamUrl = getStreamUrl(); - const text = profile - ? `Check out @${profile.handle} live on Streamplace!` - : `Check out this stream on Streamplace!`; - const twitterUrl = `https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}&url=${encodeURIComponent(streamUrl)}`; - Linking.openURL(twitterUrl); - onShare?.("share_twitter", true); - }, [profile, getStreamUrl, onShare]); - // Native share (mobile) const nativeShare = useCallback(async () => { const streamUrl = getStreamUrl(); - const text = profile - ? `Check out @${profile.handle} live on Streamplace!` - : `Check out this stream on Streamplace!`; + const text = + profile && profile.handle + ? `Check out @${profile.handle} live on Streamplace!` + : `Check out this stream on Streamplace!`; if (Platform.OS === "web" && navigator.share) { try { @@ -119,14 +111,6 @@ export function ShareSheet({ onShare }: ShareSheetProps = {}) { Share to Bluesky - {/* - - - Share to X - - */} {/* navigator isn't on non-web */} {Platform.OS !== "web" || (navigator && (navigator as any).share) ? ( diff --git a/js/components/src/index.tsx b/js/components/src/index.tsx index 81e0b948..e629f83b 100644 --- a/js/components/src/index.tsx +++ b/js/components/src/index.tsx @@ -37,6 +37,8 @@ export * from "./components/chat/system-message"; export { default as VideoRetry } from "./components/mobile-player/video-retry"; export * from "./lib/system-messages"; +export * from "./utils/format-handle"; + export { DanmuOverlay } from "./components/danmu/danmu-overlay"; export { DanmuOverlayOBS } from "./components/danmu/danmu-overlay-obs"; diff --git a/js/components/src/utils/format-handle.ts b/js/components/src/utils/format-handle.ts index 3c1b7040..13a5f7e8 100644 --- a/js/components/src/utils/format-handle.ts +++ b/js/components/src/utils/format-handle.ts @@ -5,18 +5,20 @@ import { AppBskyActorDefs } from "@atproto/api"; */ export function formatHandle( profile: Pick, + prefix: string = "", ): string { if (profile.handle === "handle.invalid") { return profile.did; } - return profile.handle; + return prefix + profile.handle; } /** - * formats a user's handle with @ prefix for display, falling back to DID if handle is invalid + * convenience function for formatting a user's handle with @ prefix for display, + * falling back to DID if handle is invalid */ export function formatHandleWithAt( profile: Pick, ): string { - return `@${formatHandle(profile)}`; + return formatHandle(profile, "@"); }