From f27527aa130ebae355a61de443c42dde1b71dc46 Mon Sep 17 00:00:00 2001 From: BP602 <3460479+BP602@users.noreply.github.com> Date: Thu, 2 Oct 2025 14:48:23 +0200 Subject: [PATCH 1/2] fix: throttle analytics prompt to monthly --- .../assets/styles/components/Chat/Input.scss | 70 +++++++++ .../src/components/Chat/Input/index.jsx | 144 +++++++++++++++++- 2 files changed, 213 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/assets/styles/components/Chat/Input.scss b/src/renderer/src/assets/styles/components/Chat/Input.scss index 7835e5c..bbdbce9 100644 --- a/src/renderer/src/assets/styles/components/Chat/Input.scss +++ b/src/renderer/src/assets/styles/components/Chat/Input.scss @@ -42,6 +42,76 @@ left: 0; z-index: 1; + > .chatTelemetryPrompt { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + width: calc(100% - 16px); + margin: 0 8px 8px; + padding: 10px 12px; + border: 1px solid var(--border-secondary); + border-radius: 6px; + background: var(--bg-tertiary); + color: var(--text-primary); + animation: slideAndFadeIn 0.2s ease-in-out forwards; + + > .chatTelemetryPromptText { + display: flex; + flex-direction: column; + gap: 2px; + font-size: 13px; + line-height: 1.4; + + > strong { + font-size: 14px; + color: var(--text-primary); + } + + > span { + color: var(--text-secondary); + } + } + + > .chatTelemetryPromptActions { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + + > button { + cursor: pointer; + border-radius: 4px; + padding: 4px 10px; + font-size: 12px; + transition: background-color 0.2s ease-in-out, border-color 0.2s ease-in-out, color 0.2s ease-in-out; + border: 1px solid transparent; + } + + > .chatTelemetryPromptEnable { + background: var(--btn-primary-bg); + color: var(--btn-primary-text); + border-color: var(--btn-primary-bg); + + &:hover { + background: var(--btn-primary-hover); + border-color: var(--btn-primary-hover); + } + } + + > .chatTelemetryPromptDismiss { + background: var(--btn-secondary-bg); + border-color: var(--btn-secondary-border); + color: var(--text-tertiary); + + &:hover { + color: var(--text-primary); + border-color: var(--btn-primary-hover); + } + } + } + } + > .chatInfoBar { display: flex; align-items: center; diff --git a/src/renderer/src/components/Chat/Input/index.jsx b/src/renderer/src/components/Chat/Input/index.jsx index ec26464..0111322 100644 --- a/src/renderer/src/components/Chat/Input/index.jsx +++ b/src/renderer/src/components/Chat/Input/index.jsx @@ -39,6 +39,7 @@ import { MessageParser } from "../../../utils/MessageParser"; import { isModeEnabled, chatModeMatches } from "../../../utils/ChatUtils"; import { recordChatModeFeatureUsage } from "../../../telemetry/chatModeTelemetry"; import { useAccessibleKickEmotes } from "./useAccessibleKickEmotes"; +import { useSettings } from "../../../providers/SettingsProvider"; const onError = (error) => { console.error(error); @@ -117,6 +118,71 @@ const isEmoteOnlyContent = (editor) => { const messageHistory = new Map(); +const TELEMETRY_PROMPT_DISMISSED_KEY = "kicktalk.telemetryPromptDismissed"; +const TELEMETRY_PROMPT_DISMISSAL_TTL_MS = 1000 * 60 * 60 * 24 * 30; // 30 days + +const persistTelemetryPromptDismissal = () => { + if (typeof window === "undefined") return null; + + const record = { dismissedAt: new Date().toISOString() }; + + try { + window.localStorage.setItem( + TELEMETRY_PROMPT_DISMISSED_KEY, + JSON.stringify(record), + ); + } catch (error) { + console.warn("[ChatInput]: Failed to persist telemetry prompt dismissal", error); + return null; + } + + return record; +}; + +const readTelemetryPromptDismissal = () => { + if (typeof window === "undefined") return null; + + try { + const storedValue = window.localStorage.getItem(TELEMETRY_PROMPT_DISMISSED_KEY); + if (!storedValue) return null; + + if (storedValue === "true") { + // migrate legacy boolean flag to structured record + return persistTelemetryPromptDismissal(); + } + + try { + const parsed = JSON.parse(storedValue); + if (parsed?.dismissedAt) { + const dismissedAt = new Date(parsed.dismissedAt); + if (!Number.isNaN(dismissedAt.getTime())) { + return { dismissedAt: dismissedAt.toISOString() }; + } + } + } catch { + // fall through to attempt parsing a plain ISO string + } + + const asDate = new Date(storedValue); + if (!Number.isNaN(asDate.getTime())) { + const record = { dismissedAt: asDate.toISOString() }; + try { + window.localStorage.setItem( + TELEMETRY_PROMPT_DISMISSED_KEY, + JSON.stringify(record), + ); + } catch (error) { + console.warn("[ChatInput]: Failed to migrate telemetry prompt dismissal", error); + } + return record; + } + } catch (error) { + console.warn("[ChatInput]: Failed to read telemetry prompt dismissal state", error); + } + + return null; +}; + const EmoteSuggestions = memo( ({ suggestions, onSelect, selectedIndex, userChatroomInfo }) => { const suggestionsRef = useRef(null); @@ -1160,6 +1226,7 @@ const ReplyHandler = ({ chatroomId, getReplyData, clearReplyData, allStvEmotes, const ChatInput = memo( ({ chatroomId, isReplyThread = false, replyMessage = {}, settings }) => { + const { updateSettings: updateAppSettings } = useSettings(); const sendMessage = useChatStore((state) => state.sendMessage); const sendReply = useChatStore((state) => state.sendReply); const clearDraftMessage = useChatStore((state) => state.clearDraftMessage); @@ -1183,6 +1250,57 @@ const ChatInput = memo( const allStvEmotes = useAllStvEmotes(chatroomId); + const [showTelemetryPrompt, setShowTelemetryPrompt] = useState(false); + + useEffect(() => { + if (!settings) { + setShowTelemetryPrompt(false); + return; + } + + if (settings?.telemetry?.enabled) { + setShowTelemetryPrompt(false); + return; + } + + const dismissalRecord = readTelemetryPromptDismissal(); + if (!dismissalRecord?.dismissedAt) { + setShowTelemetryPrompt(true); + return; + } + + const dismissedAt = new Date(dismissalRecord.dismissedAt); + if (Number.isNaN(dismissedAt.getTime())) { + setShowTelemetryPrompt(true); + return; + } + + const now = Date.now(); + const age = now - dismissedAt.getTime(); + setShowTelemetryPrompt(age >= TELEMETRY_PROMPT_DISMISSAL_TTL_MS); + }, [settings, settings?.telemetry?.enabled]); + + const handleEnableTelemetry = useCallback(async () => { + try { + await updateAppSettings?.("telemetry", { + ...settings?.telemetry, + enabled: true, + }); + } catch (error) { + console.warn("[ChatInput]: Failed to enable telemetry from prompt", error); + } + + persistTelemetryPromptDismissal(); + + setShowTelemetryPrompt(false); + }, [settings?.telemetry, updateAppSettings]); + + const handleDismissTelemetryPrompt = useCallback(() => { + persistTelemetryPromptDismissal(); + + setShowTelemetryPrompt(false); + }, []); + // Reset selected index when changing chatrooms useEffect(() => { const history = messageHistory.get(chatroomId); @@ -1316,6 +1434,29 @@ const ChatInput = memo( return (
+ {showTelemetryPrompt && ( +
+
+ Help improve KickTalk + Enable analytics so we can understand bugs and improve KickTalk. +
+
+ + +
+
+ )} {settings?.chatrooms?.showInfoBar && ( )} @@ -1378,7 +1519,8 @@ const ChatInput = memo( (prev, next) => prev.chatroomId === next.chatroomId && prev.replyMessage === next.replyMessage && - prev.settings?.chatrooms?.showInfoBar === next.settings?.chatrooms?.showInfoBar, + prev.settings?.chatrooms?.showInfoBar === next.settings?.chatrooms?.showInfoBar && + prev.settings?.telemetry?.enabled === next.settings?.telemetry?.enabled, ); export default ChatInput; From f94b9ff7f8a01386a92dfc0bb997a996c2f27105 Mon Sep 17 00:00:00 2001 From: BP602 Date: Fri, 3 Oct 2025 00:32:35 +0200 Subject: [PATCH 2/2] fix: stabilize telemetry prompt across chat contexts --- .../assets/styles/components/Chat/Input.scss | 100 +++++++++++------- .../src/components/Chat/Input/index.jsx | 38 +++---- .../src/components/Dialogs/ReplyThread.jsx | 7 +- src/renderer/src/dialogs/ReplyThread.jsx | 7 +- 4 files changed, 95 insertions(+), 57 deletions(-) diff --git a/src/renderer/src/assets/styles/components/Chat/Input.scss b/src/renderer/src/assets/styles/components/Chat/Input.scss index bbdbce9..159bedb 100644 --- a/src/renderer/src/assets/styles/components/Chat/Input.scss +++ b/src/renderer/src/assets/styles/components/Chat/Input.scss @@ -43,9 +43,10 @@ z-index: 1; > .chatTelemetryPrompt { + position: relative; display: flex; - align-items: flex-start; - justify-content: space-between; + flex-direction: column; + align-items: stretch; gap: 12px; width: calc(100% - 16px); margin: 0 8px 8px; @@ -56,57 +57,82 @@ color: var(--text-primary); animation: slideAndFadeIn 0.2s ease-in-out forwards; - > .chatTelemetryPromptText { + > .chatTelemetryPromptDismiss { + position: absolute; + top: 8px; + right: 8px; display: flex; - flex-direction: column; - gap: 2px; - font-size: 13px; - line-height: 1.4; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + padding: 0; + background: transparent; + border-color: transparent; + color: var(--text-secondary); - > strong { - font-size: 14px; + &:hover { + background: var(--bg-hover); color: var(--text-primary); + border-color: var(--border-hover); } - > span { - color: var(--text-secondary); + &:focus-visible { + outline: 2px solid var(--border-focus); + outline-offset: 2px; } } - > .chatTelemetryPromptActions { + > .chatTelemetryPromptContent { display: flex; - align-items: center; - gap: 8px; - flex-wrap: wrap; + align-items: flex-end; + justify-content: space-between; + gap: 12px; - > button { - cursor: pointer; - border-radius: 4px; - padding: 4px 10px; - font-size: 12px; - transition: background-color 0.2s ease-in-out, border-color 0.2s ease-in-out, color 0.2s ease-in-out; - border: 1px solid transparent; - } + > .chatTelemetryPromptText { + display: flex; + flex-direction: column; + gap: 4px; + font-size: 13px; + line-height: 1.4; - > .chatTelemetryPromptEnable { - background: var(--btn-primary-bg); - color: var(--btn-primary-text); - border-color: var(--btn-primary-bg); + > strong { + font-size: 14px; + color: var(--text-primary); + } - &:hover { - background: var(--btn-primary-hover); - border-color: var(--btn-primary-hover); + > span { + color: var(--text-secondary); } } - > .chatTelemetryPromptDismiss { - background: var(--btn-secondary-bg); - border-color: var(--btn-secondary-border); - color: var(--text-tertiary); + > .chatTelemetryPromptActions { + display: flex; + align-items: center; + gap: 8px; + margin-top: 0; + flex-shrink: 0; + + > button { + cursor: pointer; + border-radius: 4px; + padding: 4px 10px; + font-size: 12px; + white-space: nowrap; + transition: background-color 0.2s ease-in-out, border-color 0.2s ease-in-out, color 0.2s ease-in-out; + border: 1px solid transparent; + } - &:hover { - color: var(--text-primary); - border-color: var(--btn-primary-hover); + > .chatTelemetryPromptEnable { + align-self: flex-start; + background: var(--btn-primary-bg); + color: var(--btn-primary-text); + border-color: var(--btn-primary-bg); + + &:hover { + background: var(--btn-primary-hover); + border-color: var(--btn-primary-hover); + } } } } diff --git a/src/renderer/src/components/Chat/Input/index.jsx b/src/renderer/src/components/Chat/Input/index.jsx index 0111322..c0a1fc3 100644 --- a/src/renderer/src/components/Chat/Input/index.jsx +++ b/src/renderer/src/components/Chat/Input/index.jsx @@ -1436,24 +1436,26 @@ const ChatInput = memo(
{showTelemetryPrompt && (
-
- Help improve KickTalk - Enable analytics so we can understand bugs and improve KickTalk. -
-
- - + +
+
+ Help improve KickTalk + Share anonymous usage analytics so we can spot bugs sooner and polish the chat experience. +
+
+ +
)} diff --git a/src/renderer/src/components/Dialogs/ReplyThread.jsx b/src/renderer/src/components/Dialogs/ReplyThread.jsx index 162f061..2f4de03 100644 --- a/src/renderer/src/components/Dialogs/ReplyThread.jsx +++ b/src/renderer/src/components/Dialogs/ReplyThread.jsx @@ -107,7 +107,12 @@ const ReplyThread = () => {
{originalMessage?.original_message?.id && ( - + )}
diff --git a/src/renderer/src/dialogs/ReplyThread.jsx b/src/renderer/src/dialogs/ReplyThread.jsx index ff0c4bd..a5592a4 100644 --- a/src/renderer/src/dialogs/ReplyThread.jsx +++ b/src/renderer/src/dialogs/ReplyThread.jsx @@ -5,5 +5,10 @@ import "@utils/themeUtils"; import React from "react"; import ReactDOM from "react-dom/client"; import ReplyThread from "../components/Dialogs/ReplyThread.jsx"; +import SettingsProvider from "../providers/SettingsProvider.jsx"; -ReactDOM.createRoot(document.getElementById("root")).render(); +ReactDOM.createRoot(document.getElementById("root")).render( + + + , +);