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")}
+
+
+ {legalLinks.length > 0 && (
+ <>
+
+ {legalLinks.map((link, index) => (
+
+
+ {index < legalLinks.length - 1 && }
+
+ ))}
+ >
+ )}
+
+
{t("branding-images")}