Show reconnecting notification when screensharing

This commit is contained in:
Fedor Indutny 2024-05-06 14:48:31 -07:00 committed by GitHub
parent de2def7119
commit 1280afe619
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 227 additions and 35 deletions

View file

@ -1813,10 +1813,22 @@
"messageformat": "Click here to return to the call when you're ready to stop presenting.", "messageformat": "Click here to return to the call when you're ready to stop presenting.",
"description": "Body text for the share screen notification" "description": "Body text for the share screen notification"
}, },
"icu:calling__presenting--reconnecting--notification-title": {
"messageformat": "Reconnecting...",
"description": "Title for the share screen reconnecting notification"
},
"icu:calling__presenting--reconnecting--notification-body": {
"messageformat": "Your connection was lost. Signal is reconnecting.",
"description": "Body text for the share screen reconnecting notification"
},
"icu:calling__presenting--info": { "icu:calling__presenting--info": {
"messageformat": "Signal is sharing {window}.", "messageformat": "Signal is sharing {window}.",
"description": "Text that appears in the screen sharing controller to inform person that they are presenting" "description": "Text that appears in the screen sharing controller to inform person that they are presenting"
}, },
"icu:calling__presenting--reconnecting": {
"messageformat": "Reconnecting...",
"description": "Text that appears in the screen sharing controller to inform person that the call is in reconnecting state"
},
"icu:calling__presenting--stop": { "icu:calling__presenting--stop": {
"messageformat": "Stop sharing", "messageformat": "Stop sharing",
"description": "Button for stopping screen sharing" "description": "Button for stopping screen sharing"

View file

@ -113,6 +113,7 @@ import { load as loadLocale } from './locale';
import type { LoggerType } from '../ts/types/Logging'; import type { LoggerType } from '../ts/types/Logging';
import { HourCyclePreference } from '../ts/types/I18N'; import { HourCyclePreference } from '../ts/types/I18N';
import { ScreenShareStatus } from '../ts/types/Calling';
import { DBVersionFromFutureError } from '../ts/sql/migrations'; import { DBVersionFromFutureError } from '../ts/sql/migrations';
import type { ParsedSignalRoute } from '../ts/util/signalRoutes'; import type { ParsedSignalRoute } from '../ts/util/signalRoutes';
import { parseSignalRoute } from '../ts/util/signalRoutes'; import { parseSignalRoute } from '../ts/util/signalRoutes';
@ -2332,11 +2333,20 @@ ipc.on(
} }
); );
ipc.on('close-screen-share-controller', () => { ipc.on(
if (screenShareWindow) { 'screen-share:status-change',
screenShareWindow.close(); (_event: Electron.Event, status: ScreenShareStatus) => {
if (!screenShareWindow) {
return;
}
if (status === ScreenShareStatus.Disconnected) {
screenShareWindow.close();
} else {
screenShareWindow.webContents.send('status-change', status);
}
} }
}); );
ipc.on('stop-screen-share', () => { ipc.on('stop-screen-share', () => {
if (mainWindow) { if (mainWindow) {

View file

@ -9,6 +9,7 @@ import type { PropsType } from './CallingScreenSharingController';
import { CallingScreenSharingController } from './CallingScreenSharingController'; import { CallingScreenSharingController } from './CallingScreenSharingController';
import { setupI18n } from '../util/setupI18n'; import { setupI18n } from '../util/setupI18n';
import { ScreenShareStatus } from '../types/Calling';
import enMessages from '../../_locales/en/messages.json'; import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
@ -18,6 +19,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
onCloseController: action('on-close-controller'), onCloseController: action('on-close-controller'),
onStopSharing: action('on-stop-sharing'), onStopSharing: action('on-stop-sharing'),
presentedSourceName: overrideProps.presentedSourceName || 'Application', presentedSourceName: overrideProps.presentedSourceName || 'Application',
status: overrideProps.status || ScreenShareStatus.Connected,
}); });
export default { export default {
@ -38,3 +40,13 @@ export function ReallyLongAppName(): JSX.Element {
/> />
); );
} }
export function Reconnecting(): JSX.Element {
return (
<CallingScreenSharingController
{...createProps({
status: ScreenShareStatus.Reconnecting,
})}
/>
);
}

View file

@ -4,11 +4,13 @@
import React from 'react'; import React from 'react';
import { Button, ButtonVariant } from './Button'; import { Button, ButtonVariant } from './Button';
import type { LocalizerType } from '../types/Util'; import type { LocalizerType } from '../types/Util';
import { ScreenShareStatus } from '../types/Calling';
export type PropsType = { export type PropsType = {
i18n: LocalizerType; i18n: LocalizerType;
onCloseController: () => unknown; onCloseController: () => unknown;
onStopSharing: () => unknown; onStopSharing: () => unknown;
status: ScreenShareStatus;
presentedSourceName: string; presentedSourceName: string;
}; };
@ -16,15 +18,22 @@ export function CallingScreenSharingController({
i18n, i18n,
onCloseController, onCloseController,
onStopSharing, onStopSharing,
status,
presentedSourceName, presentedSourceName,
}: PropsType): JSX.Element { }: PropsType): JSX.Element {
let text: string;
if (status === ScreenShareStatus.Reconnecting) {
text = i18n('icu:calling__presenting--reconnecting');
} else {
text = i18n('icu:calling__presenting--info', {
window: presentedSourceName,
});
}
return ( return (
<div className="module-CallingScreenSharingController"> <div className="module-CallingScreenSharingController">
<div className="module-CallingScreenSharingController__text"> <div className="module-CallingScreenSharingController__text">{text}</div>
{i18n('icu:calling__presenting--info', {
window: presentedSourceName,
})}
</div>
<div className="module-CallingScreenSharingController__buttons"> <div className="module-CallingScreenSharingController__buttons">
<Button <Button
className="module-CallingScreenSharingController__button" className="module-CallingScreenSharingController__button"

View file

@ -64,6 +64,7 @@ import {
CallMode, CallMode,
GroupCallConnectionState, GroupCallConnectionState,
GroupCallJoinState, GroupCallJoinState,
ScreenShareStatus,
} from '../types/Calling'; } from '../types/Calling';
import { import {
findBestMatchingAudioDeviceIndex, findBestMatchingAudioDeviceIndex,
@ -310,6 +311,22 @@ function protoToCallingMessage({
}; };
} }
export type NotifyScreenShareStatusOptionsType = Readonly<
{
conversationId?: string;
isPresenting: boolean;
} & (
| {
callMode: CallMode.Direct;
callState: CallState;
}
| {
callMode: CallMode.Group | CallMode.Adhoc;
connectionState: GroupCallConnectionState;
}
)
>;
export class CallingClass { export class CallingClass {
readonly videoCapturer: GumVideoCapturer; readonly videoCapturer: GumVideoCapturer;
@ -1610,7 +1627,10 @@ export class CallingClass {
); );
} }
ipcRenderer.send('close-screen-share-controller'); ipcRenderer.send(
'screen-share:status-change',
ScreenShareStatus.Disconnected
);
const entries = Object.entries(this.callsLookup); const entries = Object.entries(this.callsLookup);
log.info(`hangup: ${entries.length} call(s) to hang up...`); log.info(`hangup: ${entries.length} call(s) to hang up...`);
@ -1785,10 +1805,84 @@ export class CallingClass {
title: window.i18n('icu:calling__presenting--notification-title'), title: window.i18n('icu:calling__presenting--notification-title'),
}); });
} else { } else {
ipcRenderer.send('close-screen-share-controller'); ipcRenderer.send(
'screen-share:status-change',
ScreenShareStatus.Disconnected
);
} }
} }
async notifyScreenShareStatus(
options: NotifyScreenShareStatusOptionsType
): Promise<void> {
let newStatus: ScreenShareStatus;
if (options.callMode === CallMode.Direct) {
switch (options.callState) {
case CallState.Prering:
case CallState.Ringing:
case CallState.Accepted:
newStatus = ScreenShareStatus.Connected;
break;
case CallState.Reconnecting:
newStatus = ScreenShareStatus.Reconnecting;
break;
case CallState.Ended:
newStatus = ScreenShareStatus.Disconnected;
break;
default:
throw missingCaseError(options.callState);
}
} else {
switch (options.connectionState) {
case GroupCallConnectionState.NotConnected:
newStatus = ScreenShareStatus.Disconnected;
break;
case GroupCallConnectionState.Connecting:
case GroupCallConnectionState.Connected:
newStatus = ScreenShareStatus.Connected;
break;
case GroupCallConnectionState.Reconnecting:
newStatus = ScreenShareStatus.Reconnecting;
break;
default:
throw missingCaseError(options.connectionState);
}
}
const { conversationId, isPresenting } = options;
if (
options.callMode !== CallMode.Adhoc &&
isPresenting &&
conversationId &&
newStatus === ScreenShareStatus.Reconnecting
) {
const conversation = window.ConversationController.get(conversationId);
strictAssert(
conversation,
'showPresentingReconnectingNotification: conversation not found'
);
const { url, absolutePath } = await conversation.getAvatarOrIdenticon();
notificationService.notify({
conversationId,
iconPath: absolutePath,
iconUrl: url,
message: window.i18n(
'icu:calling__presenting--reconnecting--notification-body'
),
type: NotificationType.IsPresenting,
sentAt: 0,
silent: true,
title: window.i18n(
'icu:calling__presenting--reconnecting--notification-title'
),
});
}
ipcRenderer.send('screen-share:status-change', newStatus);
}
private async startDeviceReselectionTimer(): Promise<void> { private async startDeviceReselectionTimer(): Promise<void> {
// Poll once // Poll once
await this.pollForMediaDevices(); await this.pollForMediaDevices();

View file

@ -1,7 +1,6 @@
// Copyright 2020 Signal Messenger, LLC // Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { ipcRenderer } from 'electron';
import type { ThunkAction, ThunkDispatch } from 'redux-thunk'; import type { ThunkAction, ThunkDispatch } from 'redux-thunk';
import { import {
hasScreenCapturePermission, hasScreenCapturePermission,
@ -1013,10 +1012,6 @@ function callStateChange(
return async dispatch => { return async dispatch => {
const { callState, acceptedTime, callEndedReason } = payload; const { callState, acceptedTime, callEndedReason } = payload;
if (callState === CallState.Ended) {
ipcRenderer.send('close-screen-share-controller');
}
const wasAccepted = acceptedTime != null; const wasAccepted = acceptedTime != null;
const isEnded = callState === CallState.Ended && callEndedReason != null; const isEnded = callState === CallState.Ended && callEndedReason != null;
@ -1304,10 +1299,6 @@ function groupCallStateChange(
if (didSomeoneStartPresenting) { if (didSomeoneStartPresenting) {
void callingTones.someonePresenting(); void callingTones.someonePresenting();
} }
if (payload.connectionState === GroupCallConnectionState.NotConnected) {
ipcRenderer.send('close-screen-share-controller');
}
}; };
} }
@ -2630,6 +2621,25 @@ export function reducer(
} }
if (action.type === CALL_STATE_CHANGE_FULFILLED) { if (action.type === CALL_STATE_CHANGE_FULFILLED) {
const call = getOwn(
state.callsByConversation,
action.payload.conversationId
);
if (
call?.callMode === CallMode.Direct &&
call?.callState !== action.payload.callState
) {
drop(
calling.notifyScreenShareStatus({
callMode: CallMode.Direct,
callState: action.payload.callState,
isPresenting: state.activeCallState?.presentingSource != null,
conversationId: state.activeCallState?.conversationId,
})
);
}
// We want to keep the state around for ended calls if they resulted in a message // We want to keep the state around for ended calls if they resulted in a message
// request so we can show the "needs permission" screen. // request so we can show the "needs permission" screen.
if ( if (
@ -2640,10 +2650,6 @@ export function reducer(
return removeConversationFromState(state, action.payload.conversationId); return removeConversationFromState(state, action.payload.conversationId);
} }
const call = getOwn(
state.callsByConversation,
action.payload.conversationId
);
if (call?.callMode !== CallMode.Direct) { if (call?.callMode !== CallMode.Direct) {
log.warn('Cannot update state for a non-direct call'); log.warn('Cannot update state for a non-direct call');
return state; return state;
@ -2830,6 +2836,17 @@ export function reducer(
...newRingState, ...newRingState,
}; };
if (existingCall?.connectionState !== connectionState) {
drop(
calling.notifyScreenShareStatus({
callMode,
connectionState,
isPresenting: state.activeCallState?.presentingSource != null,
conversationId: state.activeCallState?.conversationId,
})
);
}
return { return {
...state, ...state,
...mergeCallWithGroupCallLookups({ ...mergeCallWithGroupCallLookups({

View file

@ -205,3 +205,9 @@ export type ChangeIODevicePayloadType =
export type CallingConversationType = export type CallingConversationType =
| ConversationType | ConversationType
| CallLinkConversationType; | CallLinkConversationType;
export enum ScreenShareStatus {
Connected = 'Connected',
Reconnecting = 'Reconnecting',
Disconnected = 'Disconnected',
}

3
ts/window.d.ts vendored
View file

@ -39,6 +39,7 @@ import type { BatcherType } from './util/batcher';
import type { ConfirmationDialog } from './components/ConfirmationDialog'; import type { ConfirmationDialog } from './components/ConfirmationDialog';
import type { SignalProtocolStore } from './SignalProtocolStore'; import type { SignalProtocolStore } from './SignalProtocolStore';
import type { SocketStatus } from './types/SocketStatus'; import type { SocketStatus } from './types/SocketStatus';
import type { ScreenShareStatus } from './types/Calling';
import type SyncRequest from './textsecure/SyncRequest'; import type SyncRequest from './textsecure/SyncRequest';
import type { MessageCache } from './services/MessageCache'; import type { MessageCache } from './services/MessageCache';
import type { StateType } from './state/reducer'; import type { StateType } from './state/reducer';
@ -122,6 +123,8 @@ type PermissionsWindowPropsType = {
type ScreenShareWindowPropsType = { type ScreenShareWindowPropsType = {
onStopSharing: () => void; onStopSharing: () => void;
presentedSourceName: string; presentedSourceName: string;
getStatus: () => ScreenShareStatus;
setRenderCallback: (cb: () => void) => void;
}; };
type SettingsOnRenderCallbackType = (props: PreferencesPropsType) => void; type SettingsOnRenderCallbackType = (props: PreferencesPropsType) => void;

View file

@ -7,6 +7,7 @@ import ReactDOM from 'react-dom';
import { CallingScreenSharingController } from '../../components/CallingScreenSharingController'; import { CallingScreenSharingController } from '../../components/CallingScreenSharingController';
import { i18n } from '../sandboxedInit'; import { i18n } from '../sandboxedInit';
import { strictAssert } from '../../util/assert'; import { strictAssert } from '../../util/assert';
import { drop } from '../../util/drop';
import { parseEnvironment, setEnvironment } from '../../environment'; import { parseEnvironment, setEnvironment } from '../../environment';
const { ScreenShareWindowProps } = window.Signal; const { ScreenShareWindowProps } = window.Signal;
@ -18,15 +19,27 @@ setEnvironment(
window.SignalContext.isTestOrMockEnvironment() window.SignalContext.isTestOrMockEnvironment()
); );
ReactDOM.render( function onCloseController(): void {
<div className="App dark-theme"> drop(window.SignalContext.executeMenuRole('close'));
<CallingScreenSharingController }
i18n={i18n}
onCloseController={() => window.SignalContext.executeMenuRole('close')}
onStopSharing={ScreenShareWindowProps.onStopSharing}
presentedSourceName={ScreenShareWindowProps.presentedSourceName}
/>
</div>,
document.getElementById('app') function render() {
); // Pacify typescript
strictAssert(ScreenShareWindowProps, 'window values not provided');
ReactDOM.render(
<div className="App dark-theme">
<CallingScreenSharingController
i18n={i18n}
onCloseController={onCloseController}
onStopSharing={ScreenShareWindowProps.onStopSharing}
status={ScreenShareWindowProps.getStatus()}
presentedSourceName={ScreenShareWindowProps.presentedSourceName}
/>
</div>,
document.getElementById('app')
);
}
render();
ScreenShareWindowProps.setRenderCallback(render);

View file

@ -2,17 +2,33 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { contextBridge, ipcRenderer } from 'electron'; import { contextBridge, ipcRenderer } from 'electron';
import { ScreenShareStatus } from '../../types/Calling';
import { MinimalSignalContext } from '../minimalContext'; import { MinimalSignalContext } from '../minimalContext';
const params = new URLSearchParams(document.location.search); const params = new URLSearchParams(document.location.search);
let renderCallback: undefined | (() => undefined);
let status = ScreenShareStatus.Connected;
const Signal = { const Signal = {
ScreenShareWindowProps: { ScreenShareWindowProps: {
onStopSharing: () => { onStopSharing: () => {
ipcRenderer.send('stop-screen-share'); ipcRenderer.send('stop-screen-share');
}, },
presentedSourceName: params.get('sourceName'), presentedSourceName: params.get('sourceName'),
getStatus() {
return status;
},
setRenderCallback(callback: () => undefined) {
renderCallback = callback;
},
}, },
}; };
contextBridge.exposeInMainWorld('Signal', Signal); contextBridge.exposeInMainWorld('Signal', Signal);
contextBridge.exposeInMainWorld('SignalContext', MinimalSignalContext); contextBridge.exposeInMainWorld('SignalContext', MinimalSignalContext);
ipcRenderer.on('status-change', (_, newStatus: ScreenShareStatus) => {
status = newStatus;
renderCallback?.();
});