Skip to content

Commit

Permalink
Add feedback modal (#857)
Browse files Browse the repository at this point in the history
This PR implements feedback functionality by gathering
`sentiment:positive`, `sentiment:negative` telemetry events.


https://github.com/user-attachments/assets/a0fb9e4a-1ae5-4224-a0e0-0ad9f90c9c0b


https://github.com/user-attachments/assets/739526e8-3885-4126-b953-b5e355a36ce0

---------

Co-authored-by: filip131311 <[email protected]>
  • Loading branch information
kacperkapusciak and filip131311 authored Dec 20, 2024
1 parent 1fa81b9 commit 3838dc8
Show file tree
Hide file tree
Showing 10 changed files with 458 additions and 14 deletions.
23 changes: 23 additions & 0 deletions packages/vscode-extension/src/common/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { TelemetryEventProperties } from "@vscode/extension-telemetry";
import { RecordingData } from "./Project";

export interface UtilsEventListener<T> {
(event: T): void;
}

export interface UtilsEventMap {
telemetryEnabledChanged: boolean;
}

export interface UtilsInterface {
getCommandsCurrentKeyBinding(commandName: string): Promise<string | undefined>;

Expand All @@ -16,4 +25,18 @@ export interface UtilsInterface {
openExternalUrl(uriString: string): Promise<void>;

log(type: "info" | "error" | "warn" | "log", message: string, ...args: any[]): Promise<void>;

sendTelemetry(eventName: string, properties?: TelemetryEventProperties): Promise<void>;

isTelemetryEnabled(): Promise<boolean>;

addListener<K extends keyof UtilsEventMap>(
eventType: K,
listener: UtilsEventListener<UtilsEventMap[K]>
): Promise<void>;

removeListener<K extends keyof UtilsEventMap>(
eventType: K,
listener: UtilsEventListener<UtilsEventMap[K]>
): Promise<void>;
}
34 changes: 33 additions & 1 deletion packages/vscode-extension/src/utilities/utils.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { homedir } from "node:os";
import { EventEmitter } from "stream";
import fs from "fs";
import path from "path";
import { commands, env, Uri, window } from "vscode";
import JSON5 from "json5";
import vscode from "vscode";
import { TelemetryEventProperties } from "@vscode/extension-telemetry";
import { Logger } from "../Logger";
import { extensionContext } from "./extensionContext";
import { openFileAtPosition } from "./openFileAtPosition";
import { UtilsInterface } from "../common/utils";
import { UtilsEventListener, UtilsEventMap, UtilsInterface } from "../common/utils";
import { Platform } from "./platform";
import { RecordingData } from "../common/Project";
import { getTelemetryReporter } from "./telemetry";

type KeybindingType = {
command: string;
Expand All @@ -19,6 +22,14 @@ type KeybindingType = {
};

export class Utils implements UtilsInterface {
private eventEmitter = new EventEmitter();

constructor() {
vscode.env.onDidChangeTelemetryEnabled((telemetryEnabled) => {
this.eventEmitter.emit("telemetryEnabledChanged", telemetryEnabled);
});
}

public async getCommandsCurrentKeyBinding(commandName: string) {
const packageJsonPath = path.join(extensionContext.extensionPath, "package.json");
const extensionPackageJson = require(packageJsonPath);
Expand Down Expand Up @@ -142,4 +153,25 @@ export class Utils implements UtilsInterface {
// Combine into the desired format
return `${year}-${month}-${day} ${hours}.${minutes}.${seconds}`;
}

public async sendTelemetry(eventName: string, properties?: TelemetryEventProperties) {
getTelemetryReporter().sendTelemetryEvent(eventName, properties);
}

public async isTelemetryEnabled() {
return vscode.env.isTelemetryEnabled;
}

async addListener<K extends keyof UtilsEventMap>(
eventType: K,
listener: UtilsEventListener<UtilsEventMap[K]>
) {
this.eventEmitter.addListener(eventType, listener);
}
async removeListener<K extends keyof UtilsEventMap>(
eventType: K,
listener: UtilsEventListener<UtilsEventMap[K]>
) {
this.eventEmitter.removeListener(eventType, listener);
}
}
76 changes: 76 additions & 0 deletions packages/vscode-extension/src/webview/components/Feedback.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
.feedback {
display: flex;
gap: 4px;
}

.feedback-button {
width: 26px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 3px;
transition: all 250ms 0ms ease-in-out;
}

.feedback-button:active {
transform: scale(0.94);
}

.feedback-button-large:active {
transform: scale(0.96);
}

.feedback-button:hover span,
.feedback-button-positive-active span,
.feedback-button-negative-active span {
color: var(--swm-popover-background) !important;
}

.feedback-button:hover {
cursor: pointer;
}

.feedback-button-large {
width: 64px;
height: 46px;
}

.feedback-button-large span {
font-size: 24px !important;
}

.feedback-button-positive {
color: var(--green-light-80);
border: 1px solid var(--green-light-80);
}

.feedback-button-positive span {
color: var(--green-light-80);
}

.feedback-button-positive:hover,
.feedback-button-positive-active {
background-color: var(--green-light-80);
}

.feedback-button-negative {
border: 1px solid var(--red-light-80);
}

.feedback-button-negative span {
color: var(--red-light-80);
}

.feedback-button-negative:hover,
.feedback-button-negative-active {
background-color: var(--red-light-80);
}

.feedback-prompt {
cursor: pointer;
font-size: 12px;
}
.feedback-prompt:hover {
text-decoration: underline;
}
77 changes: 77 additions & 0 deletions packages/vscode-extension/src/webview/components/Feedback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Dispatch, MouseEvent, SetStateAction } from "react";
import "./Feedback.css";
import classNames from "classnames";
import { useUtils } from "../providers/UtilsProvider";
import { Sentiment } from "./SendFeedbackItem";

type FeedbackProps = {
sentiment: Sentiment | undefined;
setSentiment: Dispatch<SetStateAction<Sentiment | undefined>>;
};

export function Feedback({ sentiment, setSentiment }: FeedbackProps) {
const { sendTelemetry } = useUtils();

const handleFeedback = (event: MouseEvent<HTMLButtonElement>, pickedSentiment: Sentiment) => {
event.preventDefault();
sendTelemetry(`feedback:${pickedSentiment}`);
setSentiment(pickedSentiment);
};

return (
<div className="feedback">
{Boolean(sentiment) ? (
<p className="feedback-prompt">
{sentiment === "positive" ? "What went well?" : "Tell us more"}
</p>
) : (
<>
<FeedbackButton sentiment="positive" onClick={(e) => handleFeedback(e, "positive")} />
<FeedbackButton sentiment="negative" onClick={(e) => handleFeedback(e, "negative")} />
</>
)}
</div>
);
}

type FeedbackButtonProps = {
sentiment: Sentiment;
onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
type?: "small" | "large";
isActive?: boolean;
};

export function FeedbackButton({
sentiment,
onClick,
type = "small",
isActive,
}: FeedbackButtonProps) {
if (sentiment === "positive") {
return (
<button
className={classNames(
"feedback-button feedback-button-positive",
isActive && "feedback-button-positive-active",
type === "large" && "feedback-button-large"
)}
type="button"
onClick={onClick}>
<span className="codicon codicon-thumbsup" />
</button>
);
}

return (
<button
className={classNames(
"feedback-button feedback-button-negative",
isActive && "feedback-button-negative-active",
type === "large" && "feedback-button-large"
)}
type="button"
onClick={onClick}>
<span className="codicon codicon-thumbsdown" />
</button>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useState } from "react";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";

import { useModal } from "../providers/ModalProvider";
import FeedbackView from "../views/FeedbackView";
import { Feedback } from "./Feedback";

export type Sentiment = "positive" | "negative";

export function SendFeedbackItem() {
const { openModal } = useModal();

const [sentiment, setSentiment] = useState<Sentiment | undefined>();

return (
<DropdownMenu.Item
className="dropdown-menu-item"
onSelect={() => {
openModal(
"Do you enjoy using Radon IDE today?",
<FeedbackView initialSentiment={sentiment} />
);
}}>
<span className="codicon codicon-feedback" />
<div className="dropdown-menu-item-content">
Send feedback
<Feedback sentiment={sentiment} setSentiment={setSentiment} />
</div>
</DropdownMenu.Item>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { KeybindingInfo } from "./shared/KeybindingInfo";
import { useUtils } from "../providers/UtilsProvider";
import "./shared/SwitchGroup.css";
import LaunchConfigurationView from "../views/LaunchConfigurationView";
import { SendFeedbackItem } from "./SendFeedbackItem";
import { useTelemetry } from "../providers/TelemetryProvider";

interface SettingsDropdownProps {
children: React.ReactNode;
Expand All @@ -23,6 +25,7 @@ function SettingsDropdown({ project, isDeviceRunning, children, disabled }: Sett
const { panelLocation, update } = useWorkspaceConfig();
const { openModal } = useModal();
const { movePanelToNewWindow, reportIssue } = useUtils();
const { telemetryEnabled } = useTelemetry();

return (
<DropdownMenu.Root>
Expand Down Expand Up @@ -142,6 +145,7 @@ function SettingsDropdown({ project, isDeviceRunning, children, disabled }: Sett
<div className="dropdown-menu-item-content">Report Issue</div>
</span>
</DropdownMenu.Item>
{telemetryEnabled && <SendFeedbackItem />}
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
Expand Down
29 changes: 16 additions & 13 deletions packages/vscode-extension/src/webview/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import WorkspaceConfigProvider from "./providers/WorkspaceConfigProvider";

import "./styles/theme.css";
import { UtilsProvider, installLogOverrides } from "./providers/UtilsProvider";
import { TelemetryProvider } from "./providers/TelemetryProvider";
import LaunchConfigProvider from "./providers/LaunchConfigProvider";

installLogOverrides();
Expand All @@ -22,19 +23,21 @@ root.render(
<React.StrictMode>
<ProjectProvider>
<UtilsProvider>
<WorkspaceConfigProvider>
<LaunchConfigProvider>
<DevicesProvider>
<DependenciesProvider>
<ModalProvider>
<AlertProvider>
<App />
</AlertProvider>
</ModalProvider>
</DependenciesProvider>
</DevicesProvider>
</LaunchConfigProvider>
</WorkspaceConfigProvider>
<TelemetryProvider>
<WorkspaceConfigProvider>
<LaunchConfigProvider>
<DevicesProvider>
<DependenciesProvider>
<ModalProvider>
<AlertProvider>
<App />
</AlertProvider>
</ModalProvider>
</DependenciesProvider>
</DevicesProvider>
</LaunchConfigProvider>
</WorkspaceConfigProvider>
</TelemetryProvider>
</UtilsProvider>
</ProjectProvider>
</React.StrictMode>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { createContext, PropsWithChildren, useContext, useEffect, useMemo, useState } from "react";
import { useUtils } from "./UtilsProvider";

type TelemetryContextProps = { telemetryEnabled: boolean };

const TelemetryContext = createContext<TelemetryContextProps>({ telemetryEnabled: false });

export function TelemetryProvider({ children }: PropsWithChildren) {
const utils = useUtils();
const [telemetryEnabled, setTelemetryEnabled] = useState(false);

useEffect(() => {
utils.isTelemetryEnabled().then(setTelemetryEnabled);
utils.addListener("telemetryEnabledChanged", setTelemetryEnabled);

return () => {
utils.removeListener("telemetryEnabledChanged", setTelemetryEnabled);
};
}, []);

const contextValue = useMemo(() => ({ telemetryEnabled }), [telemetryEnabled]);

return <TelemetryContext.Provider value={contextValue}>{children}</TelemetryContext.Provider>;
}

export function useTelemetry() {
const context = useContext(TelemetryContext);

if (context === undefined) {
throw new Error("useTelemetry must be used within a TelemetryContextProvider");
}
return context;
}
Loading

0 comments on commit 3838dc8

Please sign in to comment.