diff --git a/.gitignore b/.gitignore index 0d4826642..990f32379 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ webapp/src/manifest.ts webapp/mattermost-webapp/ server/manifest.go server/data/ +server/assets/i18n # e2e config/ diff --git a/e2e/tests/recordings.spec.ts b/e2e/tests/recordings.spec.ts index 90e418098..79465235c 100644 --- a/e2e/tests/recordings.spec.ts +++ b/e2e/tests/recordings.spec.ts @@ -287,4 +287,47 @@ test.describe('call recordings, transcriptions, live-captions', () => { await expect(page.locator('.ThreadViewer').locator('.post__header').last()).toContainText('BOT'); await expect(page.locator('.ThreadViewer').locator('.post__body').last().filter({has: page.getByTestId('fileAttachmentList')})).toBeVisible(); }); + + test('recording - widget menu', async ({page}) => { + // start call + const devPage = new PlaywrightDevPage(page); + await devPage.startCall(); + + // Open menu + await page.locator('#calls-widget-toggle-menu-button').click(); + + // Verify record menu item has the expected text + await expect(page.locator('#calls-widget-menu-record-button')).toContainText('Record call'); + + // Click to start recording + await page.locator('#calls-widget-menu-record-button').click(); + + // Verify menu closed + await expect(page.getByTestId('calls-widget-menu')).toBeHidden(); + + // Verify recording start prompt renders correctly + await expect(page.getByTestId('calls-widget-banner-recording')).toBeVisible(); + + // Give it a few of seconds to produce a decent recording + await devPage.wait(4000); + + // Open menu + await page.locator('#calls-widget-toggle-menu-button').click(); + + // Verify record menu item has the expected text + await expect(page.locator('#calls-widget-menu-record-button')).toContainText('Stop recording'); + + // Click to stop recording + await page.locator('#calls-widget-menu-record-button').click(); + + // Verify menu closed + await expect(page.getByTestId('calls-widget-menu')).toBeHidden(); + + // Stop recording confirmation + await expect(page.locator('#stop_recording_confirmation')).toBeVisible(); + await page.getByTestId('modal-confirm-button').click(); + + // Leave call + await devPage.leaveCall(); + }); }); diff --git a/e2e/tests/start_call.spec.ts b/e2e/tests/start_call.spec.ts index 29cd67254..bbf07282a 100644 --- a/e2e/tests/start_call.spec.ts +++ b/e2e/tests/start_call.spec.ts @@ -523,3 +523,31 @@ test.describe('permissions', () => { await expect(resp.status()).toEqual(201); }); }); + +test.describe('widget menu', () => { + test.use({storageState: userStorages[0]}); + + test('menu button should open call thread', async ({page}) => { + const devPage = new PlaywrightDevPage(page); + await devPage.startCall(); + + // Verify RHS is closed. + await expect(page.locator('#rhsContainer')).toBeHidden(); + + // Open menu + await page.locator('#calls-widget-toggle-menu-button').click(); + + // Click to show chat + await page.locator('#calls-widget-menu-chat-button').click(); + + // Verify menu closed + await expect(page.getByTestId('calls-widget-menu')).toBeHidden(); + + // Verify RHS is open and call thread is showing. + await expect(page.locator('#rhsContainer')).toBeVisible(); + await expect(page.locator('#rhsContainer').filter({has: page.getByText('Call started')})).toBeVisible(); + await expect(page.locator('#rhsContainer').filter({has: page.getByText(`by ${usernames[0]}`)})).toBeVisible(); + + await devPage.leaveCall(); + }); +}); diff --git a/standalone/package-lock.json b/standalone/package-lock.json index 94602ab8c..3c2374d54 100644 --- a/standalone/package-lock.json +++ b/standalone/package-lock.json @@ -33,7 +33,7 @@ "@babel/preset-typescript": "7.16.0", "@formatjs/cli": "6.0.4", "@mattermost/client": "file:../webapp/mattermost-webapp/webapp/platform/client", - "@mattermost/desktop-api": "5.7.0-3", + "@mattermost/desktop-api": "5.9.0-1", "@mattermost/eslint-plugin": "1.1.0-0", "@mattermost/types": "file:../webapp/mattermost-webapp/webapp/platform/types", "@types/jest": "27.0.2", @@ -3101,11 +3101,12 @@ "license": "MIT" }, "node_modules/@mattermost/desktop-api": { - "version": "5.7.0-3", + "version": "5.9.0-1", + "resolved": "https://registry.npmjs.org/@mattermost/desktop-api/-/desktop-api-5.9.0-1.tgz", + "integrity": "sha512-huy04alxgb8la1d6MaRwMWC5o8fhlK8K8vb7qaRgQBRO3qnQL3CK8R89CU8WxNQIf0NdrqKqTannOT+VQsbnDg==", "dev": true, - "license": "MIT", "peerDependencies": { - "typescript": "^4.3" + "typescript": "^4.3.0 || ^5.0.0" }, "peerDependenciesMeta": { "typescript": { diff --git a/standalone/package.json b/standalone/package.json index ae372f35b..1e5db1bf4 100644 --- a/standalone/package.json +++ b/standalone/package.json @@ -46,7 +46,7 @@ "@babel/preset-typescript": "7.16.0", "@formatjs/cli": "6.0.4", "@mattermost/client": "file:../webapp/mattermost-webapp/webapp/platform/client", - "@mattermost/desktop-api": "5.7.0-3", + "@mattermost/desktop-api": "5.9.0-1", "@mattermost/eslint-plugin": "1.1.0-0", "@mattermost/types": "file:../webapp/mattermost-webapp/webapp/platform/types", "@types/jest": "27.0.2", diff --git a/standalone/src/init.ts b/standalone/src/init.ts index ccc81f0ea..61256e394 100644 --- a/standalone/src/init.ts +++ b/standalone/src/init.ts @@ -36,6 +36,7 @@ import {getChannel} from 'mattermost-redux/selectors/entities/channels'; import {getConfig} from 'mattermost-redux/selectors/entities/general'; import {getTheme, Theme} from 'mattermost-redux/selectors/entities/preferences'; import configureStore from 'mattermost-redux/store'; +import {ActionFuncAsync} from 'mattermost-redux/types/actions'; import {getCallActive, getCallsConfig, setClientConnecting} from 'plugin/actions'; import CallsClient from 'plugin/client'; import { @@ -339,6 +340,9 @@ declare global { e2eNotificationsSoundStoppedAt?: number[], e2eRingLength?: number, WebappUtils: WebAppUtils, + ProductApi: { + selectRhsPost: (postId: string) => ActionFuncAsync, + }, } interface HTMLVideoElement { diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index d22ee7613..b5c82b3d2 100644 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -33,6 +33,7 @@ "7cVXct": "Show participants", "7gVR9m": "An internal error occurred and prevented you from joining the call. Please try again.", "83mzYJ": "Call quality may be degraded due to unstable network conditions.", + "8JGf6X": "Show chat thread", "8isok9": "Mute participant", "94UiTg": "Recording is in progress", "9I3kDh": "Recording and transcription has stopped. Processing…", @@ -105,7 +106,6 @@ "TyskrG": "Welcome to your Mattermost Enterprise trial! It expires on {trialExpirationDate}. You now have access to Call recordings,RTCD services, guest accounts, automated compliance reports, and mobile secure-ID push notifications, among many other features. View all features in our documentation.", "UcFeI7": "Show participants list", "Ug/N7H": "Calls are not available in a DM with a deactivated user.", - "UxatAw": "{count, plural, =1 {# participant} other {# participants}}", "Uys4Mj": "Close reactions", "VLwHR0": "Call settings", "Vc8/fR": "Total Active Sessions", diff --git a/webapp/package-lock.json b/webapp/package-lock.json index a4e38d90f..9daab82b8 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -38,7 +38,7 @@ "@babel/preset-typescript": "7.16.0", "@formatjs/cli": "5.0.7", "@mattermost/client": "file:mattermost-webapp/webapp/platform/client", - "@mattermost/desktop-api": "5.7.0-3", + "@mattermost/desktop-api": "5.9.0-1", "@mattermost/eslint-plugin": "1.1.0-0", "@mattermost/types": "file:mattermost-webapp/webapp/platform/types", "@types/jest": "27.0.2", @@ -8310,12 +8310,12 @@ "dev": true }, "node_modules/@mattermost/desktop-api": { - "version": "5.7.0-3", - "resolved": "https://registry.npmjs.org/@mattermost/desktop-api/-/desktop-api-5.7.0-3.tgz", - "integrity": "sha512-yT6PQAENaGSnTmJp6NVHXERNllN8N9jULvQmiOlEwHSi/VpDniTIfbPFA6aij8J9OB2BmhjHEaIHtra5vVdtDw==", + "version": "5.9.0-1", + "resolved": "https://registry.npmjs.org/@mattermost/desktop-api/-/desktop-api-5.9.0-1.tgz", + "integrity": "sha512-huy04alxgb8la1d6MaRwMWC5o8fhlK8K8vb7qaRgQBRO3qnQL3CK8R89CU8WxNQIf0NdrqKqTannOT+VQsbnDg==", "dev": true, "peerDependencies": { - "typescript": "^4.3" + "typescript": "^4.3.0 || ^5.0.0" }, "peerDependenciesMeta": { "typescript": { diff --git a/webapp/package.json b/webapp/package.json index 6c0a26c56..ecc70c642 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -51,7 +51,7 @@ "@babel/preset-typescript": "7.16.0", "@formatjs/cli": "5.0.7", "@mattermost/client": "file:mattermost-webapp/webapp/platform/client", - "@mattermost/desktop-api": "5.7.0-3", + "@mattermost/desktop-api": "5.9.0-1", "@mattermost/eslint-plugin": "1.1.0-0", "@mattermost/types": "file:mattermost-webapp/webapp/platform/types", "@types/jest": "27.0.2", diff --git a/webapp/src/actions.ts b/webapp/src/actions.ts index dadcd5072..e7f5020ae 100644 --- a/webapp/src/actions.ts +++ b/webapp/src/actions.ts @@ -671,3 +671,12 @@ export const hostMuteOthers = async (callID?: string) => { export const getCallsStats = async () => { return RestClient.fetch(`${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 {