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",
|
||||
"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",
|
||||
|
||||
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"
|
||||
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}
|
||||
</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',
|
||||
backupMediaDelete: 'v1/archives/media/delete',
|
||||
callLinkCreateAuth: 'v1/call-link/create-auth',
|
||||
callQualitySurvey: 'v1/call_quality_survey',
|
||||
redeemReceipt: 'v1/donation/redeem-receipt',
|
||||
registration: 'v1/registration',
|
||||
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(
|
||||
newValue: boolean
|
||||
): 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