Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions src/renderer/src/assets/styles/components/Chat/Input.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
146 changes: 145 additions & 1 deletion src/renderer/src/components/Chat/Input/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -1316,6 +1434,31 @@ const ChatInput = memo(
return (
<div className="chatInputWrapper">
<div className="chatInputInfoBar">
{showTelemetryPrompt && (
<div className="chatTelemetryPrompt" role="alert">
<button
type="button"
className="chatTelemetryPromptDismiss"
onClick={handleDismissTelemetryPrompt}
aria-label="Dismiss analytics prompt">
<XIcon size={14} weight="bold" aria-hidden="true" />
</button>
<div className="chatTelemetryPromptContent">
<div className="chatTelemetryPromptText">
<strong>Help improve KickTalk</strong>
<span>Share anonymous usage analytics so we can spot bugs sooner and polish the chat experience.</span>
</div>
<div className="chatTelemetryPromptActions">
<button
type="button"
className="chatTelemetryPromptEnable"
onClick={handleEnableTelemetry}>
Share Analytics
</button>
</div>
</div>
</div>
)}
{settings?.chatrooms?.showInfoBar && (
<InfoBar chatroomInfo={chatroom?.chatroomInfo} initialChatroomInfo={chatroom?.initialChatroomInfo} />
)}
Expand Down Expand Up @@ -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;
7 changes: 6 additions & 1 deletion src/renderer/src/components/Dialogs/ReplyThread.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,12 @@ const ReplyThread = () => {

<div className="replyThreadInput">
{originalMessage?.original_message?.id && (
<ChatInput chatroomId={dialogData?.chatroomId} isReplyThread={true} replyMessage={originalMessage} />
<ChatInput
chatroomId={dialogData?.chatroomId}
isReplyThread={true}
replyMessage={originalMessage}
settings={dialogData?.settings}
/>
)}
</div>
</div>
Expand Down
7 changes: 6 additions & 1 deletion src/renderer/src/dialogs/ReplyThread.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<ReplyThread />);
ReactDOM.createRoot(document.getElementById("root")).render(
<SettingsProvider>
<ReplyThread />
</SettingsProvider>,
);