Skip to content

Commit df7b853

Browse files
committed
refactor feedback
1 parent ce64126 commit df7b853

3 files changed

Lines changed: 110 additions & 143 deletions

File tree

packages/component/src/Activity/ActivityFeedback.tsx

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
import { hooks } from 'botframework-webchat-api';
22
import { getOrgSchemaMessage, OrgSchemaAction, parseAction, WebChatActivity } from 'botframework-webchat-core';
33
import classNames from 'classnames';
4-
import React, { memo, useCallback, useMemo, useRef, useState } from 'react';
4+
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
55
import { defaultFeedbackEntities } from './private/DefaultFeedbackEntities';
66
import { isDefaultFeedbackActivity } from './private/isDefaultFeedbackActivity';
77

88
import Feedback from './private/Feedback';
99
import dereferenceBlankNodes from '../Utils/JSONLinkedData/dereferenceBlankNodes';
10-
import FeedbackForm, { FeedbackType } from './private/FeedbackForm';
10+
import FeedbackForm from './private/FeedbackForm';
1111
import { useStyleToEmotionObject } from '../hooks/internal/styleToEmotionObject';
12+
import { useRefFrom } from 'use-ref-from';
1213

13-
const { useStyleOptions } = hooks;
14+
const { useStyleOptions, usePostActivity, usePonyfill } = hooks;
1415

1516
type ActivityFeedbackProps = Readonly<{
1617
activity: WebChatActivity;
@@ -42,13 +43,17 @@ const useGetMessageThing = (activity: WebChatActivity) =>
4243
return { isFeedbackLoopSupported: false, messageThing, graph };
4344
}, [activity]);
4445

