/** * 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, RefObject, useCallback, useEffect, useState, } from 'react'; import { Glyph, FlexColumn, FlexRow, Button, Checkbox, styled, Input, Link, } from '../ui'; import LegacyPopover from '../ui/components/Popover2'; import {LeftRailButton} from '../sandy-chrome/LeftRail'; import GK from '../fb-stubs/GK'; 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'; type PropsFromState = { sessionId: string | null; }; type State = { promptData: FeedbackPrompt | null; isShown: boolean; hasTriggered: boolean; }; 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={true} /> , {'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} ); } } class RatingButton extends Component { state: State = { promptData: null, isShown: false, hasTriggered: false, }; glyphRef: RefObject = React.createRef(); constructor(props: PropsFromState) { super(props); if (GK.get('flipper_enable_star_ratiings')) { UserFeedback.getPrompt().then((prompt) => { this.setState({promptData: prompt}); setTimeout(this.triggerPopover.bind(this), 30000); }); } } onClick() { const willBeShown = !this.state.isShown; this.setState({isShown: willBeShown, hasTriggered: true}); if (!willBeShown) { UserFeedback.dismiss(this.props.sessionId); } } submitRating(rating: number) { UserFeedback.submitRating(rating, this.props.sessionId); } submitComment( rating: number, comment: string, selectedPredefinedComments: Array, allowUserInfoSharing: boolean, ) { UserFeedback.submitComment( rating, comment, selectedPredefinedComments, allowUserInfoSharing, this.props.sessionId, ); } triggerPopover() { if (!this.state.hasTriggered) { this.setState({isShown: true, hasTriggered: true}); } } render() { const promptData = this.state.promptData; if (!promptData) { return null; } if ( !promptData.shouldPopup || (this.state.hasTriggered && !this.state.isShown) ) { return null; } return (
{this.state.isShown ? ( { this.setState({isShown: false}); }} dismiss={this.onClick.bind(this)} promptData={promptData} /> ) : null}
); } } 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 (GK.get('flipper_enable_star_ratiings') && !hasTriggered && loggedIn) { UserFeedback.getPrompt().then((prompt) => { setPromptData(prompt); setTimeout(triggerPopover, 30000); }); } }, [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 /> ); }