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.",
"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": {
"messageformat": "Signal is sharing {window}.",
"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": {
"messageformat": "Stop 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 { HourCyclePreference } from '../ts/types/I18N';
import { ScreenShareStatus } from '../ts/types/Calling';
import { DBVersionFromFutureError } from '../ts/sql/migrations';
import type { ParsedSignalRoute } from '../ts/util/signalRoutes';
import { parseSignalRoute } from '../ts/util/signalRoutes';
@ -2332,11 +2333,20 @@ ipc.on(
}
);
ipc.on('close-screen-share-controller', () => {
if (screenShareWindow) {
screenShareWindow.close();
ipc.on(
'screen-share:status-change',
(_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', () => {
if (mainWindow) {

View file

@ -9,6 +9,7 @@ import type { PropsType } from './CallingScreenSharingController';
import { CallingScreenSharingController } from './CallingScreenSharingController';
import { setupI18n } from '../util/setupI18n';
import { ScreenShareStatus } from '../types/Calling';
import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages);
@ -18,6 +19,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
onCloseController: action('on-close-controller'),
onStopSharing: action('on-stop-sharing'),
presentedSourceName: overrideProps.presentedSourceName || 'Application',
status: overrideProps.status || ScreenShareStatus.Connected,
});
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 { Button, ButtonVariant } from './Button';
import type { LocalizerType } from '../types/Util';
import { ScreenShareStatus } from '../types/Calling';
export type PropsType = {
i18n: LocalizerType;
onCloseController: () => unknown;
onStopSharing: () => unknown;
status: ScreenShareStatus;
presentedSourceName: string;
};
@ -16,15 +18,22 @@ export function CallingScreenSharingController({
i18n,
onCloseController,
onStopSharing,
status,
presentedSourceName,
}: 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 (
<div className="module-CallingScreenSharingController">
<div className="module-CallingScreenSharingController__text">
{i18n('icu:calling__presenting--info', {
window: presentedSourceName,
})}
</div>
<div className="module-CallingScreenSharingController__text">{text}</div>
<div className="module-CallingScreenSharingController__buttons">
<Button
className="module-CallingScreenSharingController__button"

View file

@ -64,6 +64,7 @@ import {
CallMode,
GroupCallConnectionState,
GroupCallJoinState,
ScreenShareStatus,
} from '../types/Calling';
import {
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 {
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);
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'),
});
} 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> {
// Poll once
await this.pollForMediaDevices();

View file

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

View file

@ -205,3 +205,9 @@ export type ChangeIODevicePayloadType =
export type CallingConversationType =
| ConversationType
| 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 { SignalProtocolStore } from './SignalProtocolStore';
import type { SocketStatus } from './types/SocketStatus';
import type { ScreenShareStatus } from './types/Calling';
import type SyncRequest from './textsecure/SyncRequest';
import type { MessageCache } from './services/MessageCache';
import type { StateType } from './state/reducer';
@ -122,6 +123,8 @@ type PermissionsWindowPropsType = {
type ScreenShareWindowPropsType = {
onStopSharing: () => void;
presentedSourceName: string;
getStatus: () => ScreenShareStatus;
setRenderCallback: (cb: () => void) => void;
};
type SettingsOnRenderCallbackType = (props: PreferencesPropsType) => void;

View file

@ -7,6 +7,7 @@ import ReactDOM from 'react-dom';
import { CallingScreenSharingController } from '../../components/CallingScreenSharingController';
import { i18n } from '../sandboxedInit';
import { strictAssert } from '../../util/assert';
import { drop } from '../../util/drop';
import { parseEnvironment, setEnvironment } from '../../environment';
const { ScreenShareWindowProps } = window.Signal;
@ -18,15 +19,27 @@ setEnvironment(
window.SignalContext.isTestOrMockEnvironment()
);
ReactDOM.render(
<div className="App dark-theme">
<CallingScreenSharingController
i18n={i18n}
onCloseController={() => window.SignalContext.executeMenuRole('close')}
onStopSharing={ScreenShareWindowProps.onStopSharing}
presentedSourceName={ScreenShareWindowProps.presentedSourceName}
/>
</div>,
function onCloseController(): void {
drop(window.SignalContext.executeMenuRole('close'));
}
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
import { contextBridge, ipcRenderer } from 'electron';
import { ScreenShareStatus } from '../../types/Calling';
import { MinimalSignalContext } from '../minimalContext';
const params = new URLSearchParams(document.location.search);
let renderCallback: undefined | (() => undefined);
let status = ScreenShareStatus.Connected;
const Signal = {
ScreenShareWindowProps: {
onStopSharing: () => {
ipcRenderer.send('stop-screen-share');
},
presentedSourceName: params.get('sourceName'),
getStatus() {
return status;
},
setRenderCallback(callback: () => undefined) {
renderCallback = callback;
},
},
};
contextBridge.exposeInMainWorld('Signal', Signal);
contextBridge.exposeInMainWorld('SignalContext', MinimalSignalContext);
ipcRenderer.on('status-change', (_, newStatus: ScreenShareStatus) => {
status = newStatus;
renderCallback?.();
});