From 9d0f4bab33733f31d3ef143cbac98fcc54cd66fe Mon Sep 17 00:00:00 2001 From: "Natalie B." <22222885+espeon@users.noreply.github.com> Date: Mon, 24 Nov 2025 16:51:16 -0600 Subject: [PATCH] Add legal documentation links --- .../settings/about-category-settings.tsx | 58 +++++-- js/app/components/settings/branding-admin.tsx | 152 ++++++++++++++++++ js/components/locales/en-US/settings.ftl | 12 ++ .../src/streamplace-store/branding.tsx | 13 ++ 4 files changed, 223 insertions(+), 12 deletions(-) diff --git a/js/app/components/settings/about-category-settings.tsx b/js/app/components/settings/about-category-settings.tsx index 5c445751..a1b65296 100644 --- a/js/app/components/settings/about-category-settings.tsx +++ b/js/app/components/settings/about-category-settings.tsx @@ -1,9 +1,11 @@ import { MenuContainer, MenuGroup, + MenuLabel, MenuSeparator, Text, useDanmuUnlocked, + useLegalLinks, useSetDanmuUnlocked, useTheme, useToast, @@ -12,7 +14,7 @@ import { } from "@streamplace/components"; import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { ScrollView } from "react-native"; +import { Platform, ScrollView } from "react-native"; import { SettingsExternalItem, SettingsRowItem, @@ -46,6 +48,19 @@ function cutVersionPrefix(version: string) { } const UNLOCK_TAP_COUNT = 5; + +// Check if the webapp is running on a streamplace domain +function isStreamplaceDomain(): boolean { + if (Platform.OS !== "web") { + return true; + } + if (typeof window === "undefined" || !window.location) { + return false; + } + let host = window.location.hostname; + return host.endsWith("://stream.place") || host.endsWith(".stream.place"); +} + export function AboutCategorySettings() { const { t } = useTranslation("settings"); const theme = useTheme(); @@ -55,6 +70,8 @@ export function AboutCategorySettings() { const [tapCount, setTapCount] = useState(0); const danmuUnlocked = useDanmuUnlocked(); const setDanmuUnlocked = useSetDanmuUnlocked(); + const legalLinks = useLegalLinks(); + const isStreamplace = isStreamplaceDomain(); const handleVersionPress = () => { if (danmuUnlocked) { @@ -134,17 +151,34 @@ export function AboutCategorySettings() { - - - - - + {isStreamplace && ( + + + + )} + + {!isStreamplace && + Platform.OS === "web" && + legalLinks.length > 0 && ( + <> + {t("node-legal-documents")} + + {legalLinks.map((link, index) => ( + <> + {index > 0 && } + + + ))} + + + )} diff --git a/js/app/components/settings/branding-admin.tsx b/js/app/components/settings/branding-admin.tsx index f0dda425..dd46cb76 100644 --- a/js/app/components/settings/branding-admin.tsx +++ b/js/app/components/settings/branding-admin.tsx @@ -39,6 +39,9 @@ export function BrandingAdmin() { const [defaultStreamer, setDefaultStreamer] = useState(""); const [broadcasterDID, setBroadcasterDID] = useState(""); const [uploading, setUploading] = useState(false); + const [legalLinkText, setLegalLinkText] = useState(""); + const [legalLinkUrl, setLegalLinkUrl] = useState(""); + const [editingLinkIndex, setEditingLinkIndex] = useState(null); // get current values const currentTitle = useBrandingAsset("siteTitle"); @@ -49,6 +52,12 @@ export function BrandingAdmin() { const currentLogo = useBrandingAsset("mainLogo"); const currentFavicon = useBrandingAsset("favicon"); const currentSidebarBg = useSidebarBackgroundImage(); + const currentLegalLinks = useBrandingAsset("legalLinks"); + + // parse legal links + const legalLinks: { text: string; url: string }[] = currentLegalLinks?.data + ? JSON.parse(currentLegalLinks.data) + : []; // load current branding on mount useEffect(() => { @@ -269,6 +278,50 @@ export function BrandingAdmin() { } }; + const saveLegalLink = async () => { + if (!legalLinkText.trim() || !legalLinkUrl.trim()) { + toast.show(t("branding-empty-value"), t("branding-empty-value"), { + variant: "error", + }); + return; + } + + const updatedLinks = [...legalLinks]; + const newLink = { text: legalLinkText.trim(), url: legalLinkUrl.trim() }; + + if (editingLinkIndex !== null) { + updatedLinks[editingLinkIndex] = newLink; + } else { + updatedLinks.push(newLink); + } + + await uploadText("legalLinks", JSON.stringify(updatedLinks)); + setLegalLinkText(""); + setLegalLinkUrl(""); + setEditingLinkIndex(null); + }; + + const deleteLegalLink = async (index: number) => { + const updatedLinks = legalLinks.filter((_, i) => i !== index); + if (updatedLinks.length === 0) { + await deleteBlob("legalLinks"); + } else { + await uploadText("legalLinks", JSON.stringify(updatedLinks)); + } + }; + + const startEditingLink = (index: number) => { + setEditingLinkIndex(index); + setLegalLinkText(legalLinks[index].text); + setLegalLinkUrl(legalLinks[index].url); + }; + + const cancelEditingLink = () => { + setEditingLinkIndex(null); + setLegalLinkText(""); + setLegalLinkUrl(""); + }; + if (!agent) { return ( @@ -510,6 +563,105 @@ export function BrandingAdmin() { + {t("branding-legal-links")} + + + + + + {editingLinkIndex !== null + ? t("branding-edit-legal-link") + : t("branding-add-legal-link")} + + + + + + {editingLinkIndex !== null && ( + + )} + + + + + {legalLinks.length > 0 && ( + <> + + {legalLinks.map((link, index) => ( + + + + + + {link.text} + + + {link.url} + + + + + + + + + {index < legalLinks.length - 1 && } + + ))} + + )} + + {t("branding-images")} diff --git a/js/components/locales/en-US/settings.ftl b/js/components/locales/en-US/settings.ftl index 1e290e54..fba51b2f 100644 --- a/js/components/locales/en-US/settings.ftl +++ b/js/components/locales/en-US/settings.ftl @@ -148,6 +148,7 @@ branding-login-required = Please log in to manage branding branding-configuration = Configuration branding-text-settings = Text Settings branding-colors = Colors +branding-legal-links = Legal Links branding-images = Images ## Branding Fields @@ -183,6 +184,14 @@ branding-upload-background = Upload Background branding-delete-background = Delete Background branding-web-only = Image uploads are only available on web. +## Branding Legal Links +branding-add-legal-link = Add Legal Link +branding-edit-legal-link = Edit Legal Link +branding-legal-link-text-placeholder = Link text (e.g., Privacy Policy) +branding-legal-link-url-placeholder = URL (e.g., https://example.com/privacy) +add = Add +edit = Edit + ## Branding Toast Messages branding-not-authenticated = Please log in first branding-empty-value = Please enter a value @@ -192,3 +201,6 @@ branding-delete-success = { $key } deleted successfully branding-upload-failed = Failed to upload branding-delete-failed = Failed to delete branding-not-available = File uploads are only available on web + +## Navigation Categories (About Page) +node-legal-documents = Broadcaster-specific Documents diff --git a/js/components/src/streamplace-store/branding.tsx b/js/components/src/streamplace-store/branding.tsx index c1beac5d..8b919109 100644 --- a/js/components/src/streamplace-store/branding.tsx +++ b/js/components/src/streamplace-store/branding.tsx @@ -187,6 +187,19 @@ export function useSidebarBackgroundImage(): BrandingAsset | undefined { return useBrandingAsset("sidebarBackgroundImage"); } +// convenience hook for legal links +export function useLegalLinks(): { text: string; url: string }[] { + const asset = useBrandingAsset("legalLinks"); + if (!asset?.data) { + return []; + } + try { + return JSON.parse(asset.data); + } catch { + return []; + } +} + // hook to auto-fetch branding when broadcaster changes export function useBrandingAutoFetch() { const fetchBranding = useFetchBranding(); -- 2.51.2