(`${getPluginPath()}/stats`, {method: 'get'});
};
+
+export const selectRHSPost = (postID: string): ActionFuncAsync => {
+ return async (dispatch: DispatchFunc) => {
+ if (window.ProductApi) {
+ dispatch(window.ProductApi.selectRhsPost(postID));
+ }
+ return {};
+ };
+};
diff --git a/webapp/src/components/call_widget/component.tsx b/webapp/src/components/call_widget/component.tsx
index bde6eadda..5ba6c5380 100644
--- a/webapp/src/components/call_widget/component.tsx
+++ b/webapp/src/components/call_widget/component.tsx
@@ -19,7 +19,12 @@ import {Badge} from 'src/components/badge';
import {ParticipantsList} from 'src/components/call_widget/participants_list';
import {RemoveConfirmation} from 'src/components/call_widget/remove_confirmation';
import DotMenu, {DotMenuButton} from 'src/components/dot_menu/dot_menu';
+import {
+ IDStopRecordingConfirmation,
+ StopRecordingConfirmation,
+} from 'src/components/expanded_view/stop_recording_confirmation';
import {HostNotices} from 'src/components/host_notices';
+import ChatThreadIcon from 'src/components/icons/chat_thread';
import CompassIcon from 'src/components/icons/compassIcon';
import ExpandIcon from 'src/components/icons/expand';
import HorizontalDotsIcon from 'src/components/icons/horizontal_dots';
@@ -29,6 +34,7 @@ import ParticipantsIcon from 'src/components/icons/participants';
import PopOutIcon from 'src/components/icons/popout';
import RaisedHandIcon from 'src/components/icons/raised_hand';
import RecordCircleIcon from 'src/components/icons/record_circle';
+import RecordSquareIcon from 'src/components/icons/record_square';
import SettingsWheelIcon from 'src/components/icons/settings_wheel';
import ShareScreenIcon from 'src/components/icons/share_screen';
import ShowMoreIcon from 'src/components/icons/show_more';
@@ -55,6 +61,7 @@ import {
reverseKeyMappings,
SHARE_UNSHARE_SCREEN,
} from 'src/shortcuts';
+import {ModalData} from 'src/types/mattermost-webapp';
import * as Telemetry from 'src/types/telemetry';
import {
AudioDevices,
@@ -100,6 +107,7 @@ interface Props {
callHostID: string,
callHostChangeAt: number,
callRecording?: CallJobReduxState,
+ isRecording: boolean,
screenSharingSession?: UserSessionState,
show: boolean,
showExpandedView: () => void,
@@ -119,6 +127,12 @@ interface Props {
callsIncoming: IncomingCallNotification[],
transcriptionsEnabled: boolean,
clientConnecting: boolean,
+ callThreadID?: string,
+ selectRHSPost: (id: string) => void,
+ startCallRecording: (channelID: string) => void,
+ stopCallRecording: (channelID: string) => void,
+ recordingsEnabled: boolean,
+ openModal: (modalData: ModalData
) => void;
}
interface DraggingState {
@@ -314,6 +328,27 @@ export default class CallWidget extends React.PureComponent {
}
};
+ setMissingScreenPermissions = (missing: boolean, forward?: boolean) => {
+ this.setState({
+ alerts: {
+ ...this.state.alerts,
+ missingScreenPermissions: {
+ ...this.state.alerts.missingScreenPermissions,
+ active: missing,
+ show: missing,
+ },
+ },
+ });
+
+ if (forward && this.state.expandedViewWindow) {
+ this.state.expandedViewWindow.callActions?.setMissingScreenPermissions(missing);
+ }
+
+ if (window.currentCallData) {
+ window.currentCallData.missingScreenPermissions = missing;
+ }
+ };
+
private onViewportResize = () => {
if (window.devicePixelRatio === this.prevDevicePixelRatio) {
return;
@@ -330,16 +365,7 @@ export default class CallWidget extends React.PureComponent {
if (ev.data.type === 'calls-error' && ev.data.message.err === 'screen-permissions') {
logDebug('screen permissions error');
- this.setState({
- alerts: {
- ...this.state.alerts,
- missingScreenPermissions: {
- ...this.state.alerts.missingScreenPermissions,
- active: true,
- show: true,
- },
- },
- });
+ this.setMissingScreenPermissions(true, true);
} else if (ev.data.type === 'calls-widget-share-screen') {
this.shareScreen(ev.data.message.sourceID, ev.data.message.withAudio);
}
@@ -403,16 +429,7 @@ export default class CallWidget extends React.PureComponent {
logDebug('desktopAPI.onCallsError', err);
if (err === 'screen-permissions') {
logDebug('screen permissions error');
- this.setState({
- alerts: {
- ...this.state.alerts,
- missingScreenPermissions: {
- ...this.state.alerts.missingScreenPermissions,
- active: true,
- show: true,
- },
- },
- });
+ this.setMissingScreenPermissions(true, true);
}
}));
} else {
@@ -447,6 +464,7 @@ export default class CallWidget extends React.PureComponent {
// set cross-window actions
window.callActions = {
setRecordingPromptDismissedAt: this.props.recordingPromptDismissedAt,
+ setMissingScreenPermissions: this.setMissingScreenPermissions,
};
this.attachVoiceTracks(window.callsClient.getRemoteVoiceTracks());
@@ -655,32 +673,13 @@ export default class CallWidget extends React.PureComponent {
};
private shareScreen = async (sourceID: string, _withAudio: boolean) => {
- const state = {} as State;
const stream = await window.callsClient?.shareScreen(sourceID, hasExperimentalFlag());
if (stream) {
- state.screenStream = stream;
- state.alerts = {
- ...this.state.alerts,
- missingScreenPermissions: {
- ...this.state.alerts.missingScreenPermissions,
- active: false,
- show: false,
- },
- };
+ this.setState({screenStream: stream});
+ this.setMissingScreenPermissions(false, true);
} else {
- state.alerts = {
- ...this.state.alerts,
- missingScreenPermissions: {
- ...this.state.alerts.missingScreenPermissions,
- active: true,
- show: true,
- },
- };
+ this.setMissingScreenPermissions(true, true);
}
-
- this.setState({
- ...state,
- });
};
dismissRecordingPrompt = () => {
@@ -696,6 +695,57 @@ export default class CallWidget extends React.PureComponent {
this.state.expandedViewWindow?.callActions?.setRecordingPromptDismissedAt(this.props.channel.id, Date.now());
};
+ onRecordToggle = async () => {
+ if (!this.props.channel) {
+ logErr('channel should be defined');
+ return;
+ }
+
+ const recording = this.props.callRecording;
+ const isRecording = (recording?.start_at ?? 0) > (recording?.end_at ?? 0);
+
+ if (isRecording) {
+ if (this.props.global) {
+ if (window.desktopAPI?.openStopRecordingModal) {
+ logDebug('desktopAPI.openStopRecordingModal');
+ window.desktopAPI.openStopRecordingModal(this.props.channel.id);
+ } else {
+ this.props.stopCallRecording(this.props.channel.id);
+ }
+ } else {
+ this.props.openModal({
+ modalId: IDStopRecordingConfirmation,
+ dialogType: StopRecordingConfirmation,
+ dialogProps: {
+ channelID: this.props.channel.id,
+ },
+ });
+ }
+ this.props.trackEvent(Telemetry.Event.StopRecording, Telemetry.Source.Widget, {initiator: 'button'});
+ } else {
+ await this.props.startCallRecording(this.props.channel.id);
+ this.props.trackEvent(Telemetry.Event.StartRecording, Telemetry.Source.Widget, {initiator: 'button'});
+ }
+
+ this.setState({showMenu: false});
+ };
+
+ onChatThreadButtonClick = () => {
+ if (!this.props.callThreadID) {
+ logErr('missing thread ID');
+ return;
+ }
+
+ if (this.props.global && window.desktopAPI?.openThreadForCalls) {
+ logDebug('desktopAPI.openThreadForCalls');
+ window.desktopAPI.openThreadForCalls(this.props.callThreadID);
+ } else {
+ this.props.selectRHSPost(this.props.callThreadID);
+ }
+
+ this.setState({showMenu: false});
+ };
+
onShareScreenToggle = async (fromShortcut?: boolean) => {
if (!this.props.allowScreenSharing) {
return;
@@ -1236,7 +1286,92 @@ export default class CallWidget extends React.PureComponent {
}
- {deviceType === 'output' && }
+
+ );
+ };
+
+ renderChatThreadMenuItem = () => {
+ const {formatMessage} = this.props.intl;
+
+ // If we are on global widget we should show this
+ // only if we have the matching functionality available.
+ if (this.props.global && !window.desktopAPI?.openThreadForCalls) {
+ return null;
+ }
+
+ return (
+ <>
+
+
+
+ >
+ );
+ };
+
+ renderRecordingMenuItem = () => {
+ const {formatMessage} = this.props.intl;
+
+ const RecordIcon = this.props.isRecording ? RecordSquareIcon : RecordCircleIcon;
+
+ return (
+
+
+
+
);
};
@@ -1305,7 +1440,6 @@ export default class CallWidget extends React.PureComponent {
-
);
};
@@ -1315,15 +1449,30 @@ export default class CallWidget extends React.PureComponent {
return null;
}
+ const isHost = this.props.callHostID === this.props.currentUserID;
+
+ const divider = (
+
+ );
+
+ const showScreenShareItem = this.props.allowScreenSharing && !this.props.wider;
+
return (
-
+
- {this.props.allowScreenSharing && !this.props.wider && this.renderScreenSharingMenuItem()}
{this.renderAudioDevices('output')}
{this.renderAudioDevices('input')}
+ { divider }
+ {showScreenShareItem && this.renderScreenSharingMenuItem()}
+ {showScreenShareItem && divider}
+ {this.props.recordingsEnabled && isHost && this.renderRecordingMenuItem()}
+ {this.renderChatThreadMenuItem()}
);
diff --git a/webapp/src/components/call_widget/index.ts b/webapp/src/components/call_widget/index.ts
index 311da43c8..f71e43aa6 100644
--- a/webapp/src/components/call_widget/index.ts
+++ b/webapp/src/components/call_widget/index.ts
@@ -5,7 +5,15 @@ import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {injectIntl} from 'react-intl';
import {connect} from 'react-redux';
import {bindActionCreators, Dispatch} from 'redux';
-import {recordingPromptDismissedAt, showExpandedView, showScreenSourceModal, trackEvent} from 'src/actions';
+import {
+ recordingPromptDismissedAt,
+ selectRHSPost,
+ showExpandedView,
+ showScreenSourceModal,
+ startCallRecording,
+ stopCallRecording,
+ trackEvent,
+} from 'src/actions';
import {
allowScreenSharing,
callStartAtForCurrentCall,
@@ -15,17 +23,21 @@ import {
hostChangeAtForCurrentCall,
hostControlNoticesForCurrentCall,
hostIDForCurrentCall,
+ isRecordingInCurrentCall,
profilesInCurrentCallMap,
recentlyJoinedUsersInCurrentCall,
recordingForCurrentCall,
+ recordingsEnabled,
screenSharingSessionForCurrentCall,
sessionForCurrentCall,
sessionsInCurrentCall,
sessionsInCurrentCallMap,
sortedIncomingCalls,
+ threadIDForCallInChannel,
transcriptionsEnabled,
} from 'src/selectors';
import {alphaSortSessions, stateSortSessions} from 'src/utils';
+import {modals} from 'src/webapp_globals';
import CallWidget from './component';
@@ -46,6 +58,8 @@ const mapStateToProps = (state: GlobalState) => {
const {channelURL, channelDisplayName} = getChannelUrlAndDisplayName(state, channel);
+ const callThreadID = threadIDForCallInChannel(state, channel?.id || '');
+
return {
currentUserID,
channel,
@@ -60,6 +74,7 @@ const mapStateToProps = (state: GlobalState) => {
callHostID: hostIDForCurrentCall(state),
callHostChangeAt: hostChangeAtForCurrentCall(state),
callRecording: recordingForCurrentCall(state),
+ isRecording: isRecordingInCurrentCall(state),
screenSharingSession,
allowScreenSharing: allowScreenSharing(state),
show: !expandedView(state),
@@ -69,6 +84,8 @@ const mapStateToProps = (state: GlobalState) => {
callsIncoming: sortedIncomingCalls(state),
transcriptionsEnabled: transcriptionsEnabled(state),
clientConnecting: clientConnecting(state),
+ callThreadID,
+ recordingsEnabled: recordingsEnabled(state),
};
};
@@ -77,6 +94,10 @@ const mapDispatchToProps = (dispatch: Dispatch) => bindActionCreators({
showScreenSourceModal,
trackEvent,
recordingPromptDismissedAt,
+ selectRHSPost,
+ startCallRecording,
+ stopCallRecording,
+ openModal: modals?.openModal,
}, dispatch);
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(CallWidget));
diff --git a/webapp/src/components/expanded_view/component.tsx b/webapp/src/components/expanded_view/component.tsx
index fa26bcbd1..519eae15b 100644
--- a/webapp/src/components/expanded_view/component.tsx
+++ b/webapp/src/components/expanded_view/component.tsx
@@ -321,6 +321,7 @@ export default class ExpandedView extends React.PureComponent
{
// Set the inter-window actions
window.callActions = {
setRecordingPromptDismissedAt: this.props.recordingPromptDismissedAt,
+ setMissingScreenPermissions: this.setMissingScreenPermissions,
};
// Set the current state
@@ -345,6 +346,23 @@ export default class ExpandedView extends React.PureComponent {
this.screenPlayer = node;
};
+ setMissingScreenPermissions = (missing: boolean, forward?: boolean) => {
+ this.setState({
+ alerts: {
+ ...this.state.alerts,
+ missingScreenPermissions: {
+ ...this.state.alerts.missingScreenPermissions,
+ active: missing,
+ show: missing,
+ },
+ },
+ });
+
+ if (forward && window.opener?.callActions?.setMissingScreenPermissions) {
+ window.opener.callActions.setMissingScreenPermissions(missing);
+ }
+ };
+
handleBlur = () => {
if (this.pushToTalk) {
getCallsClient()?.mute();
@@ -470,7 +488,6 @@ export default class ExpandedView extends React.PureComponent {
dialogType: StopRecordingConfirmation,
dialogProps: {
channelID: this.props.channel.id,
- transcriptionsEnabled: this.props.transcriptionsEnabled,
},
});
this.props.trackEvent(Telemetry.Event.StopRecording, Telemetry.Source.ExpandedView, {initiator: fromShortcut ? 'shortcut' : 'button'});
@@ -501,35 +518,18 @@ export default class ExpandedView extends React.PureComponent {
// DEPRECATED: legacy Desktop API logic (<= 5.6.0)
sendDesktopEvent('desktop-sources-modal-request');
} else {
- const state = {} as State;
const stream = await getScreenStream('', hasExperimentalFlag());
if (window.opener && stream) {
window.screenSharingTrackId = stream.getVideoTracks()[0].id;
}
if (stream) {
callsClient?.setScreenStream(stream);
- state.screenStream = stream;
- state.alerts = {
- ...this.state.alerts,
- missingScreenPermissions: {
- ...this.state.alerts.missingScreenPermissions,
- active: false,
- show: false,
- },
- };
+ this.setState({screenStream: stream});
+ this.setMissingScreenPermissions(false, true);
} else {
- state.alerts = {
- ...this.state.alerts,
- missingScreenPermissions: {
- ...this.state.alerts.missingScreenPermissions,
- active: true,
- show: true,
- },
- };
+ this.setMissingScreenPermissions(true, true);
}
-
- this.setState(state);
}
this.props.trackEvent(Telemetry.Event.ShareScreen, Telemetry.Source.ExpandedView, {initiator: fromShortcut ? 'shortcut' : 'button'});
}
@@ -692,6 +692,10 @@ export default class ExpandedView extends React.PureComponent {
this.props.prefetchThread(this.props.threadID);
}
}
+
+ if (window.opener.currentCallData.missingScreenPermissions) {
+ this.setMissingScreenPermissions(true);
+ }
}
callsClient.on('mos', (mos: number) => {
@@ -1091,10 +1095,6 @@ export default class ExpandedView extends React.PureComponent {
- {untranslatable('•')}
-
- {formatMessage({defaultMessage: '{count, plural, =1 {# participant} other {# participants}}'}, {count: this.props.sessions.length})}
-
@@ -1163,6 +1163,7 @@ export default class ExpandedView extends React.PureComponent
{
}}
/>
}
+ text={`${this.props.sessions.length}`}
/>
@@ -1216,6 +1217,12 @@ export default class ExpandedView extends React.PureComponent {
/>
}
+
+
{isHost && this.props.recordingsEnabled &&
{
/>
}
-
-
{globalRhsSupported && (
`
display: flex;
- flex-direction: column;
align-items: center;
justify-content: center;
+ gap: 6px;
margin: ${({$margin}) => $margin || '0'};
border-radius: 8px;
padding: 12px;
border: none;
background: ${({$bgColor}) => $bgColor || 'rgba(var(--button-color-rgb), 0.08)'};
+ color: ${({$fill}) => $fill || 'rgba(var(--button-color-rgb), 0.56)'};
&:hover {
background: ${({$bgColorHover}) => $bgColorHover || 'rgba(var(--button-color-rgb), 0.12)'};
background-blend-mode: multiply;
+ color: ${({$fillHover}) => $fillHover || 'var(--button-color)'};
svg {
fill: ${({$fillHover}) => $fillHover || 'var(--button-color)'};
@@ -168,9 +170,9 @@ const ButtonContainer = styled.button`
`;
const ButtonText = styled.span`
- font-size: 14px;
+ font-size: 16px;
+ line-height: 16px;
font-weight: 600;
- margin-top: 12px;
`;
const ButtonIcon = styled.div`
diff --git a/webapp/src/components/expanded_view/stop_recording_confirmation.tsx b/webapp/src/components/expanded_view/stop_recording_confirmation.tsx
index 61c81fc5c..a98a967b0 100644
--- a/webapp/src/components/expanded_view/stop_recording_confirmation.tsx
+++ b/webapp/src/components/expanded_view/stop_recording_confirmation.tsx
@@ -1,25 +1,27 @@
import React, {ComponentProps} from 'react';
import {useIntl} from 'react-intl';
+import {useSelector} from 'react-redux';
import {stopCallRecording} from 'src/actions';
import GenericModal from 'src/components/generic_modal';
+import {transcriptionsEnabled} from 'src/selectors';
import styled from 'styled-components';
export const IDStopRecordingConfirmation = 'stop_recording_confirmation';
type Props = Partial> & {
channelID: string;
- transcriptionsEnabled: boolean;
};
-export const StopRecordingConfirmation = ({channelID, transcriptionsEnabled, ...modalProps}: Props) => {
+export const StopRecordingConfirmation = ({channelID, ...modalProps}: Props) => {
const {formatMessage} = useIntl();
+ const hasTranscriptions = useSelector(transcriptionsEnabled);
let stopRecordingText = formatMessage({defaultMessage: 'Stop recording'});
let bodyText = formatMessage({defaultMessage: 'The call recording will be processed and posted in the call thread. Are you sure you want to stop the recording?'});
const cancelText = formatMessage({defaultMessage: 'Cancel'});
const confirmText = formatMessage({defaultMessage: 'Stop recording'});
- if (transcriptionsEnabled) {
+ if (hasTranscriptions) {
stopRecordingText = formatMessage({defaultMessage: 'Stop recording and transcription'});
bodyText = formatMessage({defaultMessage: 'The call recording and transcription files will be processed and posted in the call thread. Are you sure you want to stop the recording and transcription?'});
}
diff --git a/webapp/src/index.tsx b/webapp/src/index.tsx
index 751d64699..50e0889ec 100644
--- a/webapp/src/index.tsx
+++ b/webapp/src/index.tsx
@@ -10,6 +10,7 @@ import {getConfig, getServerVersion} from 'mattermost-redux/selectors/entities/g
import {getCurrentUserLocale} from 'mattermost-redux/selectors/entities/i18n';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentUserId, isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users';
+import {ActionFuncAsync} from 'mattermost-redux/types/actions';
import React, {useEffect} from 'react';
import ReactDOM from 'react-dom';
import {injectIntl, IntlProvider} from 'react-intl';
@@ -24,6 +25,7 @@ import {
getCallsStats,
incomingCallOnChannel,
loadProfilesByIdsIfMissing,
+ selectRHSPost,
setClientConnecting,
showScreenSourceModal,
showSwitchCallModal,
@@ -55,6 +57,10 @@ import UDPServerAddress from 'src/components/admin_console_settings/udp_server_a
import UDPServerPort from 'src/components/admin_console_settings/udp_server_port';
import {PostTypeCloudTrialRequest} from 'src/components/custom_post_types/post_type_cloud_trial_request';
import {PostTypeRecording} from 'src/components/custom_post_types/post_type_recording';
+import {
+ IDStopRecordingConfirmation,
+ StopRecordingConfirmation,
+} from 'src/components/expanded_view/stop_recording_confirmation';
import {IncomingCallContainer} from 'src/components/incoming_calls/call_container';
import RecordingsFilePreview from 'src/components/recordings_file_preview';
import {CALL_RECORDING_POST_TYPE, CALL_START_POST_TYPE, CALL_TRANSCRIPTION_POST_TYPE, DisabledCallsErr} from 'src/constants';
@@ -62,6 +68,7 @@ import {desktopNotificationHandler} from 'src/desktop_notifications';
import RestClient from 'src/rest_client';
import slashCommandsHandler from 'src/slash_commands';
import {CallActions, CurrentCallData, CurrentCallDataDefault} from 'src/types/types';
+import {modals} from 'src/webapp_globals';
import {
CALL_STATE,
@@ -469,6 +476,28 @@ export default class Plugin {
}));
}
+ if (window.desktopAPI?.onOpenThreadForCalls) {
+ logDebug('registering desktopAPI.onOpenThreadForCalls');
+ this.unsubscribers.push(window.desktopAPI.onOpenThreadForCalls((threadID: string) => {
+ logDebug('desktopAPI.onOpenThreadForCalls');
+ store.dispatch(selectRHSPost(threadID));
+ }));
+ }
+
+ if (window.desktopAPI?.onOpenStopRecordingModal) {
+ logDebug('registering desktopAPI.onOpenStopRecordingModal');
+ this.unsubscribers.push(window.desktopAPI.onOpenStopRecordingModal((channelID: string) => {
+ logDebug('desktopAPI.onOpenStopRecordingModal');
+ store.dispatch(modals.openModal({
+ modalId: IDStopRecordingConfirmation,
+ dialogType: StopRecordingConfirmation,
+ dialogProps: {
+ channelID,
+ },
+ }));
+ }));
+ }
+
const connectCall = async (channelID: string, title?: string, rootId?: string) => {
// Desktop handler
const payload = {
@@ -931,6 +960,7 @@ declare global {
ProductApi: {
useWebSocketClient: () => WebSocketClient,
WebSocketProvider: React.Context,
+ selectRhsPost: (postId: string) => ActionFuncAsync,
};
}
diff --git a/webapp/src/types/types.ts b/webapp/src/types/types.ts
index 42fa487d3..29f9ff63a 100644
--- a/webapp/src/types/types.ts
+++ b/webapp/src/types/types.ts
@@ -138,16 +138,19 @@ export type CapturerSource = {
// Reminder: obviously this is not reactive; setting data will not update the other window.
export type CurrentCallData = {
recordingPromptDismissedAt: number;
+ missingScreenPermissions: boolean;
}
export const CurrentCallDataDefault: CurrentCallData = {
recordingPromptDismissedAt: 0,
+ missingScreenPermissions: false,
};
// Similar to currentCallData, callActions is a cross-window function to trigger a change in that
// owning window. recordingPromptDismissedAt should be set by that window's init function or constructor.
export type CallActions = {
setRecordingPromptDismissedAt: (callId: string, dismissedAt: number) => void;
+ setMissingScreenPermissions: (missing: boolean) => void;
}
export enum ChannelType {