diff --git a/src/renderer/src/assets/styles/components/Chat/Input.scss b/src/renderer/src/assets/styles/components/Chat/Input.scss index 7835e5c..159bedb 100644 --- a/src/renderer/src/assets/styles/components/Chat/Input.scss +++ b/src/renderer/src/assets/styles/components/Chat/Input.scss @@ -42,6 +42,102 @@ left: 0; z-index: 1; + > .chatTelemetryPrompt { + position: relative; + display: flex; + flex-direction: column; + align-items: stretch; + 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; + + > .chatTelemetryPromptDismiss { + position: absolute; + top: 8px; + right: 8px; + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + padding: 0; + background: transparent; + border-color: transparent; + color: var(--text-secondary); + + &:hover { + background: var(--bg-hover); + color: var(--text-primary); + border-color: var(--border-hover); + } + + &:focus-visible { + outline: 2px solid var(--border-focus); + outline-offset: 2px; + } + } + + > .chatTelemetryPromptContent { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 12px; + + > .chatTelemetryPromptText { + display: flex; + flex-direction: column; + gap: 4px; + 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; + 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; + } + + > .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); + } + } + } + } + } + > .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..c0a1fc3 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,31 @@ const ChatInput = memo( return (
+ {showTelemetryPrompt && ( +
+ +
+
+ Help improve KickTalk + Share anonymous usage analytics so we can spot bugs sooner and polish the chat experience. +
+
+ +
+
+
+ )} {settings?.chatrooms?.showInfoBar && ( )} @@ -1378,7 +1521,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; 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( + + + , +);