diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 56e3dc4598..beea63b79d 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -9938,6 +9938,118 @@ "messageformat": "You can adjust this on your mobile device under Settings → Donate to Signal → Badges", "description": "Help text below the toggle in donation thank you modal" }, + "icu:CallQualitySurvey__CloseButton__AccessibilityLabel": { + "messageformat": "Close", + "description": "Call Quality Survey Dialog > Dialog Close Button (Accessibility Label)" + }, + "icu:CallQualitySurvey__BackButton__AccessibilityLabel": { + "messageformat": "Back", + "description": "Call Quality Survey Dialog > Dialog Back Button (Accessibility Label)" + }, + "icu:CallQualitySurvey__HowWasYourCall__PageTitle": { + "messageformat": "How was your call?", + "description": "Call Quality Survey Dialog > How was your call? > Page Title" + }, + "icu:CallQualitySurvey__HowWasYourCall__PageDescription": { + "messageformat": "This helps us improve calls in Signal. No personally identifiable information will be stored.", + "description": "Call Quality Survey Dialog > How was your call? > Page Description" + }, + "icu:CallQualitySurvey__HowWasYourCall__Response__HadIssues": { + "messageformat": "Had issues", + "description": "Call Quality Survey Dialog > How was your call? > Response > Had Issues" + }, + "icu:CallQualitySurvey__HowWasYourCall__Response__Great": { + "messageformat": "Great", + "description": "Call Quality Survey Dialog > How was your call? > Response > Great" + }, + "icu:CallQualitySurvey__WhatIssuesDidYouHave__PageTitle": { + "messageformat": "What issues did you have?", + "description": "Call Quality Survey Dialog > What issues did you have? > Page Title" + }, + "icu:CallQualitySurvey__WhatIssuesDidYouHave__IssuesList__Heading": { + "messageformat": "Select all that apply", + "description": "Call Quality Survey Dialog > What issues did you have? > Issues list > Heading" + }, + "icu:CallQualitySurvey__Issue--AUDIO": { + "messageformat": "Audio issue", + "description": "Call Quality Survey Dialog > Select issues > AUDIO" + }, + "icu:CallQualitySurvey__Issue--AUDIO_STUTTERING": { + "messageformat": "Audio stuttering", + "description": "Call Quality Survey Dialog > Select issues > AUDIO_STUTTERING" + }, + "icu:CallQualitySurvey__Issue--AUDIO_LOCAL_ECHO": { + "messageformat": "I heard echo", + "description": "Call Quality Survey Dialog > Select issues > AUDIO_LOCAL_ECHO" + }, + "icu:CallQualitySurvey__Issue--AUDIO_REMOTE_ECHO": { + "messageformat": "Others heard echo", + "description": "Call Quality Survey Dialog > Select issues > AUDIO_REMOTE_ECHO" + }, + "icu:CallQualitySurvey__Issue--AUDIO_DROP": { + "messageformat": "Audio cut out", + "description": "Call Quality Survey Dialog > Select issues > AUDIO_DROP" + }, + "icu:CallQualitySurvey__Issue--VIDEO": { + "messageformat": "Video issue", + "description": "Call Quality Survey Dialog > Select issues > VIDEO" + }, + "icu:CallQualitySurvey__Issue--VIDEO_NO_CAMERA": { + "messageformat": "Camera didn't work", + "description": "Call Quality Survey Dialog > Select issues > VIDEO_NO_CAMERA" + }, + "icu:CallQualitySurvey__Issue--VIDEO_LOW_QUALITY": { + "messageformat": "Poor video quality", + "description": "Call Quality Survey Dialog > Select issues > VIDEO_LOW_QUALITY" + }, + "icu:CallQualitySurvey__Issue--VIDEO_LOW_RESOLUTION": { + "messageformat": "Low resolution", + "description": "Call Quality Survey Dialog > Select issues > VIDEO_LOW_RESOLUTION" + }, + "icu:CallQualitySurvey__Issue--CALL_DROPPED": { + "messageformat": "Call dropped", + "description": "Call Quality Survey Dialog > Select issues > CALL_DROPPED" + }, + "icu:CallQualitySurvey__Issue--OTHER": { + "messageformat": "Something else", + "description": "Call Quality Survey Dialog > Select issues > OTHER" + }, + "icu:CallQualitySurvey__WhatIssuesDidYouHave__SomethingElse__TextArea__AccessibilityLabel": { + "messageformat": "Issues description", + "description": "Call Quality Survey Dialog > What issues did you have? > Something else > Textarea > Accessibility Label" + }, + "icu:CallQualitySurvey__WhatIssuesDidYouHave__SomethingElse__TextArea__HelpText": { + "messageformat": "Please include any details relevant to the issue. Anything you share here will be kept private and will only be used to help improve calls in Signal.", + "description": "Call Quality Survey Dialog > What issues did you have? > Something else > Textarea > Help Text" + }, + "icu:CallQualitySurvey__WhatIssuesDidYouHave__ContinueButton": { + "messageformat": "Continue", + "description": "Call Quality Survey Dialog > What issues did you have? > Continue Button" + }, + "icu:CallQualitySurvey__ConfirmSubmission__PageTitle": { + "messageformat": "Help us improve", + "description": "Call Quality Survey Dialog > Help us improve > Page Title" + }, + "icu:CallQualitySurvey__ConfirmSubmission__PageDescription": { + "messageformat": "This helps us learn more about calls and what is working or not working. You can view your debug log before submitting.", + "description": "Call Quality Survey Dialog > Help us improve > Page Description" + }, + "icu:CallQualitySurvey__ConfirmSubmission__ShareDebugLog__Label": { + "messageformat": "Share debug log", + "description": "Call Quality Survey Dialog > Help us improve > Share debug log > Label" + }, + "icu:CallQualitySurvey__ConfirmSubmission__ShareDebugLog__ViewButton": { + "messageformat": "View", + "description": "Call Quality Survey Dialog > Help us improve > Share debug log > View Button" + }, + "icu:CallQualitySurvey__ConfirmSubmission__ShareDebugLog__HelpText": { + "messageformat": "Debug logs contain low level app information and do not reveal any of your message contents.", + "description": "Call Quality Survey Dialog > Help us improve > Share debug log > Help Text" + }, + "icu:CallQualitySurvey__ConfirmSubmission__SubmitButton": { + "messageformat": "Submit", + "description": "Call Quality Survey Dialog > Help us improve > Submit Button" + }, "icu:WhatsNew__bugfixes": { "messageformat": "This version contains a number of small tweaks and bug fixes to keep Signal running smoothly.", "description": "Release notes for releases that only include bug fixes", diff --git a/protos/CallQualitySurvey.proto b/protos/CallQualitySurvey.proto new file mode 100644 index 0000000000..7bcf9e9fae --- /dev/null +++ b/protos/CallQualitySurvey.proto @@ -0,0 +1,58 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +syntax = "proto3"; + +package signalservice; + +message SubmitCallQualitySurveyRequest { + // Indicates whether the caller was generally satisfied with the quality of + // the call + bool user_satisfied = 1; + + // A list of call quality issues selected by the caller + repeated string call_quality_issues = 2; + + // A free-form description of any additional issues as written by the caller + optional string additional_issues_description = 3; + + // A URL for a set of debug logs associated with the call if the caller chose + // to submit debug logs + optional string debug_log_url = 4; + + // The time at which the call started in microseconds since the epoch (see + // https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#timestamp_type) + int64 start_timestamp = 5; + + // The time at which the call ended in microseconds since the epoch (see + // https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#timestamp_type) + int64 end_timestamp = 6; + + // The type of call; note that direct voice calls can become video calls and + // vice versa, and this field indicates which mode was selected at call + // initiation time. At the time of writing, expected call types are + // "direct_voice", "direct_video", "group", and "call_link". + string call_type = 7; + + // Indicates whether the call completed without error or if it terminated + // abnormally + bool success = 8; + + // A client-defined, but human-readable reason for call termination + string call_end_reason = 9; + + // The median round-trip time, measured in milliseconds, for packets over the + // duration of the call + optional float rtt_median = 10; + + // The median jitter, measured in milliseconds, for the duration of the call + optional float jitter_median = 11; + + // The fraction of all packets lost over the duration of the call + optional float packet_loss_fraction = 12; + + // Machine-generated telemetry from the call; this is a serialized protobuf + // entity generated (and, critically, explained to the user!) by the calling + // library + optional bytes call_telemetry = 13; +} diff --git a/ts/axo/AxoDialog.dom.tsx b/ts/axo/AxoDialog.dom.tsx index 83839c8887..15956c8a9b 100644 --- a/ts/axo/AxoDialog.dom.tsx +++ b/ts/axo/AxoDialog.dom.tsx @@ -168,6 +168,7 @@ export namespace AxoDialog { size="sm" variant="borderless-secondary" symbol="chevron-[start]" + onClick={props.onClick} aria-label={props['aria-label']} onClick={props.onClick} /> @@ -378,6 +379,7 @@ export namespace AxoDialog { onClick={props.onClick} size="md" width="grow" + onClick={props.onClick} > {props.children} diff --git a/ts/components/CallQualitySurvey.dom.stories.tsx b/ts/components/CallQualitySurvey.dom.stories.tsx new file mode 100644 index 0000000000..78f8f6555c --- /dev/null +++ b/ts/components/CallQualitySurvey.dom.stories.tsx @@ -0,0 +1,24 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +import type { Meta } from '@storybook/react'; +import React, { useState } from 'react'; +import { action } from '@storybook/addon-actions'; +import { CallQualitySurveyDialog } from './CallQualitySurveyDialog.dom.js'; + +const { i18n } = window.SignalContext; + +export default { + title: 'Components/CallQualitySurveyDialog', +} satisfies Meta; + +export function Default(): JSX.Element { + const [open, setOpen] = useState(true); + return ( + + ); +} diff --git a/ts/components/CallQualitySurveyDialog.dom.tsx b/ts/components/CallQualitySurveyDialog.dom.tsx new file mode 100644 index 0000000000..62f1aa9fb9 --- /dev/null +++ b/ts/components/CallQualitySurveyDialog.dom.tsx @@ -0,0 +1,511 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +import React, { useCallback, useId, useState } from 'react'; +import type { LocalizerType } from '../types/I18N.std.js'; +import { AxoSymbol } from '../axo/AxoSymbol.dom.js'; +import { AxoButton } from '../axo/AxoButton.dom.js'; +import { AxoDialog } from '../axo/AxoDialog.dom.js'; +import { CallQualitySurvey } from '../types/CallQualitySurvey.std.js'; +import { tw } from '../axo/tw.dom.js'; +import { missingCaseError } from '../util/missingCaseError.std.js'; +import { AxoCheckbox } from '../axo/AxoCheckbox.dom.js'; +import { strictAssert } from '../util/assert.std.js'; + +import Issue = CallQualitySurvey.Issue; + +enum Page { + HOW_WAS_YOUR_CALL, + WHAT_ISSUES_DID_YOU_HAVE, + CONFIRM_SUBMISSION, + PREVIEW_DEBUGLOGS, +} + +export type CallQualitySurveyDialogProps = Readonly<{ + i18n: LocalizerType; + open: boolean; + onOpenChange: (open: boolean) => void; + onSubmit: (form: CallQualitySurvey.Form) => void; +}>; + +export function CallQualitySurveyDialog( + props: CallQualitySurveyDialogProps +): JSX.Element { + const { i18n, onSubmit } = props; + + const [page, setPage] = useState(Page.HOW_WAS_YOUR_CALL); + const [userSatisfied, setUserSatisfied] = useState(null); + const [callQualityIssues, setCallQualityIssues] = useState< + ReadonlySet + >(() => new Set()); + const [additionalIssuesDescription, setAdditionalIssuesDescription] = + useState(''); + const debugLogCheckboxId = useId(); + const [shareDebugLog, setShareDebugLog] = useState(false); + + const handleSubmit = useCallback(() => { + strictAssert(userSatisfied != null, 'userSatisfied cannot be null'); + + const form: CallQualitySurvey.Form = { + userSatisfied, + // TODO: Only include if `!userSatisfied` + callQualityIssues, + // TODO: Only include if `callQualityIssues.has(Issue.OTHER)` + additionalIssuesDescription, + shareDebugLog, + }; + + onSubmit(form); + }, [ + onSubmit, + userSatisfied, + callQualityIssues, + additionalIssuesDescription, + shareDebugLog, + ]); + + return ( + + + {page === Page.HOW_WAS_YOUR_CALL && ( + <> + + + {i18n('icu:CallQualitySurvey__HowWasYourCall__PageTitle')} + + + + +

+ + {i18n( + 'icu:CallQualitySurvey__HowWasYourCall__PageDescription' + )} + +

+
+ { + setUserSatisfied(false); + setPage(Page.WHAT_ISSUES_DID_YOU_HAVE); + }} + label={i18n( + 'icu:CallQualitySurvey__HowWasYourCall__Response__HadIssues' + )} + /> + + { + setUserSatisfied(true); + setPage(Page.CONFIRM_SUBMISSION); + }} + label={i18n( + 'icu:CallQualitySurvey__HowWasYourCall__Response__Great' + )} + /> +
+
+ + )} + {page === Page.WHAT_ISSUES_DID_YOU_HAVE && ( + <> + + { + setPage(Page.HOW_WAS_YOUR_CALL); + }} + /> + + {i18n('icu:CallQualitySurvey__WhatIssuesDidYouHave__PageTitle')} + + + + +

+ + {i18n( + 'icu:CallQualitySurvey__WhatIssuesDidYouHave__IssuesList__Heading' + )} + +

+
+ +
+ {callQualityIssues.has(Issue.OTHER) && ( +
+