-
Notifications
You must be signed in to change notification settings - Fork 73
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Notifications for calling errors in composite (#4737)
* error notifications * Change files * fix build * build files * Update packages/react-composites CallComposite browser test snapshots * fix test * Update packages/react-composites CallComposite browser test snapshots * Update packages/react-composites ChatComposite browser test snapshots * Update packages/react-composites CallComposite browser test snapshots * Update packages/react-composites ChatComposite browser test snapshots --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
- Loading branch information
1 parent
d6600b2
commit 83d44f7
Showing
117 changed files
with
646 additions
and
169 deletions.
There are no files selected for viewing
9 changes: 9 additions & 0 deletions
9
change-beta/@azure-communication-react-57fb0cf7-b0ea-4862-8137-997abd8a9ce8.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"type": "prerelease", | ||
"area": "feature", | ||
"workstream": "Notifications", | ||
"comment": "Error notifications in composite", | ||
"packageName": "@azure/communication-react", | ||
"email": "[email protected]", | ||
"dependentChangeType": "patch" | ||
} |
210 changes: 210 additions & 0 deletions
210
packages/calling-component-bindings/src/errorNotificationsSelector.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,210 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT License. | ||
/* @conditional-compile-remove(notifications) */ | ||
import { | ||
CallingBaseSelectorProps, | ||
getDeviceManager, | ||
getDiagnostics, | ||
getLatestErrors, | ||
getEnvironmentInfo | ||
} from './baseSelectors'; | ||
/* @conditional-compile-remove(notifications) */ | ||
import { ActiveNotification, NotificationType } from '@internal/react-components'; | ||
/* @conditional-compile-remove(notifications) */ | ||
import { createSelector } from 'reselect'; | ||
/* @conditional-compile-remove(notifications) */ | ||
import { CallClientState, CallErrors, CallErrorTarget } from '@internal/calling-stateful-client'; | ||
/* @conditional-compile-remove(notifications) */ | ||
import { DiagnosticQuality } from '@azure/communication-calling'; | ||
/* @conditional-compile-remove(notifications) */ | ||
/** | ||
* Selector type for {@link Notification} component. | ||
* | ||
* @beta | ||
*/ | ||
export type ErrorNotificationsSelector = ( | ||
state: CallClientState, | ||
props: CallingBaseSelectorProps | ||
) => { | ||
activeErrorMessages: ActiveNotification[]; | ||
}; | ||
/* @conditional-compile-remove(notifications) */ | ||
/** | ||
* Select the active errors from the state for the `Notification` component. | ||
* | ||
* Invariants: | ||
* - `ErrorType` is never repeated in the returned errors. | ||
* - Errors are returned in a fixed order by `ErrorType`. | ||
* | ||
* @beta | ||
*/ | ||
export const errorNotificationsSelector: ErrorNotificationsSelector = createSelector( | ||
[getLatestErrors, getDiagnostics, getDeviceManager, getEnvironmentInfo], | ||
( | ||
latestErrors: CallErrors, | ||
diagnostics, | ||
deviceManager, | ||
environmentInfo | ||
): { activeErrorMessages: ActiveNotification[] } => { | ||
// The order in which the errors are returned is significant: The `Notification` shows errors on the UI in that order. | ||
// There are several options for the ordering: | ||
// - Sorted by when the errors happened (latest first / oldest first). | ||
// - Stable sort by error type. | ||
// | ||
// We chose to stable sort by error type: We intend to show only a small number of errors on the UI and we do not | ||
// have timestamps for errors. | ||
const activeErrorMessages: ActiveNotification[] = []; | ||
|
||
const isSafari = (): boolean => { | ||
/* @conditional-compile-remove(calling-environment-info) */ | ||
return environmentInfo?.environment.browser === 'safari'; | ||
return /^((?!chrome|android|crios|fxios).)*safari/i.test(navigator.userAgent); | ||
}; | ||
|
||
const isMacOS = (): boolean => { | ||
/* @conditional-compile-remove(calling-environment-info) */ | ||
return environmentInfo?.environment.platform === 'mac'; | ||
return false; | ||
}; | ||
|
||
// Errors reported via diagnostics are more reliable than from API method failures, so process those first. | ||
if ( | ||
diagnostics?.network.latest.networkReceiveQuality?.value === DiagnosticQuality.Bad || | ||
diagnostics?.network.latest.networkReceiveQuality?.value === DiagnosticQuality.Poor | ||
) { | ||
activeErrorMessages.push({ type: 'callNetworkQualityLow' }); | ||
} | ||
if (diagnostics?.media.latest.noSpeakerDevicesEnumerated?.value === true) { | ||
activeErrorMessages.push({ type: 'callNoSpeakerFound' }); | ||
} | ||
if (diagnostics?.media.latest.noMicrophoneDevicesEnumerated?.value === true) { | ||
activeErrorMessages.push({ type: 'callNoMicrophoneFound' }); | ||
} | ||
if (deviceManager.deviceAccess?.audio === false && isSafari()) { | ||
activeErrorMessages.push({ type: 'callMicrophoneAccessDeniedSafari' }); | ||
} | ||
if (deviceManager.deviceAccess?.audio === false && !isSafari()) { | ||
activeErrorMessages.push({ type: 'callMicrophoneAccessDenied' }); | ||
} | ||
|
||
if (diagnostics?.media.latest.microphonePermissionDenied?.value === true && isMacOS()) { | ||
activeErrorMessages.push({ type: 'callMacOsMicrophoneAccessDenied' }); | ||
} else if (diagnostics?.media.latest.microphonePermissionDenied?.value === true) { | ||
activeErrorMessages.push({ type: 'callMicrophoneAccessDenied' }); | ||
} | ||
|
||
const microphoneMuteUnexpectedlyDiagnostic = | ||
diagnostics?.media.latest.microphoneMuteUnexpectedly || diagnostics?.media.latest.microphoneNotFunctioning; | ||
if (microphoneMuteUnexpectedlyDiagnostic) { | ||
if (microphoneMuteUnexpectedlyDiagnostic.value === DiagnosticQuality.Bad) { | ||
// Inform the user that microphone stopped working and inform them to start microphone again | ||
activeErrorMessages.push({ type: 'callMicrophoneMutedBySystem' }); | ||
} else if (microphoneMuteUnexpectedlyDiagnostic.value === DiagnosticQuality.Good) { | ||
// Inform the user that microphone recovered | ||
activeErrorMessages.push({ type: 'callMicrophoneUnmutedBySystem' }); | ||
} | ||
} | ||
|
||
const cameraStoppedUnexpectedlyDiagnostic = diagnostics?.media.latest.cameraStoppedUnexpectedly; | ||
if (cameraStoppedUnexpectedlyDiagnostic) { | ||
if (cameraStoppedUnexpectedlyDiagnostic.value === DiagnosticQuality.Bad) { | ||
// Inform the user that camera stopped working and inform them to start video again | ||
activeErrorMessages.push({ type: 'callVideoStoppedBySystem' }); | ||
} else if (cameraStoppedUnexpectedlyDiagnostic.value === DiagnosticQuality.Good) { | ||
// Inform the user that camera recovered | ||
activeErrorMessages.push({ type: 'callVideoRecoveredBySystem' }); | ||
} | ||
} | ||
if (deviceManager.deviceAccess?.video === false && isSafari()) { | ||
activeErrorMessages.push({ type: 'callCameraAccessDeniedSafari' }); | ||
} else if (deviceManager.deviceAccess?.video === false) { | ||
activeErrorMessages.push({ type: 'callCameraAccessDenied' }); | ||
} else { | ||
if (diagnostics?.media.latest.cameraFreeze?.value === true) { | ||
activeErrorMessages.push({ type: 'cameraFrozenForRemoteParticipants' }); | ||
} | ||
} | ||
|
||
/** | ||
* show the Mac specific strings if the platform is detected as mac | ||
*/ | ||
if (diagnostics?.media.latest.cameraPermissionDenied?.value === true && isMacOS()) { | ||
activeErrorMessages.push({ type: 'callMacOsCameraAccessDenied' }); | ||
} | ||
|
||
/** | ||
* This UFD only works on mac still so we should only see it fire on mac. | ||
*/ | ||
if (diagnostics?.media.latest.screenshareRecordingDisabled?.value === true && isMacOS()) { | ||
activeErrorMessages.push({ type: 'callMacOsScreenShareAccessDenied' }); | ||
} else if (diagnostics?.media.latest.screenshareRecordingDisabled?.value === true) { | ||
activeErrorMessages.push({ type: 'startScreenShareGeneric' }); | ||
} | ||
|
||
// Prefer to show errors with privacy implications. | ||
appendActiveErrorIfDefined(activeErrorMessages, latestErrors, 'Call.stopVideo', 'stopVideoGeneric'); | ||
appendActiveErrorIfDefined(activeErrorMessages, latestErrors, 'Call.mute', 'muteGeneric'); | ||
appendActiveErrorIfDefined(activeErrorMessages, latestErrors, 'Call.stopScreenSharing', 'stopScreenShareGeneric'); | ||
|
||
if ( | ||
latestErrors['Call.startVideo']?.message === 'Call.startVideo: Video operation failure SourceUnavailableError' | ||
) { | ||
appendActiveErrorIfDefined(activeErrorMessages, latestErrors, 'Call.startVideo', 'callCameraAlreadyInUse'); | ||
} else if ( | ||
latestErrors['Call.startVideo']?.message === 'Call.startVideo: Video operation failure permissionDeniedError' | ||
) { | ||
appendActiveErrorIfDefined(activeErrorMessages, latestErrors, 'Call.startVideo', 'callCameraAccessDenied'); | ||
} else { | ||
appendActiveErrorIfDefined(activeErrorMessages, latestErrors, 'Call.startVideo', 'startVideoGeneric'); | ||
} | ||
|
||
appendActiveErrorIfDefined(activeErrorMessages, latestErrors, 'Call.unmute', 'unmuteGeneric'); | ||
|
||
appendActiveErrorIfDefined( | ||
activeErrorMessages, | ||
latestErrors, | ||
'VideoEffectsFeature.startEffects', | ||
'unableToStartVideoEffect' | ||
); | ||
|
||
if (latestErrors['CallAgent.join']?.message === 'CallAgent.join: Invalid meeting link') { | ||
appendActiveErrorIfDefined( | ||
activeErrorMessages, | ||
latestErrors, | ||
'CallAgent.join', | ||
'failedToJoinCallInvalidMeetingLink' | ||
); | ||
} else { | ||
appendActiveErrorIfDefined(activeErrorMessages, latestErrors, 'CallAgent.join', 'failedToJoinCallGeneric'); | ||
} | ||
|
||
if ( | ||
latestErrors['Call.feature']?.message.match( | ||
/Call\.feature: startSpotlight failed\. \d+ is the max number of participants that can be Spotlighted/g | ||
) | ||
) { | ||
appendActiveErrorIfDefined( | ||
activeErrorMessages, | ||
latestErrors, | ||
'Call.feature', | ||
'startSpotlightWhileMaxParticipantsAreSpotlighted' | ||
); | ||
} | ||
return { activeErrorMessages: activeErrorMessages }; | ||
} | ||
); | ||
/* @conditional-compile-remove(notifications) */ | ||
const appendActiveErrorIfDefined = ( | ||
activeErrorMessages: ActiveNotification[], | ||
latestErrors: CallErrors, | ||
target: CallErrorTarget, | ||
activeErrorType: NotificationType | ||
): void => { | ||
if (latestErrors[target] === undefined) { | ||
return; | ||
} | ||
activeErrorMessages.push({ | ||
type: activeErrorType, | ||
timestamp: latestErrors[target].timestamp | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.