mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2025-12-05 01:10:49 +00:00
Init Call Quality Survey UI
This commit is contained in:
@@ -9938,6 +9938,118 @@
|
|||||||
"messageformat": "You can adjust this on your mobile device under Settings → Donate to Signal → Badges",
|
"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"
|
"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": {
|
"icu:WhatsNew__bugfixes": {
|
||||||
"messageformat": "This version contains a number of small tweaks and bug fixes to keep Signal running smoothly.",
|
"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",
|
"description": "Release notes for releases that only include bug fixes",
|
||||||
|
|||||||
58
protos/CallQualitySurvey.proto
Normal file
58
protos/CallQualitySurvey.proto
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -168,6 +168,7 @@ export namespace AxoDialog {
|
|||||||
size="sm"
|
size="sm"
|
||||||
variant="borderless-secondary"
|
variant="borderless-secondary"
|
||||||
symbol="chevron-[start]"
|
symbol="chevron-[start]"
|
||||||
|
onClick={props.onClick}
|
||||||
aria-label={props['aria-label']}
|
aria-label={props['aria-label']}
|
||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
/>
|
/>
|
||||||
@@ -378,6 +379,7 @@ export namespace AxoDialog {
|
|||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
size="md"
|
size="md"
|
||||||
width="grow"
|
width="grow"
|
||||||
|
onClick={props.onClick}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</AxoButton.Root>
|
</AxoButton.Root>
|
||||||
|
|||||||
24
ts/components/CallQualitySurvey.dom.stories.tsx
Normal file
24
ts/components/CallQualitySurvey.dom.stories.tsx
Normal file
@@ -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 (
|
||||||
|
<CallQualitySurveyDialog
|
||||||
|
i18n={i18n}
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
onSubmit={action('onSubmit')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
511
ts/components/CallQualitySurveyDialog.dom.tsx
Normal file
511
ts/components/CallQualitySurveyDialog.dom.tsx
Normal file
@@ -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<boolean | null>(null);
|
||||||
|
const [callQualityIssues, setCallQualityIssues] = useState<
|
||||||
|
ReadonlySet<Issue>
|
||||||
|
>(() => 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 (
|
||||||
|
<AxoDialog.Root open={props.open} onOpenChange={props.onOpenChange}>
|
||||||
|
<AxoDialog.Content escape="cancel-is-destructive" size="md">
|
||||||
|
{page === Page.HOW_WAS_YOUR_CALL && (
|
||||||
|
<>
|
||||||
|
<AxoDialog.Header>
|
||||||
|
<AxoDialog.Title>
|
||||||
|
{i18n('icu:CallQualitySurvey__HowWasYourCall__PageTitle')}
|
||||||
|
</AxoDialog.Title>
|
||||||
|
<AxoDialog.Close
|
||||||
|
aria-label={i18n(
|
||||||
|
'icu:CallQualitySurvey__CloseButton__AccessibilityLabel'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</AxoDialog.Header>
|
||||||
|
<AxoDialog.Body>
|
||||||
|
<p className={tw('mb-3 type-body-medium text-label-primary')}>
|
||||||
|
<AxoDialog.Description>
|
||||||
|
{i18n(
|
||||||
|
'icu:CallQualitySurvey__HowWasYourCall__PageDescription'
|
||||||
|
)}
|
||||||
|
</AxoDialog.Description>
|
||||||
|
</p>
|
||||||
|
<div className={tw('mb-6 flex justify-center gap-10')}>
|
||||||
|
<BigCircleButton
|
||||||
|
symbol="thumbsdown"
|
||||||
|
className={tw(
|
||||||
|
'bg-color-fill-destructive/10 text-color-fill-destructive group-hovered:bg-color-fill-destructive/15'
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
setUserSatisfied(false);
|
||||||
|
setPage(Page.WHAT_ISSUES_DID_YOU_HAVE);
|
||||||
|
}}
|
||||||
|
label={i18n(
|
||||||
|
'icu:CallQualitySurvey__HowWasYourCall__Response__HadIssues'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<BigCircleButton
|
||||||
|
symbol="thumbsup"
|
||||||
|
className={tw(
|
||||||
|
'bg-color-fill-primary/10 text-color-fill-primary group-hovered:bg-color-fill-primary/15'
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
setUserSatisfied(true);
|
||||||
|
setPage(Page.CONFIRM_SUBMISSION);
|
||||||
|
}}
|
||||||
|
label={i18n(
|
||||||
|
'icu:CallQualitySurvey__HowWasYourCall__Response__Great'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</AxoDialog.Body>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{page === Page.WHAT_ISSUES_DID_YOU_HAVE && (
|
||||||
|
<>
|
||||||
|
<AxoDialog.Header>
|
||||||
|
<AxoDialog.Back
|
||||||
|
aria-label={i18n(
|
||||||
|
'icu:CallQualitySurvey__BackButton__AccessibilityLabel'
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
setPage(Page.HOW_WAS_YOUR_CALL);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<AxoDialog.Title>
|
||||||
|
{i18n('icu:CallQualitySurvey__WhatIssuesDidYouHave__PageTitle')}
|
||||||
|
</AxoDialog.Title>
|
||||||
|
<AxoDialog.Close
|
||||||
|
aria-label={i18n(
|
||||||
|
'icu:CallQualitySurvey__CloseButton__AccessibilityLabel'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</AxoDialog.Header>
|
||||||
|
<AxoDialog.Body>
|
||||||
|
<p className={tw('mb-3 type-body-medium text-label-primary')}>
|
||||||
|
<AxoDialog.Description>
|
||||||
|
{i18n(
|
||||||
|
'icu:CallQualitySurvey__WhatIssuesDidYouHave__IssuesList__Heading'
|
||||||
|
)}
|
||||||
|
</AxoDialog.Description>
|
||||||
|
</p>
|
||||||
|
<div className={tw('mb-3')}>
|
||||||
|
<IssueSelector
|
||||||
|
i18n={i18n}
|
||||||
|
issues={callQualityIssues}
|
||||||
|
onIssuesChange={setCallQualityIssues}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{callQualityIssues.has(Issue.OTHER) && (
|
||||||
|
<div className={tw('mb-3')}>
|
||||||
|
<textarea
|
||||||
|
aria-label={i18n(
|
||||||
|
'icu:CallQualitySurvey__WhatIssuesDidYouHave__SomethingElse__TextArea__AccessibilityLabel'
|
||||||
|
)}
|
||||||
|
value={additionalIssuesDescription}
|
||||||
|
onChange={event => {
|
||||||
|
setAdditionalIssuesDescription(event.currentTarget.value);
|
||||||
|
}}
|
||||||
|
placeholder="Describe your issue"
|
||||||
|
className={tw(
|
||||||
|
'field-sizing-content max-h-50 min-h-20 w-full resize-none',
|
||||||
|
'rounded-lg border-[0.5px] border-border-primary px-3 py-2 shadow-elevation-1',
|
||||||
|
'text-label-primary placeholder:text-label-placeholder disabled:text-label-disabled',
|
||||||
|
'outline-border-focused not-forced-colors:outline-0 not-forced-colors:focused:outline-[2.5px]'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
className={tw('mt-3 type-body-small text-label-secondary')}
|
||||||
|
>
|
||||||
|
{i18n(
|
||||||
|
'icu:CallQualitySurvey__WhatIssuesDidYouHave__SomethingElse__TextArea__HelpText'
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AxoDialog.Body>
|
||||||
|
<AxoDialog.Footer>
|
||||||
|
<AxoDialog.Actions>
|
||||||
|
<AxoDialog.Action
|
||||||
|
variant="primary"
|
||||||
|
onClick={() => {
|
||||||
|
setPage(Page.CONFIRM_SUBMISSION);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{i18n(
|
||||||
|
'icu:CallQualitySurvey__WhatIssuesDidYouHave__ContinueButton'
|
||||||
|
)}
|
||||||
|
</AxoDialog.Action>
|
||||||
|
</AxoDialog.Actions>
|
||||||
|
</AxoDialog.Footer>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{page === Page.CONFIRM_SUBMISSION && (
|
||||||
|
<>
|
||||||
|
<AxoDialog.Header>
|
||||||
|
<AxoDialog.Back
|
||||||
|
aria-label={i18n(
|
||||||
|
'icu:CallQualitySurvey__BackButton__AccessibilityLabel'
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
if (!userSatisfied) {
|
||||||
|
setPage(Page.WHAT_ISSUES_DID_YOU_HAVE);
|
||||||
|
} else {
|
||||||
|
setPage(Page.HOW_WAS_YOUR_CALL);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<AxoDialog.Title>
|
||||||
|
{i18n('icu:CallQualitySurvey__ConfirmSubmission__PageTitle')}
|
||||||
|
</AxoDialog.Title>
|
||||||
|
<AxoDialog.Close
|
||||||
|
aria-label={i18n(
|
||||||
|
'icu:CallQualitySurvey__CloseButton__AccessibilityLabel'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</AxoDialog.Header>
|
||||||
|
<AxoDialog.Body>
|
||||||
|
<p className={tw('mb-3 type-body-medium text-label-primary')}>
|
||||||
|
<AxoDialog.Description>
|
||||||
|
{i18n(
|
||||||
|
'icu:CallQualitySurvey__ConfirmSubmission__PageDescription'
|
||||||
|
)}
|
||||||
|
</AxoDialog.Description>
|
||||||
|
</p>
|
||||||
|
<div className={tw('my-1.5 flex items-center gap-3')}>
|
||||||
|
<AxoCheckbox.Root
|
||||||
|
id={debugLogCheckboxId}
|
||||||
|
checked={shareDebugLog}
|
||||||
|
onCheckedChange={setShareDebugLog}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor={debugLogCheckboxId}
|
||||||
|
className={tw('grow truncate')}
|
||||||
|
>
|
||||||
|
{i18n(
|
||||||
|
'icu:CallQualitySurvey__ConfirmSubmission__ShareDebugLog__Label'
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
<AxoButton.Root
|
||||||
|
variant="subtle-primary"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setPage(Page.PREVIEW_DEBUGLOGS);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{i18n(
|
||||||
|
'icu:CallQualitySurvey__ConfirmSubmission__ShareDebugLog__ViewButton'
|
||||||
|
)}
|
||||||
|
</AxoButton.Root>
|
||||||
|
</div>
|
||||||
|
<p className={tw('mt-3 type-body-small text-label-secondary')}>
|
||||||
|
{i18n(
|
||||||
|
'icu:CallQualitySurvey__ConfirmSubmission__ShareDebugLog__HelpText'
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</AxoDialog.Body>
|
||||||
|
<AxoDialog.Footer>
|
||||||
|
<AxoDialog.Actions>
|
||||||
|
<AxoDialog.Action variant="primary" onClick={handleSubmit}>
|
||||||
|
{i18n(
|
||||||
|
'icu:CallQualitySurvey__ConfirmSubmission__SubmitButton'
|
||||||
|
)}
|
||||||
|
</AxoDialog.Action>
|
||||||
|
</AxoDialog.Actions>
|
||||||
|
</AxoDialog.Footer>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AxoDialog.Content>
|
||||||
|
</AxoDialog.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BigCircleButton(props: {
|
||||||
|
symbol: AxoSymbol.IconName;
|
||||||
|
className: string;
|
||||||
|
label: string;
|
||||||
|
onClick: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={tw(
|
||||||
|
'group flex w-24 flex-col items-center gap-3 rounded-lg p-3',
|
||||||
|
'outline-border-focused not-forced-colors:outline-0 not-forced-colors:focused:outline-[2.5px]'
|
||||||
|
)}
|
||||||
|
onClick={props.onClick}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={tw(
|
||||||
|
'flex size-10 items-center justify-center rounded-full',
|
||||||
|
props.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<AxoSymbol.Icon size={24} symbol={props.symbol} label={null} />
|
||||||
|
</span>
|
||||||
|
<span className={tw('type-body-medium text-label-primary')}>
|
||||||
|
{props.label}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ISSUE_ICONS: Record<Issue, AxoSymbol.InlineGlyphName> = {
|
||||||
|
[Issue.AUDIO]: 'speaker',
|
||||||
|
[Issue.AUDIO_STUTTERING]: 'speaker',
|
||||||
|
[Issue.AUDIO_LOCAL_ECHO]: 'speaker',
|
||||||
|
[Issue.AUDIO_REMOTE_ECHO]: 'speaker',
|
||||||
|
[Issue.AUDIO_DROP]: 'speaker',
|
||||||
|
[Issue.VIDEO]: 'videocamera',
|
||||||
|
[Issue.VIDEO_NO_CAMERA]: 'videocamera',
|
||||||
|
[Issue.VIDEO_LOW_QUALITY]: 'videocamera',
|
||||||
|
[Issue.VIDEO_LOW_RESOLUTION]: 'videocamera',
|
||||||
|
[Issue.CALL_DROPPED]: 'x-circle',
|
||||||
|
[Issue.OTHER]: 'error',
|
||||||
|
};
|
||||||
|
|
||||||
|
function getIssueLabel(i18n: LocalizerType, issue: Issue): string {
|
||||||
|
switch (issue) {
|
||||||
|
case Issue.AUDIO:
|
||||||
|
return i18n('icu:CallQualitySurvey__Issue--AUDIO');
|
||||||
|
case Issue.AUDIO_STUTTERING:
|
||||||
|
return i18n('icu:CallQualitySurvey__Issue--AUDIO_STUTTERING');
|
||||||
|
case Issue.AUDIO_LOCAL_ECHO:
|
||||||
|
return i18n('icu:CallQualitySurvey__Issue--AUDIO_LOCAL_ECHO');
|
||||||
|
case Issue.AUDIO_REMOTE_ECHO:
|
||||||
|
return i18n('icu:CallQualitySurvey__Issue--AUDIO_REMOTE_ECHO');
|
||||||
|
case Issue.AUDIO_DROP:
|
||||||
|
return i18n('icu:CallQualitySurvey__Issue--AUDIO_DROP');
|
||||||
|
case Issue.VIDEO:
|
||||||
|
return i18n('icu:CallQualitySurvey__Issue--VIDEO');
|
||||||
|
case Issue.VIDEO_NO_CAMERA:
|
||||||
|
return i18n('icu:CallQualitySurvey__Issue--VIDEO_NO_CAMERA');
|
||||||
|
case Issue.VIDEO_LOW_QUALITY:
|
||||||
|
return i18n('icu:CallQualitySurvey__Issue--VIDEO_LOW_QUALITY');
|
||||||
|
case Issue.VIDEO_LOW_RESOLUTION:
|
||||||
|
return i18n('icu:CallQualitySurvey__Issue--VIDEO_LOW_RESOLUTION');
|
||||||
|
case Issue.CALL_DROPPED:
|
||||||
|
return i18n('icu:CallQualitySurvey__Issue--CALL_DROPPED');
|
||||||
|
case Issue.OTHER:
|
||||||
|
return i18n('icu:CallQualitySurvey__Issue--OTHER');
|
||||||
|
default:
|
||||||
|
throw missingCaseError(issue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type IssueGroup = Readonly<{
|
||||||
|
parent: Issue;
|
||||||
|
children: ReadonlyArray<Issue>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const IssueGroups: ReadonlyArray<IssueGroup> = [
|
||||||
|
{
|
||||||
|
parent: Issue.AUDIO,
|
||||||
|
children: [
|
||||||
|
Issue.AUDIO_STUTTERING,
|
||||||
|
Issue.AUDIO_LOCAL_ECHO,
|
||||||
|
Issue.AUDIO_REMOTE_ECHO,
|
||||||
|
Issue.AUDIO_DROP,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parent: Issue.VIDEO,
|
||||||
|
children: [
|
||||||
|
Issue.VIDEO_NO_CAMERA,
|
||||||
|
Issue.VIDEO_LOW_QUALITY,
|
||||||
|
Issue.VIDEO_LOW_RESOLUTION,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parent: Issue.CALL_DROPPED,
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parent: Issue.OTHER,
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function IssueSelector(props: {
|
||||||
|
i18n: LocalizerType;
|
||||||
|
issues: ReadonlySet<Issue>;
|
||||||
|
onIssuesChange: (issues: ReadonlySet<Issue>) => void;
|
||||||
|
}): JSX.Element {
|
||||||
|
const { i18n, issues, onIssuesChange } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={tw('flex flex-wrap justify-center-safe gap-2 px-4')}>
|
||||||
|
{IssueGroups.map(group => {
|
||||||
|
return (
|
||||||
|
<IssueToggleGroup
|
||||||
|
key={group.parent}
|
||||||
|
i18n={i18n}
|
||||||
|
group={group}
|
||||||
|
issues={issues}
|
||||||
|
onIssuesChange={onIssuesChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function IssueToggleGroup(props: {
|
||||||
|
i18n: LocalizerType;
|
||||||
|
group: IssueGroup;
|
||||||
|
issues: ReadonlySet<Issue>;
|
||||||
|
onIssuesChange: (issues: ReadonlySet<Issue>) => void;
|
||||||
|
}) {
|
||||||
|
const { i18n, group, issues, onIssuesChange } = props;
|
||||||
|
|
||||||
|
const selected = issues.has(group.parent);
|
||||||
|
const [stored, setStored] = useState<ReadonlySet<Issue>>(() => new Set());
|
||||||
|
|
||||||
|
const handleParentToggle = useCallback(
|
||||||
|
(_issue: Issue, toggle: boolean) => {
|
||||||
|
const newIssues = new Set(issues);
|
||||||
|
if (toggle) {
|
||||||
|
newIssues.add(group.parent);
|
||||||
|
for (const child of stored) {
|
||||||
|
newIssues.add(child);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newIssues.delete(group.parent);
|
||||||
|
for (const child of group.children) {
|
||||||
|
newIssues.delete(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onIssuesChange(newIssues);
|
||||||
|
},
|
||||||
|
[issues, stored, group, onIssuesChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChildToggle = useCallback(
|
||||||
|
(issue: Issue, toggle: boolean) => {
|
||||||
|
const newIssues = new Set(issues);
|
||||||
|
const newStored = new Set(stored);
|
||||||
|
if (toggle) {
|
||||||
|
newIssues.add(issue);
|
||||||
|
newStored.add(issue);
|
||||||
|
} else {
|
||||||
|
newIssues.delete(issue);
|
||||||
|
newStored.delete(issue);
|
||||||
|
}
|
||||||
|
setStored(newStored);
|
||||||
|
onIssuesChange(newIssues);
|
||||||
|
},
|
||||||
|
[issues, stored, onIssuesChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<IssueToggle
|
||||||
|
i18n={i18n}
|
||||||
|
issue={group.parent}
|
||||||
|
isParent
|
||||||
|
isSelected={selected}
|
||||||
|
onToggle={handleParentToggle}
|
||||||
|
/>
|
||||||
|
{selected && (
|
||||||
|
<>
|
||||||
|
{group.children.map(child => {
|
||||||
|
return (
|
||||||
|
<IssueToggle
|
||||||
|
key={child}
|
||||||
|
i18n={i18n}
|
||||||
|
issue={child}
|
||||||
|
isParent={false}
|
||||||
|
isSelected={issues.has(child)}
|
||||||
|
onToggle={handleChildToggle}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function IssueToggle(props: {
|
||||||
|
i18n: LocalizerType;
|
||||||
|
issue: Issue;
|
||||||
|
isParent: boolean;
|
||||||
|
isSelected: boolean;
|
||||||
|
onToggle: (issue: Issue, toggle: boolean) => void;
|
||||||
|
}) {
|
||||||
|
const { i18n, issue, isSelected, onToggle } = props;
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
onToggle(issue, !isSelected);
|
||||||
|
}, [issue, isSelected, onToggle]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AxoButton.Root
|
||||||
|
variant={props.isSelected ? 'primary' : 'secondary'}
|
||||||
|
size="md"
|
||||||
|
symbol={ISSUE_ICONS[issue]}
|
||||||
|
aria-pressed={props.isSelected}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
{getIssueLabel(i18n, issue)}
|
||||||
|
</AxoButton.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -738,6 +738,7 @@ const CHAT_CALLS = {
|
|||||||
backupMediaBatch: 'v1/archives/media/batch',
|
backupMediaBatch: 'v1/archives/media/batch',
|
||||||
backupMediaDelete: 'v1/archives/media/delete',
|
backupMediaDelete: 'v1/archives/media/delete',
|
||||||
callLinkCreateAuth: 'v1/call-link/create-auth',
|
callLinkCreateAuth: 'v1/call-link/create-auth',
|
||||||
|
callQualitySurvey: 'v1/call_quality_survey',
|
||||||
redeemReceipt: 'v1/donation/redeem-receipt',
|
redeemReceipt: 'v1/donation/redeem-receipt',
|
||||||
registration: 'v1/registration',
|
registration: 'v1/registration',
|
||||||
registerCapabilities: 'v1/devices/capabilities',
|
registerCapabilities: 'v1/devices/capabilities',
|
||||||
@@ -3337,6 +3338,19 @@ export async function callLinkCreateAuth(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function submitCallQualitySurvey(
|
||||||
|
survey: Proto.ISubmitCallQualitySurveyRequest
|
||||||
|
): Promise<void> {
|
||||||
|
const data = Proto.SubmitCallQualitySurveyRequest.encode(survey).finish();
|
||||||
|
await _ajax({
|
||||||
|
call: 'callQualitySurvey',
|
||||||
|
contentType: 'application/x-protobuf',
|
||||||
|
data,
|
||||||
|
host: 'chatService',
|
||||||
|
httpType: 'PUT',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function setPhoneNumberDiscoverability(
|
export async function setPhoneNumberDiscoverability(
|
||||||
newValue: boolean
|
newValue: boolean
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
|||||||
52
ts/types/CallQualitySurvey.std.ts
Normal file
52
ts/types/CallQualitySurvey.std.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
// Copyright 2025 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { CallEndedReason } from '@signalapp/ringrtc';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||||
|
export namespace CallQualitySurvey {
|
||||||
|
// IMPORTANT: These strings need to be the same across clients
|
||||||
|
export enum Issue {
|
||||||
|
AUDIO = 'audio',
|
||||||
|
AUDIO_STUTTERING = 'audio_stuttering',
|
||||||
|
AUDIO_LOCAL_ECHO = 'audio_local_echo',
|
||||||
|
AUDIO_REMOTE_ECHO = 'audio_remote_echo',
|
||||||
|
AUDIO_DROP = 'audio_drop',
|
||||||
|
VIDEO = 'video',
|
||||||
|
VIDEO_NO_CAMERA = 'video_no_camera',
|
||||||
|
VIDEO_LOW_QUALITY = 'video_low_quality',
|
||||||
|
VIDEO_LOW_RESOLUTION = 'video_low_resolution',
|
||||||
|
CALL_DROPPED = 'call_dropped',
|
||||||
|
OTHER = 'other',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CallType {
|
||||||
|
DIRECT_VOICE = 'direct_voice',
|
||||||
|
DIRECT_VIDEO = 'direct_video',
|
||||||
|
GROUP = 'group',
|
||||||
|
CALL_LINK = 'call_link',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Request = Readonly<{
|
||||||
|
userSatisfied: boolean;
|
||||||
|
callQualityIssues: ReadonlyArray<Issue>;
|
||||||
|
additionalIssuesDescription: string;
|
||||||
|
debugLogUrl: string | null;
|
||||||
|
startTimestamp: number;
|
||||||
|
endTimestamp: number;
|
||||||
|
callType: CallType;
|
||||||
|
success: boolean;
|
||||||
|
callEndReason: CallEndedReason;
|
||||||
|
rttMedian: number | null;
|
||||||
|
jitterMedian: number | null;
|
||||||
|
packetLossFraction: number | null;
|
||||||
|
callTelemetry: Uint8Array | null;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type Form = Readonly<{
|
||||||
|
userSatisfied: boolean;
|
||||||
|
callQualityIssues: ReadonlySet<Issue>;
|
||||||
|
additionalIssuesDescription: string;
|
||||||
|
shareDebugLog: boolean;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user