/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import React, {
Component,
ReactElement,
useCallback,
useEffect,
useState,
} from 'react';
import {
FlexColumn,
FlexRow,
Button,
Checkbox,
styled,
Input,
Link,
} from '../ui';
import {LeftRailButton} from '../sandy-chrome/LeftRail';
import * as UserFeedback from '../fb-stubs/UserFeedback';
import {FeedbackPrompt} from '../fb-stubs/UserFeedback';
import {StarOutlined} from '@ant-design/icons';
import {Popover, Rate} from 'antd';
import {useStore} from '../utils/useStore';
import {isLoggedIn} from '../fb-stubs/user';
import {useValue} from 'flipper-plugin';
import {reportPlatformFailures} from 'flipper-common';
import {getRenderHostInstance} from '../RenderHost';
type NextAction = 'select-rating' | 'leave-comment' | 'finished';
class PredefinedComment extends Component<{
comment: string;
selected: boolean;
onClick: (_: unknown) => unknown;
}> {
static Container = styled.div<{selected: boolean}>((props) => {
return {
border: '1px solid #f2f3f5',
cursor: 'pointer',
borderRadius: 24,
backgroundColor: props.selected ? '#ecf3ff' : '#f2f3f5',
marginBottom: 4,
marginRight: 4,
padding: '4px 8px',
color: props.selected ? 'rgb(56, 88, 152)' : undefined,
borderColor: props.selected ? '#3578e5' : undefined,
':hover': {
borderColor: '#3578e5',
},
};
});
render() {
return (
{this.props.comment}
);
}
}
const Row = styled(FlexRow)({
marginTop: 5,
marginBottom: 5,
justifyContent: 'center',
textAlign: 'center',
color: '#9a9a9a',
flexWrap: 'wrap',
});
const DismissRow = styled(Row)({
marginBottom: 0,
marginTop: 10,
});
const DismissButton = styled.span({
'&:hover': {
textDecoration: 'underline',
cursor: 'pointer',
},
});
const Spacer = styled(FlexColumn)({
flexGrow: 1,
});
function dismissRow(dismiss: () => void) {
return (
Dismiss
);
}
type FeedbackComponentState = {
rating: number | null;
hoveredRating: number;
allowUserInfoSharing: boolean;
nextAction: NextAction;
predefinedComments: {[key: string]: boolean};
comment: string;
};
class FeedbackComponent extends Component<
{
submitRating: (rating: number) => void;
submitComment: (
rating: number,
comment: string,
selectedPredefinedComments: Array,
allowUserInfoSharing: boolean,
) => void;
close: () => void;
dismiss: () => void;
promptData: FeedbackPrompt;
},
FeedbackComponentState
> {
state: FeedbackComponentState = {
rating: null,
hoveredRating: 0,
allowUserInfoSharing: true,
nextAction: 'select-rating' as NextAction,
predefinedComments: this.props.promptData.predefinedComments.reduce(
(acc, cv) => ({...acc, [cv]: false}),
{},
),
comment: '',
};
onSubmitRating(newRating: number) {
const nextAction = newRating <= 2 ? 'leave-comment' : 'finished';
this.setState({rating: newRating, nextAction: nextAction});
this.props.submitRating(newRating);
if (nextAction === 'finished') {
setTimeout(this.props.close, 5000);
}
}
onCommentSubmitted(comment: string) {
this.setState({nextAction: 'finished'});
const selectedPredefinedComments: Array = Object.entries(
this.state.predefinedComments,
)
.map((x) => ({comment: x[0], enabled: x[1]}))
.filter((x) => x.enabled)
.map((x) => x.comment);
const currentRating = this.state.rating;
if (currentRating) {
this.props.submitComment(
currentRating,
comment,
selectedPredefinedComments,
this.state.allowUserInfoSharing,
);
} else {
console.error('Illegal state: Submitting comment with no rating set.');
}
setTimeout(this.props.close, 1000);
}
onAllowUserSharingChanged(allowed: boolean) {
this.setState({allowUserInfoSharing: allowed});
}
render() {
let body: Array;
switch (this.state.nextAction) {
case 'select-rating':
body = [
{this.props.promptData.bodyText}
,
this.onSubmitRating(newRating)} />
,
dismissRow(this.props.dismiss),
];
break;
case 'leave-comment':
const predefinedComments = Object.entries(
this.state.predefinedComments,
).map((c: [string, unknown], idx: number) => (
this.setState({
predefinedComments: {
...this.state.predefinedComments,
[c[0]]: !c[1],
},
})
}
/>
));
body = [
{predefinedComments}
,
this.setState({comment: e.target.value})}
onKeyDown={(e) =>
e.key == 'Enter' && this.onCommentSubmitted(this.state.comment)
}
autoFocus
/>
,
{'Tool owner can contact me '}
,
,
dismissRow(this.props.dismiss),
];
break;
case 'finished':
body = [
Thanks for the feedback! You can now help
prioritize bugs and features for Flipper in Papercuts
,
dismissRow(this.props.dismiss),
];
break;
default: {
console.error('Illegal state: nextAction: ' + this.state.nextAction);
return null;
}
}
return (
{this.state.nextAction === 'finished'
? this.props.promptData.postSubmitHeading
: this.props.promptData.preSubmitHeading}
{body}
);
}
}
export function SandyRatingButton() {
const [promptData, setPromptData] =
useState(null);
const [isShown, setIsShown] = useState(false);
const [hasTriggered, setHasTriggered] = useState(false);
const sessionId = useStore((store) => store.application.sessionId);
const loggedIn = useValue(isLoggedIn());
const triggerPopover = useCallback(() => {
if (!hasTriggered) {
setIsShown(true);
setHasTriggered(true);
}
}, [hasTriggered]);
useEffect(() => {
if (
getRenderHostInstance().GK('flipper_enable_star_ratiings') &&
!hasTriggered &&
loggedIn
) {
reportPlatformFailures(
UserFeedback.getPrompt().then((prompt) => {
setPromptData(prompt);
setTimeout(triggerPopover, 30000);
}),
'RatingButton:getPrompt',
).catch((e) => {
console.warn('Failed to load ratings prompt:', e);
});
}
}, [triggerPopover, hasTriggered, loggedIn]);
const onClick = () => {
const willBeShown = !isShown;
setIsShown(willBeShown);
setHasTriggered(true);
if (!willBeShown) {
UserFeedback.dismiss(sessionId);
}
};
const submitRating = (rating: number) => {
UserFeedback.submitRating(rating, sessionId);
};
const submitComment = (
rating: number,
comment: string,
selectedPredefinedComments: Array,
allowUserInfoSharing: boolean,
) => {
UserFeedback.submitComment(
rating,
comment,
selectedPredefinedComments,
allowUserInfoSharing,
sessionId,
);
};
if (!promptData) {
return null;
}
if (!promptData.shouldPopup || (hasTriggered && !isShown)) {
return null;
}
return (
{
setIsShown(false);
}}
dismiss={onClick}
promptData={promptData}
/>
}
placement="right"
trigger="click">
}
title="Rate Flipper"
onClick={onClick}
small
/>
);
}