diff --git a/public/locales/en-GB/app.json b/public/locales/en-GB/app.json index ccd111b..987402a 100644 --- a/public/locales/en-GB/app.json +++ b/public/locales/en-GB/app.json @@ -1,7 +1,9 @@ { "{{count}} people connected|one": "{{count}} person connected", "{{count}} people connected|other": "{{count}} people connected", - "{{displayName}}, your call is now ended": "{{displayName}}, your call is now ended", + "{{count}} stars|one": "{{count}} star", + "{{count}} stars|other": "{{count}} stars", + "{{displayName}}, your call has ended.": "{{displayName}}, your call has ended.", "{{name}} (Connecting...)": "{{name}} (Connecting...)", "{{name}} (Waiting for video...)": "{{name}} (Waiting for video...)", "{{name}} is presenting": "{{name}} is presenting", @@ -14,6 +16,8 @@ "<0>Join call now<1>Or<2>Copy call link and join later": "<0>Join call now<1>Or<2>Copy call link and join later", "<0>Oops, something's gone wrong.": "<0>Oops, something's gone wrong.", "<0>Submitting debug logs will help us track down the problem.": "<0>Submitting debug logs will help us track down the problem.", + "<0>Thanks for your feedback!": "<0>Thanks for your feedback!", + "<0>We'd love to hear your feedback so we can improve your experience.": "<0>We'd love to hear your feedback so we can improve your experience.", "<0>Why not finish by setting up a password to keep your account?<1>You'll be able to keep your name and set an avatar for use on future calls": "<0>Why not finish by setting up a password to keep your account?<1>You'll be able to keep your name and set an avatar for use on future calls", "Accept camera/microphone permissions to join the call.": "Accept camera/microphone permissions to join the call.", "Accept microphone permissions to join the call.": "Accept microphone permissions to join the call.", @@ -53,6 +57,7 @@ "Go": "Go", "Grid layout menu": "Grid layout menu", "Home": "Home", + "How did it go?": "How did it go?", "If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.", "Include debug logs": "Include debug logs", "Incompatible versions": "Incompatible versions", diff --git a/src/analytics/PosthogAnalytics.ts b/src/analytics/PosthogAnalytics.ts index ed8ada3..fad315b 100644 --- a/src/analytics/PosthogAnalytics.ts +++ b/src/analytics/PosthogAnalytics.ts @@ -29,6 +29,7 @@ import { MuteCameraTracker, MuteMicrophoneTracker, UndecryptableToDeviceEventTracker, + QualitySurveyEventTracker, } from "./PosthogEvents"; import { Config } from "../config/Config"; import { getUrlParams } from "../UrlParams"; @@ -431,4 +432,5 @@ export class PosthogAnalytics { public eventMuteMicrophone = new MuteMicrophoneTracker(); public eventMuteCamera = new MuteCameraTracker(); public eventUndecryptableToDevice = new UndecryptableToDeviceEventTracker(); + public eventQualitySurvey = new QualitySurveyEventTracker(); } diff --git a/src/analytics/PosthogEvents.ts b/src/analytics/PosthogEvents.ts index aa8aa32..f2fecb4 100644 --- a/src/analytics/PosthogEvents.ts +++ b/src/analytics/PosthogEvents.ts @@ -163,3 +163,21 @@ export class UndecryptableToDeviceEventTracker { }); } } + +interface QualitySurveyEvent { + eventName: "QualitySurvey"; + callId: string; + feedbackText: string; + stars: number; +} + +export class QualitySurveyEventTracker { + track(callId: string, feedbackText: string, stars: number) { + PosthogAnalytics.instance.trackEvent({ + eventName: "QualitySurvey", + callId, + feedbackText, + stars, + }); + } +} diff --git a/src/icons/StarSelected.svg b/src/icons/StarSelected.svg new file mode 100644 index 0000000..69a8ce8 --- /dev/null +++ b/src/icons/StarSelected.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/icons/StarUnselected.svg b/src/icons/StarUnselected.svg new file mode 100644 index 0000000..be28194 --- /dev/null +++ b/src/icons/StarUnselected.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/input/FeedbackInput.module.css b/src/input/FeedbackInput.module.css new file mode 100644 index 0000000..7564793 --- /dev/null +++ b/src/input/FeedbackInput.module.css @@ -0,0 +1,23 @@ +/* +Copyright 2023 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.feedback textarea { + height: 75px; + border-radius: 8px; +} +.feedback { + border-radius: 8px; +} diff --git a/src/input/StarRatingInput.module.css b/src/input/StarRatingInput.module.css new file mode 100644 index 0000000..08a65d1 --- /dev/null +++ b/src/input/StarRatingInput.module.css @@ -0,0 +1,41 @@ +/* +Copyright 2023 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.starIcon { + cursor: pointer; +} + +.starRating { + display: flex; + justify-content: center; + flex: 1; +} + +.inputContainer { + display: inline-block; +} + +.hideElement { + border: 0; + clip-path: content-box; + height: 0px; + width: 0px; + margin: -1px; + overflow: hidden; + padding: 0; + width: 1px; + display: inline-block; +} diff --git a/src/input/StarRatingInput.tsx b/src/input/StarRatingInput.tsx new file mode 100644 index 0000000..34f0120 --- /dev/null +++ b/src/input/StarRatingInput.tsx @@ -0,0 +1,85 @@ +/* +Copyright 2023 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; + +import styles from "./StarRatingInput.module.css"; +import { ReactComponent as StarSelected } from "../icons/StarSelected.svg"; +import { ReactComponent as StarUnselected } from "../icons/StarUnselected.svg"; + +interface Props { + starCount: number; + onChange: (stars: number) => void; + required?: boolean; +} + +export function StarRatingInput({ + starCount, + onChange, + required, +}: Props): JSX.Element { + const [rating, setRating] = useState(0); + const [hover, setHover] = useState(0); + const { t } = useTranslation(); + return ( +
+ {[...Array(starCount)].map((_star, index) => { + index += 1; + return ( +
setHover(index)} + onMouseLeave={() => setHover(rating)} + key={index} + > + { + setRating(index); + onChange(index); + }} + required + /> + + +
+ ); + })} +
+ ); +} diff --git a/src/room/CallEndedView.module.css b/src/room/CallEndedView.module.css index dcf11f0..fd9bad9 100644 --- a/src/room/CallEndedView.module.css +++ b/src/room/CallEndedView.module.css @@ -17,20 +17,31 @@ limitations under the License. .headline { text-align: center; margin-bottom: 60px; + white-space: pre; } .callEndedContent { text-align: center; - max-width: 360px; + max-width: 450px; +} +.callEndedContent p { + font-size: var(--font-size-subtitle); } - .callEndedContent h3 { margin-bottom: 32px; } .callEndedButton { + margin-top: 54px; + margin-left: 30px; + margin-right: 30px !important; +} + +.submitButton { width: 100%; margin-top: 54px; + margin-left: 30px; + margin-right: 30px !important; } .container { diff --git a/src/room/CallEndedView.tsx b/src/room/CallEndedView.tsx index 7543d66..e36d06c 100644 --- a/src/room/CallEndedView.tsx +++ b/src/room/CallEndedView.tsx @@ -14,19 +14,130 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { FormEventHandler, useCallback, useState } from "react"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { Trans, useTranslation } from "react-i18next"; +import { useHistory } from "react-router-dom"; import styles from "./CallEndedView.module.css"; -import { LinkButton } from "../button"; +import feedbackStyle from "../input/FeedbackInput.module.css"; +import { Button, LinkButton } from "../button"; import { useProfile } from "../profile/useProfile"; -import { Subtitle, Body, Link, Headline } from "../typography/Typography"; +import { Body, Link, Headline } from "../typography/Typography"; import { Header, HeaderLogo, LeftNav, RightNav } from "../Header"; +import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; +import { FieldRow, InputField } from "../input/Input"; +import { StarRatingInput } from "../input/StarRatingInput"; -export function CallEndedView({ client }: { client: MatrixClient }) { +export function CallEndedView({ + client, + isPasswordlessUser, + endedCallId, +}: { + client: MatrixClient; + isPasswordlessUser: boolean; + endedCallId: string; +}) { const { t } = useTranslation(); + const history = useHistory(); + const { displayName } = useProfile(client); + const [surveySubmitted, setSurverySubmitted] = useState(false); + const [starRating, setStarRating] = useState(0); + const [submitting, setSubmitting] = useState(false); + const [submitDone, setSubmitDone] = useState(false); + const submitSurvery: FormEventHandler = useCallback( + (e) => { + e.preventDefault(); + const data = new FormData(e.target as HTMLFormElement); + const feedbackText = data.get("feedbackText") as string; + + PosthogAnalytics.instance.eventQualitySurvey.track( + endedCallId, + feedbackText, + starRating + ); + + setSubmitting(true); + + setTimeout(() => { + setSubmitDone(true); + + setTimeout(() => { + if (isPasswordlessUser) { + // setting this renders the callEndedView with the invitation to create an account + setSurverySubmitted(true); + } else { + // if the user already has an account immediately go back to the home screen + history.push("/"); + } + }, 1000); + }, 1000); + }, + [endedCallId, history, isPasswordlessUser, starRating] + ); + const createAccountDialog = isPasswordlessUser && ( +
+ +

Why not finish by setting up a password to keep your account?

+

+ You'll be able to keep your name and set an avatar for use on future + calls +

+
+ + {t("Create account")} + +
+ ); + + const qualitySurveyDialog = ( +
+ +

+ We'd love to hear your feedback so we can improve your experience. +

+
+
+ + + + + + {" "} + + {submitDone ? ( + +

Thanks for your feedback!

+
+ ) : ( + + )} +
+
+
+ ); return ( <> @@ -39,27 +150,19 @@ export function CallEndedView({ client }: { client: MatrixClient }) {
- {t("{{displayName}}, your call is now ended", { displayName })} + {surveySubmitted + ? t("{{displayName}}, your call has ended.", { + displayName, + }) + : t("{{displayName}}, your call has ended.", { + displayName, + }) + + "\n" + + t("How did it go?")} -
- - - Why not finish by setting up a password to keep your account? - - - You'll be able to keep your name and set an avatar for use on - future calls - - - - {t("Create account")} - -
+ {!surveySubmitted && PosthogAnalytics.instance.isEnabled() + ? qualitySurveyDialog + : createAccountDialog}
diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index 722686c..595bc3f 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -203,7 +203,11 @@ export function GroupCallView({ widget.api.transport.send(ElementWidgetActions.HangupCall, {}); } - if (!isPasswordlessUser && !isEmbedded) { + if ( + !isPasswordlessUser && + !isEmbedded && + !PosthogAnalytics.instance.isEnabled() + ) { history.push("/"); } }, [groupCall, leave, isPasswordlessUser, isEmbedded, history]); @@ -268,8 +272,14 @@ export function GroupCallView({ ); } } else if (left) { - if (isPasswordlessUser) { - return ; + if (isPasswordlessUser || PosthogAnalytics.instance.isEnabled()) { + return ( + + ); } else { // If the user is a regular user, we'll have sent them back to the homepage, // so just sit here & do nothing: otherwise we would (briefly) mount the diff --git a/src/settings/FeedbackSettingsTab.tsx b/src/settings/FeedbackSettingsTab.tsx index da40678..b57f0f7 100644 --- a/src/settings/FeedbackSettingsTab.tsx +++ b/src/settings/FeedbackSettingsTab.tsx @@ -23,6 +23,7 @@ import { FieldRow, InputField, ErrorMessage } from "../input/Input"; import { useSubmitRageshake, useRageshakeRequest } from "./submit-rageshake"; import { Body } from "../typography/Typography"; import styles from "../input/SelectInput.module.css"; +import feedbackStyles from "../input/FeedbackInput.module.css"; interface Props { roomId?: string; @@ -68,9 +69,11 @@ export function FeedbackSettingsTab({ roomId }: Props) {