46+
const DEBOUNCE_TIMEOUT = 500;
47+
4548
function ActivityFeedback({ activity }: ActivityFeedbackProps) {
49+
const [{ clearTimeout, setTimeout }] = usePonyfill();
50+
const postActivity = usePostActivity();
51+
4652
const [{ feedbackActionsPlacement }] = useStyleOptions();
4753
const rootClassName = useStyleToEmotionObject()(ROOT_STYLE) + '';
54+
const postActivityRef = useRefFrom(postActivity);
4855

49-
const [showFeedbackForm, setShowFeedbackForm] = useState(false);
50-
const [feedbackType, setFeedbackType] = useState<string | undefined>(undefined);
51-
const resetFeedbackRef = useRef<() => void>();
56+
const [selectedAction, setSelectedAction] = useState<OrgSchemaAction | undefined>();
5257

5358
const { messageThing, graph, isFeedbackLoopSupported } = useGetMessageThing(activity);
5459

@@ -81,39 +86,52 @@ function ActivityFeedback({ activity }: ActivityFeedbackProps) {
8186
[activity, isFeedbackLoopSupported]
8287
);
8388

84-
const onFeedbackTypeChange = useCallback((newType?: string) => {
85-
setFeedbackType(newType);
86-
setShowFeedbackForm(newType !== undefined);
87-
if (newType === undefined) {
88-
resetFeedbackRef.current?.();
89-
}
89+
const handleFeedbackActionClick = useCallback((action?: OrgSchemaAction) => {
90+
setSelectedAction(action);
9091
}, []);
9192

93+
useEffect(() => {
94+
if (!selectedAction) {
95+
return;
96+
}
97+
98+
const timeout = setTimeout(
99+
() =>
100+
// TODO: We should update this to use W3C Hydra.1
101+
postActivityRef.current({
102+
entities: [selectedAction],
103+
name: 'webchat:activity-status/feedback',
104+
type: 'event'
105+
} as any),
106+
DEBOUNCE_TIMEOUT
107+
);
108+
109+
return () => clearTimeout(timeout);
110+
}, [clearTimeout, postActivityRef, selectedAction, setTimeout]);
111+
92112
const FeedbackComponent = useMemo(
93113
() => (
94114
<Feedback
95115
actions={feedbackActions}
96116
className={classNames({
97117
'webchat__thumb-button--large': feedbackActionsPlacement === 'activity-actions'
98118
})}
99-
handleFeedbackActionClick={onFeedbackTypeChange}
100-
isFeedbackFormSupported={isFeedbackLoopSupported}
101-
resetFeedbackRef={resetFeedbackRef}
119+
onHandleFeedbackActionClick={handleFeedbackActionClick}
102120
/>
103121
),
104-
[feedbackActions, feedbackActionsPlacement, isFeedbackLoopSupported, onFeedbackTypeChange]
122+
[feedbackActions, feedbackActionsPlacement, handleFeedbackActionClick]
105123
);
106124

107125
const FeedbackFormComponent = useMemo(
108126
() => (
109127
<FeedbackForm
110128
disclaimer={disclaimer}
111-
feedbackType={feedbackType as FeedbackType}
112-
handeFeedbackTypeChange={onFeedbackTypeChange}
129+
feedbackType={selectedAction?.['@type']}
130+
onResetFeedbackForm={handleFeedbackActionClick}
113131
replyToId={activity.id}
114132
/>
115133
),
116-
[activity.id, disclaimer, feedbackType, onFeedbackTypeChange]
134+
[activity.id, disclaimer, handleFeedbackActionClick, selectedAction]
117135
);
118136

119137
if (feedbackActionsPlacement === 'activity-actions' && isFeedbackLoopSupported) {
@@ -122,7 +140,7 @@ function ActivityFeedback({ activity }: ActivityFeedbackProps) {
122140
<div className={classNames('webchat__feedback-form__root-container__child', rootClassName)}>
123141
{FeedbackComponent}
124142
</div>
125-
{showFeedbackForm && feedbackType && FeedbackFormComponent}
143+
{selectedAction && selectedAction['@type'] && FeedbackFormComponent}
126144
</div>
127145
);
128146
}

packages/component/src/Activity/private/Feedback.tsx

Lines changed: 37 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,96 +1,53 @@
11
import { hooks } from 'botframework-webchat-api';
22
import { type OrgSchemaAction } from 'botframework-webchat-core';
3-
import React, { Fragment, memo, useEffect, useMemo, useState, type PropsWithChildren } from 'react';
4-
import { useRefFrom } from 'use-ref-from';
3+
import React, { Fragment, memo, useMemo, type PropsWithChildren } from 'react';
54

65
import FeedbackVoteButton from './VoteButton';
76

8-
const { usePonyfill, usePostActivity, useLocalizer } = hooks;
7+
const { useLocalizer } = hooks;
98

109
type Props = Readonly<
1110
PropsWithChildren<{
1211
actions: ReadonlySet<OrgSchemaAction>;
1312
className?: string | undefined;
14-
isFeedbackFormSupported?: boolean;
15-
handleFeedbackActionClick: (feedbackType?: string) => void;
16-
resetFeedbackRef: React.MutableRefObject<() => void>;
13+
onHandleFeedbackActionClick?: (action: OrgSchemaAction) => void;
14+
selectedAction?: OrgSchemaAction | undefined;
1715
}>
1816
>;
1917

20-
const DEBOUNCE_TIMEOUT = 500;
21-
22-
const Feedback = memo(
23-
({ actions, className, isFeedbackFormSupported, handleFeedbackActionClick, resetFeedbackRef }: Props) => {
24-
const [{ clearTimeout, setTimeout }] = usePonyfill();
25-
const [selectedAction, setSelectedAction] = useState<OrgSchemaAction | undefined>();
26-
const postActivity = usePostActivity();
27-
const localize = useLocalizer();
28-
29-
const postActivityRef = useRefFrom(postActivity);
30-
31-
useEffect(() => {
32-
resetFeedbackRef.current = () => {
33-
setSelectedAction(undefined);
34-
};
35-
// Only want to set the ref once
36-
// eslint-disable-next-line react-hooks/exhaustive-deps
37-
}, []);
38-
39-
useEffect(() => {
40-
if (!selectedAction) {
41-
return;
42-
}
43-
44-
if (isFeedbackFormSupported) {
45-
handleFeedbackActionClick(selectedAction['@type']);
46-
return;
47-
}
48-
49-
const timeout = setTimeout(
50-
() =>
51-
// TODO: We should update this to use W3C Hydra.1
52-
postActivityRef.current({
53-
entities: [selectedAction],
54-
name: 'webchat:activity-status/feedback',
55-
type: 'event'
56-
} as any),
57-
DEBOUNCE_TIMEOUT
58-
);
59-
60-
return () => clearTimeout(timeout);
61-
}, [clearTimeout, isFeedbackFormSupported, handleFeedbackActionClick, postActivityRef, selectedAction, setTimeout]);
62-
63-
const actionProps = useMemo(
64-
() =>
65-
[...actions].some(action => action.actionStatus === 'CompletedActionStatus')
66-
? {
67-
disabled: true,
68-
title: localize('VOTE_COMPLETE_ALT')
69-
}
70-
: undefined,
71-
[actions, localize]
72-
);
73-
74-
return (
75-
<Fragment>
76-
{[...actions].map((action, index) => (
77-
<FeedbackVoteButton
78-
action={action}
79-
className={className}
80-
key={action['@id'] || index}
81-
onClick={setSelectedAction}
82-
pressed={
83-
selectedAction === action ||
84-
action.actionStatus === 'CompletedActionStatus' ||
85-
action.actionStatus === 'ActiveActionStatus'
86-
}
87-
{...actionProps}
88-
/>
89-
))}
90-
</Fragment>
91-
);
92-
}
93-
);
18+
const Feedback = memo(({ actions, className, onHandleFeedbackActionClick, selectedAction }: Props) => {
19+
const localize = useLocalizer();
20+
21+
const actionProps = useMemo(
22+
() =>
23+
[...actions].some(action => action.actionStatus === 'CompletedActionStatus')
24+
? {
25+
disabled: true,
26+
title: localize('VOTE_COMPLETE_ALT')
27+
}
28+
: undefined,
29+
[actions, localize]
30+
);
31+
32+
return (
33+
<Fragment>
34+
{[...actions].map((action, index) => (
35+
<FeedbackVoteButton
36+
action={action}
37+
className={className}
38+
key={action['@id'] || index}
39+
onClick={onHandleFeedbackActionClick}
40+
pressed={
41+
selectedAction === action ||
42+
action.actionStatus === 'CompletedActionStatus' ||
43+
action.actionStatus === 'ActiveActionStatus'
44+
}
45+
{...actionProps}
46+
/>
47+
))}
48+
</Fragment>
49+
);
50+
});
9451

9552
Feedback.displayName = 'ActivityStatusFeedback';
9653

packages/component/src/Activity/private/FeedbackForm.tsx

Lines changed: 35 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -13,51 +13,59 @@ const FeedbackOptions = {
1313
DislikeAction: 'dislike'
1414
} as const;
1515

16-
export type FeedbackType = keyof typeof FeedbackOptions;
17-
1816
const MultiLineTextBox = withEmoji(TextArea);
1917

2018
function FeedbackForm({
2119
feedbackType,
2220
disclaimer,
23-
handeFeedbackTypeChange,
21+
onResetFeedbackForm,
2422
replyToId
2523
}: Readonly<{
26-
feedbackType: FeedbackType;
24+
feedbackType: string;
2725
disclaimer?: string;
28-
handeFeedbackTypeChange: () => void;
26+
onResetFeedbackForm: () => void;
2927
replyToId?: string;
3028
}>) {
29+
const feedbackTextAreaRef = useRef<HTMLTextAreaElement>(null);
3130
const [{ feedbackForm }] = useStyleSet();
32-
const localize = useLocalizer();
33-
const [feedback, setFeedback] = useState('');
3431
const [hasFocused, setHasFocused] = useState(false);
32+
const localize = useLocalizer();
3533
const postActivity = usePostActivity();
36-
const feedbackTextAreaRef = useRef<HTMLTextAreaElement>(null);
34+
const [userFeedback, setUserFeedback] = useState('');
35+
36+
const handleReset = useCallback(() => {
37+
setUserFeedback('');
38+
onResetFeedbackForm();
39+
setHasFocused(false);
40+
}, [onResetFeedbackForm]);
3741

3842
const handleSubmit = useCallback(
3943
event => {
4044
event.preventDefault();
41-
if (feedback) {
42-
postActivity({
43-
type: 'invoke',
44-
name: 'message/submitAction',
45-
replyToId,
46-
value: {
47-
actionName: 'feedback',
48-
actionValue: {
49-
reaction: feedbackType === 'LikeAction' ? FeedbackOptions.LikeAction : FeedbackOptions.DislikeAction,
50-
feedback: {
51-
feedbackText: feedback
52-
}
45+
postActivity({
46+
type: 'invoke',
47+
name: 'message/submitAction',
48+
replyToId,
49+
value: {
50+
actionName: 'feedback',
51+
actionValue: {
52+
reaction: feedbackType === 'LikeAction' ? FeedbackOptions.LikeAction : FeedbackOptions.DislikeAction,
53+
feedback: {
54+
feedbackText: userFeedback
5355
}
5456
}
55-
} as any);
56-
setFeedback('');
57-
handeFeedbackTypeChange();
58-
}
57+
}
58+
} as any);
59+
handleReset();
60+
},
61+
[postActivity, replyToId, feedbackType, handleReset, userFeedback]
62+
);
63+
64+
const handleChange = useCallback(
65+
(value: string) => {
66+
setUserFeedback(value);
5967
},
60-
[feedback, postActivity, replyToId, feedbackType, handeFeedbackTypeChange]
68+
[setUserFeedback]
6169
);
6270

6371
useEffect(() => {
@@ -67,22 +75,6 @@ function FeedbackForm({
6775
}
6876
}, [feedbackTextAreaRef, hasFocused]);
6977

70-
const handleCancel = useCallback(() => {
71-
setFeedback('');
72-
handeFeedbackTypeChange();
73-
}, [handeFeedbackTypeChange]);
74-
75-
const handleChange = useCallback(
76-
(value: string) => {
77-
setFeedback(value);
78-
},
79-
[setFeedback]
80-
);
81-
82-
if (!feedbackType) {
83-
return null;
84-
}
85-
8678
return (
8779
<div>
8880
<span className={classNames('feedback-form__body1', feedbackForm + '')}>{localize('FEEDBACK_FORM_TITLE')}</span>
@@ -92,7 +84,7 @@ function FeedbackForm({
9284
onChange={handleChange}
9385
placeholder={localize('FEEDBACK_FORM_PLACEHOLDER')}
9486
ref={feedbackTextAreaRef}
95-
value={feedback}
87+
value={userFeedback}
9688
/>
9789
{disclaimer && <span className={classNames('feedback-form__caption1', feedbackForm + '')}>{disclaimer}</span>}
9890
<div className={classNames('feedback-form__container', feedbackForm + '')}>
@@ -101,7 +93,7 @@ function FeedbackForm({
10193
</button>
10294
<button
10395
className={classNames('feedback-form__button__cancel', feedbackForm + '')}
104-
onClick={handleCancel}
96+
onClick={handleReset}
10597
type="button"
10698
>
10799
{localize('FEEDBACK_FORM_CANCEL_BUTTON_LABEL')}

0 commit comments

Comments
 (0)