/** * 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 /> ); }