Skip to content

Commit

Permalink
feature: show survey card on Home page (#8078)
Browse files Browse the repository at this point in the history
* add custom style

* Update Publish.tsx

* display card on Home page

* Update constants.tsx

* Update shell.ts

* Update Home.tsx

* move survey into separate component

* get more info automatically

* move URL into constant

* move more props around

* Update constants.tsx

* turn non-rendering component into hook

* move card to design page

* start adding ipcRenderer stuff

* update card design

* get machineID and stash in Recoil

* read OS and place in URL

* add spacer to notification card

* better notification styling

* fix constants and typercheck error
  • Loading branch information
beyackle committed Jun 21, 2021
1 parent b9ddfc4 commit 5b9f164
Show file tree
Hide file tree
Showing 15 changed files with 195 additions and 57 deletions.
5 changes: 5 additions & 0 deletions Composer/packages/client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const App: React.FC = () => {
checkNodeVersion,
performAppCleanupOnQuit,
setSurveyEligibility,
setMachineInfo,
} = useRecoilValue(dispatcherState);

useEffect(() => {
Expand All @@ -42,6 +43,10 @@ export const App: React.FC = () => {
ipcRenderer?.on('cleanup', (_event) => {
performAppCleanupOnQuit();
});

ipcRenderer?.on('machine-info', (_event, info) => {
setMachineInfo(info);
});
setSurveyEligibility();
}, []);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useRef } from 'react';
import { FontSizes, SharedColors } from '@uifabric/fluent-theme';
import { Shimmer, ShimmerElementType } from 'office-ui-fabric-react/lib/Shimmer';
import { Icon } from 'office-ui-fabric-react/lib/Icon';
import { Stack, IStackProps } from 'office-ui-fabric-react/lib/Stack';
import formatMessage from 'format-message';
import { Notification, NotificationLink } from '@botframework-composer/types';

Expand Down Expand Up @@ -36,7 +37,7 @@ const cardContainer = (show: boolean, ref?: HTMLDivElement | null) => () => {
border-left: 4px solid #0078d4;
background: white;
box-shadow: 0 6.4px 14.4px 0 rgba(0, 0, 0, 0.132), 0 1.2px 3.6px 0 rgba(0, 0, 0, 0.108);
min-width: 340px;
width: 340px;
border-radius: 2px;
display: flex;
flex-direction: column;
Expand Down Expand Up @@ -112,13 +113,20 @@ const cardDescription = css`
word-break: break-word;
`;

const linkButton = css`
color: #0078d4;
float: right;
font-size: 12px;
height: auto;
margin: 4px 0 4px 8px;
`;
const linkButton = {
root: {
padding: '0',
border: '0',
},
label: {
fontSize: '12px',
color: '#0078d4',
margin: '0',
},
textContainer: {
height: '16px',
},
};

const getShimmerStyles = {
root: {
Expand Down Expand Up @@ -148,27 +156,53 @@ export type NotificationProps = {
};

const makeLinkLabel = (link: NotificationLink) => (
<ActionButton css={linkButton} onClick={link.onClick}>
<ActionButton styles={linkButton} onClick={link.onClick}>
{link.label}
</ActionButton>
);

const defaultCardContentRenderer = (props: CardProps) => {
const { title, description, type, link, links } = props;
const { title, description, type, link, links, stretchLinks } = props;

const linkList = links ?? [link ?? null];

const stackProps: IStackProps = {
horizontalAlign: stretchLinks ? 'space-between' : 'end',
tokens: {
childrenGap: stretchLinks ? undefined : '20px',
padding: '0 16px 0 0',
maxHeight: '24px',
},
};

return (
<div css={cardContent}>
{type === 'error' && <Icon css={errorType} iconName="ErrorBadge" />}
{type === 'success' && <Icon css={successType} iconName="Completed" />}
{type === 'warning' && <Icon css={warningType} iconName="Warning" />}
{type === 'question' && <Icon css={questionType} iconName="UnknownSolid" />}
{type === 'congratulation' && <Icon css={congratulationType} iconName="Trophy2Solid" />}
{type === 'custom' && (
<Icon
css={css`
margin-top: ${iconMargin};
color: ${props.color ?? SharedColors.gray10};
`}
iconName={props.icon ?? 'UnknownSolid'}
/>
)}
<div css={cardDetail}>
<div css={cardTitle}>{title}</div>
{description && <div css={cardDescription}>{description}</div>}
{link && makeLinkLabel(link)}
{links?.map((link) => (
<div key={link.label}>{makeLinkLabel(link)}</div>
))}
<Stack horizontal {...stackProps}>
{linkList.map((link) =>
link != null ? (
<Stack.Item key={link.label}>{makeLinkLabel(link)}</Stack.Item>
) : (
<span key="blank" style={{ width: '25px' }} />
)
)}
</Stack>
{type === 'pending' && (
<Shimmer shimmerElements={[{ type: ShimmerElementType.line, height: 2 }]} styles={getShimmerStyles} />
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { useEffect } from 'react';
import { useRecoilValue } from 'recoil';

import { ClientStorage } from '../../utils/storage';
import { surveyEligibilityState, dispatcherState, machineInfoState } from '../../recoilModel/atoms/appState';
import { MachineInfo } from '../../recoilModel/types';
import { SURVEY_URL_BASE } from '../../constants';
import TelemetryClient from '../../telemetry/TelemetryClient';

function buildUrl(info: MachineInfo) {
// User OS
// hashed machineId
// composer version
// maybe include subscription ID; wait for global sign-in feature
// session ID (global telemetry GUID)
const version = process.env.COMPOSER_VERSION;

const parameters = {
Source: 'Composer',
os: info.os || 'Unknown',
machineId: info.id,
version,
};

return (
`${SURVEY_URL_BASE}?` +
Object.keys(parameters)
.map((key) => `${key}=${parameters[key]}`)
.join('&')
);
}

export function useSurveyNotification() {
const { addNotification, deleteNotification } = useRecoilValue(dispatcherState);
const surveyEligible = useRecoilValue(surveyEligibilityState);
const machineInfo = useRecoilValue(machineInfoState);

useEffect(() => {
const url = buildUrl(machineInfo);
deleteNotification('survey');

if (surveyEligible) {
const surveyStorage = new ClientStorage(window.localStorage, 'survey');
TelemetryClient.track('HATSSurveyOffered');

addNotification({
id: 'survey',
type: 'question',
title: 'Would you mind taking a quick survey?',
description: `We read every response and will use your feedback to improve Composer.`,
stretchLinks: true,
links: [
{
label: 'Take survey',
onClick: () => {
// This is safe; we control what the URL that gets built is
// eslint-disable-next-line security/detect-non-literal-fs-filename
window.open(url, '_blank');
TelemetryClient.track('HATSSurveyAccepted');
deleteNotification('survey');
},
},

{
// this is functionally identical to clicking the close box
label: 'Remind me later',
onClick: () => {
TelemetryClient.track('HATSSurveyDismissed');
deleteNotification('survey');
},
},
null,
{
label: 'No thanks',
onClick: () => {
TelemetryClient.track('HATSSurveyRejected');
deleteNotification('survey');
surveyStorage.set('optedOut', true);
},
},
],
});
}
}, []);
}
7 changes: 4 additions & 3 deletions Composer/packages/client/src/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -527,9 +527,10 @@ export const defaultBotPort = 3979;
export const defaultBotEndpoint = `http://localhost:${defaultBotPort}/api/messages`;

const DAYS_IN_MS = 1000 * 60 * 60 * 24;

export const SURVEY_PARAMETERS = {
daysUntilEligible: 5,
daysUntilEligible: 2,
timeUntilNextSurvey: 90 * DAYS_IN_MS,
chanceToAppear: 0.15,
chanceToAppear: 0.3,
};

export const SURVEY_URL_BASE = 'https://microsoft.qualtrics.com/jfe/form/SV_bwlHGwEO2UDwo2F';
3 changes: 3 additions & 0 deletions Composer/packages/client/src/pages/design/DesignPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Split, SplitMeasuredSizes } from '@geoffcox/react-splitter';
import { dispatcherState } from '../../recoilModel';
import { renderThinSplitter } from '../../components/Split/ThinSplitter';
import { Conversation } from '../../components/Conversation';
import { useSurveyNotification } from '../../components/Notifications/SurveyNotification';

import SideBar from './SideBar';
import CommandBar from './CommandBar';
Expand All @@ -33,6 +34,8 @@ const DesignPage: React.FC<RouteComponentProps<{ dialogId: string; projectId: st

const activeBot = skillId ?? projectId;

useSurveyNotification();

return (
<div css={contentWrapper} role="main">
<Split
Expand Down
1 change: 1 addition & 0 deletions Composer/packages/client/src/pages/home/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ const Home: React.FC<RouteComponentProps> = () => {
// disabled: botName ? false : true,
// },
];

return (
<div css={home.outline}>
<div css={home.page}>
Expand Down
34 changes: 0 additions & 34 deletions Composer/packages/client/src/pages/publish/Publish.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,40 +163,6 @@ const Publish: React.FC<RouteComponentProps<{ projectId: string; targetName?: st
}, [botList]);

useEffect(() => {
addNotification({
id: 'test',
type: 'question',
title: 'Would you like to take a survey?',
description: "This is where we would ask the user if they'd like to take a survey.",
links: [
{
label: 'Take the survey?',
onClick: () => {
console.log('clicked');
},
},
{
label: 'Opt out of this and future surveys',
onClick: () => {
console.log('opted out');
},
},
],
});

addNotification({
id: 'test2',
type: 'congratulation',
title: "You've published 10 bots!",
description: 'Congratulations!',
link: {
label: 'Got it',
onClick: () => {
console.log('clicked this');
},
},
});

// Clear intervals when unmount
return () => {
if (pollingUpdaterList) {
Expand Down
6 changes: 6 additions & 0 deletions Composer/packages/client/src/recoilModel/atoms/appState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
AppUpdateState,
BoilerplateVersion,
Notification,
MachineInfo,
} from '../../recoilModel/types';
import { getUserSettings } from '../utils';
import onboardingStorage from '../../utils/onboardingStorage';
Expand Down Expand Up @@ -370,6 +371,11 @@ export const surveyEligibilityState = atom<boolean>({
default: false,
});

export const machineInfoState = atom<MachineInfo>({
key: getFullyQualifiedKey('machineInfoState'),
default: { id: '', os: '' },
});

export const showGetStartedTeachingBubbleState = atom<boolean>({
key: getFullyQualifiedKey('showGetStartedTeachingBubbleState'),
default: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ import {
userHasNodeInstalledState,
applicationErrorState,
surveyEligibilityState,
machineInfoState,
showGetStartedTeachingBubbleState,
} from '../atoms/appState';
import { AppUpdaterStatus, CreationFlowStatus, CreationFlowType, SURVEY_PARAMETERS } from '../../constants';
import OnboardingState from '../../utils/onboardingStorage';
import { StateError, AppUpdateState } from '../../recoilModel/types';
import { StateError, AppUpdateState, MachineInfo } from '../../recoilModel/types';
import { DebugDrawerKeys } from '../../pages/design/DebugPanel/TabExtensions/types';
import httpClient from '../../utils/httpUtil';
import { ClientStorage } from '../../utils/storage';
Expand Down Expand Up @@ -160,7 +161,10 @@ export const applicationDispatcher = () => {
const surveyStorage = new ClientStorage(window.localStorage, 'survey');

const optedOut = surveyStorage.get('optedOut', false);
if (optedOut) return;
if (optedOut) {
set(surveyEligibilityState, false);
return;
}

let days = surveyStorage.get('days', 0);
const lastUsed = surveyStorage.get('dateLastUsed', null);
Expand All @@ -182,6 +186,10 @@ export const applicationDispatcher = () => {
surveyStorage.set('dateLastUsed', today);
});

const setMachineInfo = useRecoilCallback((callbackHelpers: CallbackInterface) => (info: MachineInfo) => {
callbackHelpers.set(machineInfoState, info);
});

const setShowGetStartedTeachingBubble = useRecoilCallback((callbackHelpers: CallbackInterface) => (show: boolean) => {
callbackHelpers.set(showGetStartedTeachingBubbleState, show);
});
Expand All @@ -203,6 +211,7 @@ export const applicationDispatcher = () => {
setDebugPanelExpansion,
setActiveTabInDebugPanel,
setSurveyEligibility,
setMachineInfo,
setShowGetStartedTeachingBubble,
};
};
5 changes: 5 additions & 0 deletions Composer/packages/client/src/recoilModel/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,8 @@ export type WebChatInspectionData = {
};

export type RuntimeOutputData = { standardOutput: string; standardError: BotStartError | null };

export type MachineInfo = {
id: string;
os: string;
};
1 change: 1 addition & 0 deletions Composer/packages/electron-server/src/electronWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export default class ElectronWindow {
this.currentBrowserWindow = new BrowserWindow(browserWindowOptions);
this.currentBrowserWindow.on('page-title-updated', (ev) => ev.preventDefault()); // preserve explicit window title
this.currentBrowserWindow.webContents.on('new-window', this.onOpenNewWindow.bind(this));
this.currentBrowserWindow.webContents.userAgent = 'Electron';
log('Rendered Electron window dimensions: ', this.currentBrowserWindow.getSize());
}

Expand Down
5 changes: 5 additions & 0 deletions Composer/packages/electron-server/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,11 +295,16 @@ async function run() {
setTimeout(() => startApp(signalThatMainWindowIsShowing), 500);

const mainWindow = getMainWindow();
const machineId = await getMachineId();

mainWindow?.webContents.send('session-update', 'session-started');

if (process.env.COMPOSER_DEV_TOOLS) {
mainWindow?.webContents.openDevTools();
}

log(`Machine ID is ${machineId}`);
mainWindow?.webContents.send('machine-info', { id: machineId, os: os.platform() });
});

// Quit when all windows are closed.
Expand Down
Loading

0 comments on commit 5b9f164

Please sign in to comment.