diff --git a/js/app/components/create-livestream.tsx b/js/app/components/create-livestream.tsx
index 99586158a..8d3d72eeb 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 4cbc7704d..50bb0e3b8 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 0416cd3ff..cac46725b 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 14dd47a79..f12cfc59d 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 53a384414..9db03af87 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 3565f914d..562a25e36 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 81e0b9481..e629f83bf 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 3c1b70401..13a5f7e84 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, "@");
}