From 8bcf12a3b8775204c1616df75e6388f960bb9be0 Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Wed, 30 Sep 2026 18:13:56 -0500 Subject: [PATCH] fix web chat interactions and teleport replay --- js/i18n/locales/en-US/common.ftl | 27 ++++ js/i18n/locales/es-ES/common.ftl | 27 ++++ js/i18n/locales/fr-FR/common.ftl | 27 ++++ js/i18n/locales/pt-BR/common.ftl | 27 ++++ js/i18n/locales/ro-RO/common.ftl | 27 ++++ js/i18n/locales/zh-Hans/common.ftl | 24 ++++ js/i18n/locales/zh-Hant/common.ftl | 24 ++++ .../src/components/stream/chat-input.test.tsx | 131 ++++++++++++++++++ js/web/src/components/stream/chat-input.tsx | 5 +- .../stream/teleport-dialog.test.tsx | 97 +++++++++++++ .../src/components/stream/teleport-dialog.tsx | 108 ++++++++++++--- js/web/src/hooks/use-follow.test.tsx | 59 +++++++- js/web/src/hooks/use-follow.ts | 45 ++++-- pkg/api/websocket.go | 18 +++ pkg/model/model.go | 1 + pkg/model/teleport.go | 18 +++ pkg/model/teleport_test.go | 54 ++++++++ 17 files changed, 687 insertions(+), 32 deletions(-) create mode 100644 js/web/src/components/stream/chat-input.test.tsx create mode 100644 js/web/src/components/stream/teleport-dialog.test.tsx create mode 100644 pkg/model/teleport_test.go diff --git a/js/i18n/locales/en-US/common.ftl b/js/i18n/locales/en-US/common.ftl index 1e94f5171..970778937 100644 --- a/js/i18n/locales/en-US/common.ftl +++ b/js/i18n/locales/en-US/common.ftl @@ -283,6 +283,33 @@ theatre-exit = Exit theatre mode teleporting-in = Teleporting in teleporting-to = Teleporting to @{ $handle } +## Teleport dialog +teleport-dialog-title = Teleport to another live streamer +teleport-dialog-description = Select a streamer to teleport your viewers to their stream. +teleport-unknown-streamer = Unknown streamer +teleport-search-label = Search live streams +teleport-search-placeholder = Search by handle or title +teleport-loading-streamers = Loading live streamers… +teleport-empty-streamers = No live streamers found. +teleport-no-matching-streamers = No matching live streamers found. +teleport-viewer-count = { $count -> + [one] { $count } viewer + *[other] { $count } viewers +} +teleport-countdown = Countdown +teleport-seconds-range = seconds (5–300) +teleport-countdown-error = Countdown must be between 5 and 300 seconds. +teleport-selection-expired = That streamer is no longer live. Choose another streamer. +teleport-starting = Starting… +teleport-start = Teleport +teleport-error-streamer-only = Only the streamer of the current livestream can start a teleport. +teleport-error-handle-format = Enter a valid handle, such as handle.bsky.social. +teleport-error-countdown-number = Countdown must be a number of seconds. +teleport-error-self = You cannot teleport to yourself. +teleport-error-resolve-handle = Could not find @{ $handle }. +teleport-error-create = Could not start the teleport. +teleport-error-generic = Could not start the teleport: { $message } + ## Video Card views-count = { $count -> [one] { $count } view diff --git a/js/i18n/locales/es-ES/common.ftl b/js/i18n/locales/es-ES/common.ftl index 8d4b1a5dc..801a0d0a9 100644 --- a/js/i18n/locales/es-ES/common.ftl +++ b/js/i18n/locales/es-ES/common.ftl @@ -80,3 +80,30 @@ login-show-live-on-bluesky-description = Añade el anillo rojo LIVE a tu avatar ## Stream notifications teleporting-in = Teletransportando en teleporting-to = Teletransportando a @{ $handle } + +## Teleport dialog +teleport-dialog-title = Teletransportarse a otra persona en directo +teleport-dialog-description = Selecciona a una persona en directo para enviar a tus espectadores a su stream. +teleport-unknown-streamer = Streamer desconocido +teleport-search-label = Buscar streams en directo +teleport-search-placeholder = Buscar por identificador o título +teleport-loading-streamers = Cargando streamers en directo… +teleport-empty-streamers = No se encontraron streamers en directo. +teleport-no-matching-streamers = No hay streamers en directo que coincidan. +teleport-viewer-count = { $count -> + [one] { $count } espectador + *[other] { $count } espectadores +} +teleport-countdown = Cuenta atrás +teleport-seconds-range = segundos (5–300) +teleport-countdown-error = La cuenta atrás debe estar entre 5 y 300 segundos. +teleport-selection-expired = Ese streamer ya no está en directo. Elige otro. +teleport-starting = Iniciando… +teleport-start = Teletransportar +teleport-error-streamer-only = Solo quien transmite el directo actual puede iniciar una teletransportación. +teleport-error-handle-format = Introduce un identificador válido, como handle.bsky.social. +teleport-error-countdown-number = La cuenta atrás debe ser un número de segundos. +teleport-error-self = No puedes teletransportarte a ti mismo. +teleport-error-resolve-handle = No se encontró a @{ $handle }. +teleport-error-create = No se pudo iniciar la teletransportación. +teleport-error-generic = No se pudo iniciar la teletransportación: { $message } diff --git a/js/i18n/locales/fr-FR/common.ftl b/js/i18n/locales/fr-FR/common.ftl index 9cf84e54d..913c570a2 100644 --- a/js/i18n/locales/fr-FR/common.ftl +++ b/js/i18n/locales/fr-FR/common.ftl @@ -80,3 +80,30 @@ login-show-live-on-bluesky-description = Ajoute l'anneau rouge LIVE à votre ava ## Stream notifications teleporting-in = Téléportation dans teleporting-to = Téléportation vers @{ $handle } + +## Teleport dialog +teleport-dialog-title = Téléporter vers un autre streamer en direct +teleport-dialog-description = Choisissez un streamer vers lequel téléporter vos spectateurs. +teleport-unknown-streamer = Streamer inconnu +teleport-search-label = Rechercher les streams en direct +teleport-search-placeholder = Rechercher par identifiant ou titre +teleport-loading-streamers = Chargement des streamers en direct… +teleport-empty-streamers = Aucun streamer en direct trouvé. +teleport-no-matching-streamers = Aucun streamer en direct ne correspond. +teleport-viewer-count = { $count -> + [one] { $count } spectateur + *[other] { $count } spectateurs +} +teleport-countdown = Compte à rebours +teleport-seconds-range = secondes (5–300) +teleport-countdown-error = Le compte à rebours doit être compris entre 5 et 300 secondes. +teleport-selection-expired = Ce streamer n'est plus en direct. Choisissez-en un autre. +teleport-starting = Démarrage… +teleport-start = Téléporter +teleport-error-streamer-only = Seul le streamer du direct actuel peut lancer une téléportation. +teleport-error-handle-format = Saisissez un identifiant valide, par exemple handle.bsky.social. +teleport-error-countdown-number = Le compte à rebours doit être un nombre de secondes. +teleport-error-self = Vous ne pouvez pas vous téléporter vers vous-même. +teleport-error-resolve-handle = @{ $handle } est introuvable. +teleport-error-create = Impossible de démarrer la téléportation. +teleport-error-generic = Impossible de démarrer la téléportation : { $message } diff --git a/js/i18n/locales/pt-BR/common.ftl b/js/i18n/locales/pt-BR/common.ftl index 96adf49f7..0417e8f12 100644 --- a/js/i18n/locales/pt-BR/common.ftl +++ b/js/i18n/locales/pt-BR/common.ftl @@ -80,3 +80,30 @@ login-show-live-on-bluesky-description = Adiciona o anel vermelho LIVE ao seu av ## Stream notifications teleporting-in = Teletransportando em teleporting-to = Teletransportando para @{ $handle } + +## Teleport dialog +teleport-dialog-title = Teletransportar para outro streamer ao vivo +teleport-dialog-description = Selecione um streamer para enviar seus espectadores ao canal dele. +teleport-unknown-streamer = Streamer desconhecido +teleport-search-label = Buscar transmissões ao vivo +teleport-search-placeholder = Buscar por @ ou título +teleport-loading-streamers = Carregando streamers ao vivo… +teleport-empty-streamers = Nenhum streamer ao vivo encontrado. +teleport-no-matching-streamers = Nenhum streamer ao vivo corresponde à busca. +teleport-viewer-count = { $count -> + [one] { $count } espectador + *[other] { $count } espectadores +} +teleport-countdown = Contagem regressiva +teleport-seconds-range = segundos (5–300) +teleport-countdown-error = A contagem deve ficar entre 5 e 300 segundos. +teleport-selection-expired = Esse streamer não está mais ao vivo. Escolha outro. +teleport-starting = Iniciando… +teleport-start = Teletransportar +teleport-error-streamer-only = Apenas o streamer da transmissão atual pode iniciar um teletransporte. +teleport-error-handle-format = Digite um identificador válido, como handle.bsky.social. +teleport-error-countdown-number = A contagem deve ser um número de segundos. +teleport-error-self = Você não pode se teletransportar para si mesmo. +teleport-error-resolve-handle = Não foi possível encontrar @{ $handle }. +teleport-error-create = Não foi possível iniciar o teletransporte. +teleport-error-generic = Não foi possível iniciar o teletransporte: { $message } diff --git a/js/i18n/locales/ro-RO/common.ftl b/js/i18n/locales/ro-RO/common.ftl index 0187c4b35..843082df4 100644 --- a/js/i18n/locales/ro-RO/common.ftl +++ b/js/i18n/locales/ro-RO/common.ftl @@ -80,3 +80,30 @@ login-show-live-on-bluesky-description = Adaugă inelul roșu LIVE avatarului dv ## Stream notifications teleporting-in = Teleportare în teleporting-to = Teleportare către @{ $handle } + +## Teleport dialog +teleport-dialog-title = Teleportează-te la un alt streamer live +teleport-dialog-description = Alege un streamer către care să-ți teleportezi spectatorii. +teleport-unknown-streamer = Streamer necunoscut +teleport-search-label = Caută transmisiuni live +teleport-search-placeholder = Caută după handle sau titlu +teleport-loading-streamers = Se încarcă streamerii live… +teleport-empty-streamers = Nu s-au găsit streameri live. +teleport-no-matching-streamers = Niciun streamer live nu corespunde căutării. +teleport-viewer-count = { $count -> + [one] { $count } spectator + *[other] { $count } spectatori +} +teleport-countdown = Numărătoare inversă +teleport-seconds-range = secunde (5–300) +teleport-countdown-error = Numărătoarea trebuie să fie între 5 și 300 de secunde. +teleport-selection-expired = Acest streamer nu mai este live. Alege altul. +teleport-starting = Se pornește… +teleport-start = Teleportează +teleport-error-streamer-only = Doar streamerul transmisiunii curente poate porni o teleportare. +teleport-error-handle-format = Introdu un handle valid, de exemplu handle.bsky.social. +teleport-error-countdown-number = Numărătoarea trebuie să fie un număr de secunde. +teleport-error-self = Nu te poți teleporta la tine însuți. +teleport-error-resolve-handle = Nu s-a putut găsi @{ $handle }. +teleport-error-create = Nu s-a putut porni teleportarea. +teleport-error-generic = Nu s-a putut porni teleportarea: { $message } diff --git a/js/i18n/locales/zh-Hans/common.ftl b/js/i18n/locales/zh-Hans/common.ftl index c45a12883..f01b99c8c 100644 --- a/js/i18n/locales/zh-Hans/common.ftl +++ b/js/i18n/locales/zh-Hans/common.ftl @@ -75,3 +75,27 @@ login-show-live-on-bluesky-description = 直播时为您的 Bluesky 头像加上 ## Stream notifications teleporting-in = 传送倒计时 teleporting-to = 正在传送至 @{ $handle } + +## Teleport dialog +teleport-dialog-title = 传送至其他直播主播 +teleport-dialog-description = 选择一位主播,将观众传送到对方的直播间。 +teleport-unknown-streamer = 未知主播 +teleport-search-label = 搜索直播 +teleport-search-placeholder = 按账号或标题搜索 +teleport-loading-streamers = 正在加载直播主播… +teleport-empty-streamers = 未找到直播主播。 +teleport-no-matching-streamers = 没有符合条件的直播主播。 +teleport-viewer-count = { $count } 位观众 +teleport-countdown = 倒计时 +teleport-seconds-range = 秒(5–300) +teleport-countdown-error = 倒计时必须为 5 到 300 秒。 +teleport-selection-expired = 该主播已不在直播。请选择其他主播。 +teleport-starting = 正在启动… +teleport-start = 开始传送 +teleport-error-streamer-only = 只有当前直播的主播可以发起传送。 +teleport-error-handle-format = 请输入有效账号,例如 handle.bsky.social。 +teleport-error-countdown-number = 倒计时必须是秒数。 +teleport-error-self = 无法传送到自己的直播间。 +teleport-error-resolve-handle = 找不到 @{ $handle }。 +teleport-error-create = 无法启动传送。 +teleport-error-generic = 无法启动传送:{ $message } diff --git a/js/i18n/locales/zh-Hant/common.ftl b/js/i18n/locales/zh-Hant/common.ftl index 9ec07cc46..eef4794e5 100644 --- a/js/i18n/locales/zh-Hant/common.ftl +++ b/js/i18n/locales/zh-Hant/common.ftl @@ -80,3 +80,27 @@ login-show-live-on-bluesky-description = 直播時為您的 Bluesky 頭像加上 ## Stream notifications teleporting-in = 傳送倒數 teleporting-to = 正在傳送至 @{ $handle } + +## Teleport dialog +teleport-dialog-title = 傳送至其他直播主 +teleport-dialog-description = 選擇一位直播主,將觀眾傳送到對方的直播間。 +teleport-unknown-streamer = 未知直播主 +teleport-search-label = 搜尋直播 +teleport-search-placeholder = 依帳號或標題搜尋 +teleport-loading-streamers = 正在載入直播主… +teleport-empty-streamers = 找不到直播主。 +teleport-no-matching-streamers = 沒有符合條件的直播主。 +teleport-viewer-count = { $count } 位觀眾 +teleport-countdown = 倒數計時 +teleport-seconds-range = 秒(5–300) +teleport-countdown-error = 倒數時間必須介於 5 到 300 秒。 +teleport-selection-expired = 這位直播主已不在直播。請選擇其他直播主。 +teleport-starting = 正在啟動… +teleport-start = 開始傳送 +teleport-error-streamer-only = 只有目前直播的直播主可以發起傳送。 +teleport-error-handle-format = 請輸入有效帳號,例如 handle.bsky.social。 +teleport-error-countdown-number = 倒數時間必須是秒數。 +teleport-error-self = 無法傳送到自己的直播間。 +teleport-error-resolve-handle = 找不到 @{ $handle }。 +teleport-error-create = 無法啟動傳送。 +teleport-error-generic = 無法啟動傳送:{ $message } diff --git a/js/web/src/components/stream/chat-input.test.tsx b/js/web/src/components/stream/chat-input.test.tsx new file mode 100644 index 000000000..5374e01ff --- /dev/null +++ b/js/web/src/components/stream/chat-input.test.tsx @@ -0,0 +1,131 @@ +import { makeLivestreamStore, registerSlashCommand } from "@streamplace/core"; +import { act } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { ChatInput } from "./chat-input"; + +const { mockSend, mockText, mockUseSession, mockClearContent } = vi.hoisted( + () => ({ + mockSend: vi.fn(), + mockText: { current: "" }, + mockUseSession: vi.fn(), + mockClearContent: vi.fn(), + }), +); + +vi.mock("@/lib/session", () => ({ useSession: mockUseSession })); +vi.mock("../../hooks/use-chat-send", () => ({ useChatSend: () => mockSend })); +vi.mock("../../hooks/use-skin-tone", () => ({ + skinToneIndex: () => 0, + useSkinTone: () => [0, vi.fn()], +})); +vi.mock("../../lib/emoji-data", () => ({ + getEmojiData: vi.fn(), + getSkinNative: vi.fn(), + searchEmojis: vi.fn(), + useEmojiData: () => null, +})); +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key }), +})); +vi.mock("@tiptap/react", () => ({ + EditorContent: () => ( +